Add support for button entities (#2824)

This commit is contained in:
Jesse Hills 2021-11-30 08:00:51 +13:00 committed by GitHub
parent f50e40e0b8
commit b5639a6472
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 697 additions and 0 deletions

View file

@ -30,6 +30,7 @@ esphome/components/bang_bang/* @OttoWinter
esphome/components/binary_sensor/* @esphome/core esphome/components/binary_sensor/* @esphome/core
esphome/components/ble_client/* @buxtronix esphome/components/ble_client/* @buxtronix
esphome/components/bme680_bsec/* @trvrnrth esphome/components/bme680_bsec/* @trvrnrth
esphome/components/button/* @esphome/core
esphome/components/canbus/* @danielschramm @mvturnho esphome/components/canbus/* @danielschramm @mvturnho
esphome/components/cap1188/* @MrEditor97 esphome/components/cap1188/* @MrEditor97
esphome/components/captive_portal/* @OttoWinter esphome/components/captive_portal/* @OttoWinter

View file

@ -40,6 +40,7 @@ service APIConnection {
rpc climate_command (ClimateCommandRequest) returns (void) {} rpc climate_command (ClimateCommandRequest) returns (void) {}
rpc number_command (NumberCommandRequest) returns (void) {} rpc number_command (NumberCommandRequest) returns (void) {}
rpc select_command (SelectCommandRequest) returns (void) {} rpc select_command (SelectCommandRequest) returns (void) {}
rpc button_command (ButtonCommandRequest) returns (void) {}
} }
@ -944,3 +945,27 @@ message SelectCommandRequest {
fixed32 key = 1; fixed32 key = 1;
string state = 2; string state = 2;
} }
// ==================== BUTTON ====================
message ListEntitiesButtonResponse {
option (id) = 61;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_BUTTON";
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
string icon = 5;
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
}
message ButtonCommandRequest {
option (id) = 62;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_BUTTON";
option (no_delay) = true;
fixed32 key = 1;
}

View file

@ -674,6 +674,27 @@ void APIConnection::select_command(const SelectCommandRequest &msg) {
} }
#endif #endif
#ifdef USE_BUTTON
bool APIConnection::send_button_info(button::Button *button) {
ListEntitiesButtonResponse msg;
msg.key = button->get_object_id_hash();
msg.object_id = button->get_object_id();
msg.name = button->get_name();
msg.unique_id = get_default_unique_id("button", button);
msg.icon = button->get_icon();
msg.disabled_by_default = button->is_disabled_by_default();
msg.entity_category = static_cast<enums::EntityCategory>(button->get_entity_category());
return this->send_list_entities_button_response(msg);
}
void APIConnection::button_command(const ButtonCommandRequest &msg) {
button::Button *button = App.get_button_by_key(msg.key);
if (button == nullptr)
return;
button->press();
}
#endif
#ifdef USE_ESP32_CAMERA #ifdef USE_ESP32_CAMERA
void APIConnection::send_camera_state(std::shared_ptr<esp32_camera::CameraImage> image) { void APIConnection::send_camera_state(std::shared_ptr<esp32_camera::CameraImage> image) {
if (!this->state_subscription_) if (!this->state_subscription_)

View file

@ -73,6 +73,10 @@ class APIConnection : public APIServerConnection {
bool send_select_state(select::Select *select, std::string state); bool send_select_state(select::Select *select, std::string state);
bool send_select_info(select::Select *select); bool send_select_info(select::Select *select);
void select_command(const SelectCommandRequest &msg) override; void select_command(const SelectCommandRequest &msg) override;
#endif
#ifdef USE_BUTTON
bool send_button_info(button::Button *button);
void button_command(const ButtonCommandRequest &msg) override;
#endif #endif
bool send_log_message(int level, const char *tag, const char *line); bool send_log_message(int level, const char *tag, const char *line);
void send_homeassistant_service_call(const HomeassistantServiceResponse &call) { void send_homeassistant_service_call(const HomeassistantServiceResponse &call) {

View file

@ -4147,6 +4147,118 @@ void SelectCommandRequest::dump_to(std::string &out) const {
out.append("}"); out.append("}");
} }
#endif #endif
bool ListEntitiesButtonResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 6: {
this->disabled_by_default = value.as_bool();
return true;
}
case 7: {
this->entity_category = value.as_enum<enums::EntityCategory>();
return true;
}
default:
return false;
}
}
bool ListEntitiesButtonResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 1: {
this->object_id = value.as_string();
return true;
}
case 3: {
this->name = value.as_string();
return true;
}
case 4: {
this->unique_id = value.as_string();
return true;
}
case 5: {
this->icon = value.as_string();
return true;
}
default:
return false;
}
}
bool ListEntitiesButtonResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
switch (field_id) {
case 2: {
this->key = value.as_fixed32();
return true;
}
default:
return false;
}
}
void ListEntitiesButtonResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(1, this->object_id);
buffer.encode_fixed32(2, this->key);
buffer.encode_string(3, this->name);
buffer.encode_string(4, this->unique_id);
buffer.encode_string(5, this->icon);
buffer.encode_bool(6, this->disabled_by_default);
buffer.encode_enum<enums::EntityCategory>(7, this->entity_category);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void ListEntitiesButtonResponse::dump_to(std::string &out) const {
char buffer[64];
out.append("ListEntitiesButtonResponse {\n");
out.append(" object_id: ");
out.append("'").append(this->object_id).append("'");
out.append("\n");
out.append(" key: ");
sprintf(buffer, "%u", this->key);
out.append(buffer);
out.append("\n");
out.append(" name: ");
out.append("'").append(this->name).append("'");
out.append("\n");
out.append(" unique_id: ");
out.append("'").append(this->unique_id).append("'");
out.append("\n");
out.append(" icon: ");
out.append("'").append(this->icon).append("'");
out.append("\n");
out.append(" disabled_by_default: ");
out.append(YESNO(this->disabled_by_default));
out.append("\n");
out.append(" entity_category: ");
out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category));
out.append("\n");
out.append("}");
}
#endif
bool ButtonCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
switch (field_id) {
case 1: {
this->key = value.as_fixed32();
return true;
}
default:
return false;
}
}
void ButtonCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); }
#ifdef HAS_PROTO_MESSAGE_DUMP
void ButtonCommandRequest::dump_to(std::string &out) const {
char buffer[64];
out.append("ButtonCommandRequest {\n");
out.append(" key: ");
sprintf(buffer, "%u", this->key);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
} // namespace api } // namespace api
} // namespace esphome } // namespace esphome

View file

@ -1041,6 +1041,36 @@ class SelectCommandRequest : public ProtoMessage {
bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
}; };
class ListEntitiesButtonResponse : public ProtoMessage {
public:
std::string object_id{};
uint32_t key{0};
std::string name{};
std::string unique_id{};
std::string icon{};
bool disabled_by_default{false};
enums::EntityCategory entity_category{};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class ButtonCommandRequest : public ProtoMessage {
public:
uint32_t key{0};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
};
} // namespace api } // namespace api
} // namespace esphome } // namespace esphome

View file

@ -282,6 +282,16 @@ bool APIServerConnectionBase::send_select_state_response(const SelectStateRespon
#endif #endif
#ifdef USE_SELECT #ifdef USE_SELECT
#endif #endif
#ifdef USE_BUTTON
bool APIServerConnectionBase::send_list_entities_button_response(const ListEntitiesButtonResponse &msg) {
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "send_list_entities_button_response: %s", msg.dump().c_str());
#endif
return this->send_message_<ListEntitiesButtonResponse>(msg, 61);
}
#endif
#ifdef USE_BUTTON
#endif
bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) { bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {
switch (msg_type) { switch (msg_type) {
case 1: { case 1: {
@ -513,6 +523,17 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
ESP_LOGVV(TAG, "on_select_command_request: %s", msg.dump().c_str()); ESP_LOGVV(TAG, "on_select_command_request: %s", msg.dump().c_str());
#endif #endif
this->on_select_command_request(msg); this->on_select_command_request(msg);
#endif
break;
}
case 62: {
#ifdef USE_BUTTON
ButtonCommandRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_button_command_request: %s", msg.dump().c_str());
#endif
this->on_button_command_request(msg);
#endif #endif
break; break;
} }
@ -737,6 +758,19 @@ void APIServerConnection::on_select_command_request(const SelectCommandRequest &
this->select_command(msg); this->select_command(msg);
} }
#endif #endif
#ifdef USE_BUTTON
void APIServerConnection::on_button_command_request(const ButtonCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->button_command(msg);
}
#endif
} // namespace api } // namespace api
} // namespace esphome } // namespace esphome

View file

@ -129,6 +129,12 @@ class APIServerConnectionBase : public ProtoService {
#endif #endif
#ifdef USE_SELECT #ifdef USE_SELECT
virtual void on_select_command_request(const SelectCommandRequest &value){}; virtual void on_select_command_request(const SelectCommandRequest &value){};
#endif
#ifdef USE_BUTTON
bool send_list_entities_button_response(const ListEntitiesButtonResponse &msg);
#endif
#ifdef USE_BUTTON
virtual void on_button_command_request(const ButtonCommandRequest &value){};
#endif #endif
protected: protected:
bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override; bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;
@ -171,6 +177,9 @@ class APIServerConnection : public APIServerConnectionBase {
#endif #endif
#ifdef USE_SELECT #ifdef USE_SELECT
virtual void select_command(const SelectCommandRequest &msg) = 0; virtual void select_command(const SelectCommandRequest &msg) = 0;
#endif
#ifdef USE_BUTTON
virtual void button_command(const ButtonCommandRequest &msg) = 0;
#endif #endif
protected: protected:
void on_hello_request(const HelloRequest &msg) override; void on_hello_request(const HelloRequest &msg) override;
@ -209,6 +218,9 @@ class APIServerConnection : public APIServerConnectionBase {
#ifdef USE_SELECT #ifdef USE_SELECT
void on_select_command_request(const SelectCommandRequest &msg) override; void on_select_command_request(const SelectCommandRequest &msg) override;
#endif #endif
#ifdef USE_BUTTON
void on_button_command_request(const ButtonCommandRequest &msg) override;
#endif
}; };
} // namespace api } // namespace api

View file

@ -27,6 +27,9 @@ bool ListEntitiesIterator::on_sensor(sensor::Sensor *sensor) { return this->clie
#ifdef USE_SWITCH #ifdef USE_SWITCH
bool ListEntitiesIterator::on_switch(switch_::Switch *a_switch) { return this->client_->send_switch_info(a_switch); } bool ListEntitiesIterator::on_switch(switch_::Switch *a_switch) { return this->client_->send_switch_info(a_switch); }
#endif #endif
#ifdef USE_BUTTON
bool ListEntitiesIterator::on_button(button::Button *button) { return this->client_->send_button_info(button); }
#endif
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
bool ListEntitiesIterator::on_text_sensor(text_sensor::TextSensor *text_sensor) { bool ListEntitiesIterator::on_text_sensor(text_sensor::TextSensor *text_sensor) {
return this->client_->send_text_sensor_info(text_sensor); return this->client_->send_text_sensor_info(text_sensor);

View file

@ -30,6 +30,9 @@ class ListEntitiesIterator : public ComponentIterator {
#ifdef USE_SWITCH #ifdef USE_SWITCH
bool on_switch(switch_::Switch *a_switch) override; bool on_switch(switch_::Switch *a_switch) override;
#endif #endif
#ifdef USE_BUTTON
bool on_button(button::Button *button) override;
#endif
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
bool on_text_sensor(text_sensor::TextSensor *text_sensor) override; bool on_text_sensor(text_sensor::TextSensor *text_sensor) override;
#endif #endif

View file

@ -31,6 +31,9 @@ class InitialStateIterator : public ComponentIterator {
#ifdef USE_SWITCH #ifdef USE_SWITCH
bool on_switch(switch_::Switch *a_switch) override; bool on_switch(switch_::Switch *a_switch) override;
#endif #endif
#ifdef USE_BUTTON
bool on_button(button::Button *button) override { return true; };
#endif
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
bool on_text_sensor(text_sensor::TextSensor *text_sensor) override; bool on_text_sensor(text_sensor::TextSensor *text_sensor) override;
#endif #endif

View file

@ -116,6 +116,21 @@ void ComponentIterator::advance() {
} }
break; break;
#endif #endif
#ifdef USE_BUTTON
case IteratorState::BUTTON:
if (this->at_ >= App.get_buttons().size()) {
advance_platform = true;
} else {
auto *button = App.get_buttons()[this->at_];
if (button->is_internal()) {
success = true;
break;
} else {
success = this->on_button(button);
}
}
break;
#endif
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
case IteratorState::TEXT_SENSOR: case IteratorState::TEXT_SENSOR:
if (this->at_ >= App.get_text_sensors().size()) { if (this->at_ >= App.get_text_sensors().size()) {

View file

@ -38,6 +38,9 @@ class ComponentIterator {
#ifdef USE_SWITCH #ifdef USE_SWITCH
virtual bool on_switch(switch_::Switch *a_switch) = 0; virtual bool on_switch(switch_::Switch *a_switch) = 0;
#endif #endif
#ifdef USE_BUTTON
virtual bool on_button(button::Button *button) = 0;
#endif
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
virtual bool on_text_sensor(text_sensor::TextSensor *text_sensor) = 0; virtual bool on_text_sensor(text_sensor::TextSensor *text_sensor) = 0;
#endif #endif
@ -78,6 +81,9 @@ class ComponentIterator {
#ifdef USE_SWITCH #ifdef USE_SWITCH
SWITCH, SWITCH,
#endif #endif
#ifdef USE_BUTTON
BUTTON,
#endif
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
TEXT_SENSOR, TEXT_SENSOR,
#endif #endif

View file

@ -0,0 +1,104 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import automation
from esphome.automation import maybe_simple_id
from esphome.components import mqtt
from esphome.const import (
CONF_ENTITY_CATEGORY,
CONF_ICON,
CONF_ID,
CONF_ON_PRESS,
CONF_TRIGGER_ID,
CONF_MQTT_ID,
)
from esphome.core import CORE, coroutine_with_priority
from esphome.cpp_helpers import setup_entity
CODEOWNERS = ["@esphome/core"]
IS_PLATFORM_COMPONENT = True
button_ns = cg.esphome_ns.namespace("button")
Button = button_ns.class_("Button", cg.EntityBase)
ButtonPtr = Button.operator("ptr")
PressAction = button_ns.class_("PressAction", automation.Action)
ButtonPressTrigger = button_ns.class_(
"ButtonPressTrigger", automation.Trigger.template()
)
BUTTON_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend(
{
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTButtonComponent),
cv.Optional(CONF_ON_PRESS): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ButtonPressTrigger),
}
),
}
)
_UNDEF = object()
def button_schema(
icon: str = _UNDEF,
entity_category: str = _UNDEF,
) -> cv.Schema:
schema = BUTTON_SCHEMA
if icon is not _UNDEF:
schema = schema.extend({cv.Optional(CONF_ICON, default=icon): cv.icon})
if entity_category is not _UNDEF:
schema = schema.extend(
{
cv.Optional(
CONF_ENTITY_CATEGORY, default=entity_category
): cv.entity_category
}
)
return schema
async def setup_button_core_(var, config):
await setup_entity(var, config)
for conf in config.get(CONF_ON_PRESS, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
if CONF_MQTT_ID in config:
mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var)
await mqtt.register_mqtt_component(mqtt_, config)
async def register_button(var, config):
if not CORE.has_id(config[CONF_ID]):
var = cg.Pvariable(config[CONF_ID], var)
cg.add(cg.App.register_button(var))
await setup_button_core_(var, config)
async def new_button(config):
var = cg.new_Pvariable(config[CONF_ID])
await register_button(var, config)
return var
BUTTON_PRESS_SCHEMA = maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(Button),
}
)
@automation.register_action("button.press", PressAction, BUTTON_PRESS_SCHEMA)
async def button_press_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@coroutine_with_priority(100.0)
async def to_code(config):
cg.add_global(button_ns.using)
cg.add_define("USE_BUTTON")

View file

@ -0,0 +1,28 @@
#pragma once
#include "esphome/components/button/button.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
namespace esphome {
namespace button {
template<typename... Ts> class PressAction : public Action<Ts...> {
public:
explicit PressAction(Button *button) : button_(button) {}
void play(Ts... x) override { this->button_->press(); }
protected:
Button *button_;
};
class ButtonPressTrigger : public Trigger<> {
public:
ButtonPressTrigger(Button *button) {
button->add_on_press_callback([this]() { this->trigger(); });
}
};
} // namespace button
} // namespace esphome

View file

@ -0,0 +1,21 @@
#include "button.h"
#include "esphome/core/log.h"
namespace esphome {
namespace button {
static const char *const TAG = "button";
Button::Button(const std::string &name) : EntityBase(name) {}
Button::Button() : Button("") {}
void Button::press() {
ESP_LOGD(TAG, "'%s' Pressed.", this->get_name().c_str());
this->press_action();
this->press_callback_.call();
}
void Button::add_on_press_callback(std::function<void()> &&callback) { this->press_callback_.add(std::move(callback)); }
uint32_t Button::hash_base() { return 1495763804UL; }
} // namespace button
} // namespace esphome

View file

@ -0,0 +1,50 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/entity_base.h"
#include "esphome/core/helpers.h"
namespace esphome {
namespace button {
#define LOG_BUTTON(prefix, type, obj) \
if ((obj) != nullptr) { \
ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \
if (!(obj)->get_icon().empty()) { \
ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \
} \
}
/** Base class for all buttons.
*
* A button is just a momentary switch that does not have a state, only a trigger.
*/
class Button : public EntityBase {
public:
explicit Button();
explicit Button(const std::string &name);
/** Press this button. This is called by the front-end.
*
* For implementing buttons, please override press_action.
*/
void press();
/** Set callback for state changes.
*
* @param callback The void() callback.
*/
void add_on_press_callback(std::function<void()> &&callback);
protected:
/** You should implement this virtual method if you want to create your own button.
*/
virtual void press_action(){};
uint32_t hash_base() override;
CallbackManager<void()> press_callback_{};
};
} // namespace button
} // namespace esphome

View file

@ -95,6 +95,7 @@ MQTTSwitchComponent = mqtt_ns.class_("MQTTSwitchComponent", MQTTComponent)
MQTTTextSensor = mqtt_ns.class_("MQTTTextSensor", MQTTComponent) MQTTTextSensor = mqtt_ns.class_("MQTTTextSensor", MQTTComponent)
MQTTNumberComponent = mqtt_ns.class_("MQTTNumberComponent", MQTTComponent) MQTTNumberComponent = mqtt_ns.class_("MQTTNumberComponent", MQTTComponent)
MQTTSelectComponent = mqtt_ns.class_("MQTTSelectComponent", MQTTComponent) MQTTSelectComponent = mqtt_ns.class_("MQTTSelectComponent", MQTTComponent)
MQTTButtonComponent = mqtt_ns.class_("MQTTButtonComponent", MQTTComponent)
MQTTDiscoveryUniqueIdGenerator = mqtt_ns.enum("MQTTDiscoveryUniqueIdGenerator") MQTTDiscoveryUniqueIdGenerator = mqtt_ns.enum("MQTTDiscoveryUniqueIdGenerator")
MQTT_DISCOVERY_UNIQUE_ID_GENERATOR_OPTIONS = { MQTT_DISCOVERY_UNIQUE_ID_GENERATOR_OPTIONS = {

View file

@ -0,0 +1,40 @@
#include "mqtt_button.h"
#include "esphome/core/log.h"
#include "mqtt_const.h"
#ifdef USE_MQTT
#ifdef USE_BUTTON
namespace esphome {
namespace mqtt {
static const char *const TAG = "mqtt.button";
using namespace esphome::button;
MQTTButtonComponent::MQTTButtonComponent(button::Button *button) : MQTTComponent(), button_(button) {}
void MQTTButtonComponent::setup() {
this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) {
if (payload == "press") {
this->button_->press();
} else {
ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name().c_str(), payload.c_str());
this->status_momentary_warning("state", 5000);
}
});
}
void MQTTButtonComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT Button '%s': ", this->button_->get_name().c_str());
LOG_MQTT_COMPONENT(true, true);
}
std::string MQTTButtonComponent::component_type() const { return "button"; }
const EntityBase *MQTTButtonComponent::get_entity() const { return this->button_; }
} // namespace mqtt
} // namespace esphome
#endif
#endif // USE_MQTT

View file

@ -0,0 +1,40 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_MQTT
#ifdef USE_BUTTON
#include "esphome/components/button/button.h"
#include "mqtt_component.h"
namespace esphome {
namespace mqtt {
class MQTTButtonComponent : public mqtt::MQTTComponent {
public:
explicit MQTTButtonComponent(button::Button *button);
// ========== INTERNAL METHODS ==========
// (In most use cases you won't need these)
void setup() override;
void dump_config() override;
/// Buttons do not send a state so just return true.
bool send_initial_state() override { return true; }
void send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) override {}
protected:
/// "button" component type.
std::string component_type() const override;
const EntityBase *get_entity() const override;
button::Button *button_;
};
} // namespace mqtt
} // namespace esphome
#endif
#endif // USE_MQTT

View file

@ -0,0 +1,23 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import button
from esphome.const import (
CONF_ID,
ENTITY_CATEGORY_CONFIG,
ICON_RESTART,
)
restart_ns = cg.esphome_ns.namespace("restart")
RestartButton = restart_ns.class_("RestartButton", button.Button, cg.Component)
CONFIG_SCHEMA = (
button.button_schema(icon=ICON_RESTART, entity_category=ENTITY_CATEGORY_CONFIG)
.extend({cv.GenerateID(): cv.declare_id(RestartButton)})
.extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await button.register_button(var, config)

View file

@ -0,0 +1,20 @@
#include "restart_button.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome {
namespace restart {
static const char *const TAG = "restart.button";
void RestartButton::press_action() {
ESP_LOGI(TAG, "Restarting device...");
// Let MQTT settle a bit
delay(100); // NOLINT
App.safe_reboot();
}
void RestartButton::dump_config() { LOG_BUTTON("", "Restart Button", this); }
} // namespace restart
} // namespace esphome

View file

@ -0,0 +1,18 @@
#pragma once
#include "esphome/components/button/button.h"
#include "esphome/core/component.h"
namespace esphome {
namespace restart {
class RestartButton : public button::Button, public Component {
public:
void dump_config() override;
protected:
void press_action() override;
};
} // namespace restart
} // namespace esphome

View file

@ -0,0 +1,13 @@
import esphome.config_validation as cv
from esphome.components import button
CONFIG_SCHEMA = button.BUTTON_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(button.Button),
}
).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
await button.new_button(config)

View file

@ -198,6 +198,11 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) {
write_row(stream, obj, "switch", "<button>Toggle</button>"); write_row(stream, obj, "switch", "<button>Toggle</button>");
#endif #endif
#ifdef USE_BUTTON
for (auto *obj : App.get_buttons())
write_row(stream, obj, "button", "<button>Press</button>");
#endif
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
for (auto *obj : App.get_binary_sensors()) for (auto *obj : App.get_binary_sensors())
if (this->include_internal_ || !obj->is_internal()) if (this->include_internal_ || !obj->is_internal())
@ -382,6 +387,26 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM
} }
#endif #endif
#ifdef USE_BUTTON
void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (button::Button *obj : App.get_buttons()) {
if (obj->is_internal())
continue;
if (obj->get_object_id() != match.id)
continue;
if (request->method() == HTTP_POST && match.method == "press") {
this->defer([obj]() { obj->press(); });
request->send(200);
} else {
request->send(404);
}
return;
}
request->send(404);
}
#endif
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
void WebServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) { void WebServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) {
this->events_.send(this->binary_sensor_json(obj, state).c_str(), "state"); this->events_.send(this->binary_sensor_json(obj, state).c_str(), "state");
@ -715,6 +740,11 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) {
return true; return true;
#endif #endif
#ifdef USE_BUTTON
if (request->method() == HTTP_POST && match.domain == "button")
return true;
#endif
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
if (request->method() == HTTP_GET && match.domain == "binary_sensor") if (request->method() == HTTP_GET && match.domain == "binary_sensor")
return true; return true;
@ -787,6 +817,13 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) {
} }
#endif #endif
#ifdef USE_BUTTON
if (match.domain == "button") {
this->handle_button_request(request, match);
return;
}
#endif
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
if (match.domain == "binary_sensor") { if (match.domain == "binary_sensor") {
this->handle_binary_sensor_request(request, match); this->handle_binary_sensor_request(request, match);

View file

@ -112,6 +112,11 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
std::string switch_json(switch_::Switch *obj, bool value); std::string switch_json(switch_::Switch *obj, bool value);
#endif #endif
#ifdef USE_BUTTON
/// Handle a button request under '/button/<id>/press'.
void handle_button_request(AsyncWebServerRequest *request, const UrlMatch &match);
#endif
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
void on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) override; void on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) override;

View file

@ -17,6 +17,9 @@
#ifdef USE_SWITCH #ifdef USE_SWITCH
#include "esphome/components/switch/switch.h" #include "esphome/components/switch/switch.h"
#endif #endif
#ifdef USE_BUTTON
#include "esphome/components/button/button.h"
#endif
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
#include "esphome/components/text_sensor/text_sensor.h" #include "esphome/components/text_sensor/text_sensor.h"
#endif #endif
@ -67,6 +70,10 @@ class Application {
void register_switch(switch_::Switch *a_switch) { this->switches_.push_back(a_switch); } void register_switch(switch_::Switch *a_switch) { this->switches_.push_back(a_switch); }
#endif #endif
#ifdef USE_BUTTON
void register_button(button::Button *button) { this->buttons_.push_back(button); }
#endif
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
void register_text_sensor(text_sensor::TextSensor *sensor) { this->text_sensors_.push_back(sensor); } void register_text_sensor(text_sensor::TextSensor *sensor) { this->text_sensors_.push_back(sensor); }
#endif #endif
@ -167,6 +174,15 @@ class Application {
return nullptr; return nullptr;
} }
#endif #endif
#ifdef USE_BUTTON
const std::vector<button::Button *> &get_buttons() { return this->buttons_; }
button::Button *get_button_by_key(uint32_t key, bool include_internal = false) {
for (auto *obj : this->buttons_)
if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
return obj;
return nullptr;
}
#endif
#ifdef USE_SENSOR #ifdef USE_SENSOR
const std::vector<sensor::Sensor *> &get_sensors() { return this->sensors_; } const std::vector<sensor::Sensor *> &get_sensors() { return this->sensors_; }
sensor::Sensor *get_sensor_by_key(uint32_t key, bool include_internal = false) { sensor::Sensor *get_sensor_by_key(uint32_t key, bool include_internal = false) {
@ -260,6 +276,9 @@ class Application {
#ifdef USE_SWITCH #ifdef USE_SWITCH
std::vector<switch_::Switch *> switches_{}; std::vector<switch_::Switch *> switches_{};
#endif #endif
#ifdef USE_BUTTON
std::vector<button::Button *> buttons_{};
#endif
#ifdef USE_SENSOR #ifdef USE_SENSOR
std::vector<sensor::Sensor *> sensors_{}; std::vector<sensor::Sensor *> sensors_{};
#endif #endif

View file

@ -22,6 +22,9 @@
#ifdef USE_SWITCH #ifdef USE_SWITCH
#include "esphome/components/switch/switch.h" #include "esphome/components/switch/switch.h"
#endif #endif
#ifdef USE_BUTTON
#include "esphome/components/button/button.h"
#endif
#ifdef USE_CLIMATE #ifdef USE_CLIMATE
#include "esphome/components/climate/climate.h" #include "esphome/components/climate/climate.h"
#endif #endif

View file

@ -16,6 +16,7 @@
#define USE_API_NOISE #define USE_API_NOISE
#define USE_API_PLAINTEXT #define USE_API_PLAINTEXT
#define USE_BINARY_SENSOR #define USE_BINARY_SENSOR
#define USE_BUTTON
#define USE_CLIMATE #define USE_CLIMATE
#define USE_COVER #define USE_COVER
#define USE_DEEP_SLEEP #define USE_DEEP_SLEEP

View file

@ -603,6 +603,7 @@ def lint_inclusive_language(fname, match):
"esphome/components/switch/switch.h", "esphome/components/switch/switch.h",
"esphome/components/text_sensor/text_sensor.h", "esphome/components/text_sensor/text_sensor.h",
"esphome/components/climate/climate.h", "esphome/components/climate/climate.h",
"esphome/components/button/button.h",
"esphome/core/component.h", "esphome/core/component.h",
"esphome/core/gpio.h", "esphome/core/gpio.h",
"esphome/core/log.h", "esphome/core/log.h",

View file

@ -520,3 +520,7 @@ xpt2046:
id(touchscreen).y_raw, id(touchscreen).y_raw,
id(touchscreen).z_raw id(touchscreen).z_raw
); );
button:
- platform: restart
name: Restart Button