From c531a528f0fd56d0778921aad96a01df80062222 Mon Sep 17 00:00:00 2001 From: David Friedland Date: Tue, 23 Apr 2024 19:35:26 -0700 Subject: [PATCH] Event entity support (#6451) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 2 + esphome/components/api/api.proto | 26 +++ esphome/components/api/api_connection.cpp | 24 +++ esphome/components/api/api_connection.h | 5 + esphome/components/api/api_pb2.cpp | 151 ++++++++++++++++++ esphome/components/api/api_pb2.h | 34 ++++ esphome/components/api/api_pb2_service.cpp | 16 ++ esphome/components/api/api_pb2_service.h | 6 + esphome/components/api/api_server.cpp | 7 + esphome/components/api/api_server.h | 3 + esphome/components/api/list_entities.cpp | 3 + esphome/components/api/list_entities.h | 3 + esphome/components/api/subscribe_state.h | 3 + esphome/components/event/__init__.py | 134 ++++++++++++++++ esphome/components/event/automation.h | 25 +++ esphome/components/event/event.cpp | 24 +++ esphome/components/event/event.h | 37 +++++ esphome/components/mqtt/__init__.py | 1 + esphome/components/mqtt/mqtt_const.h | 4 + esphome/components/mqtt/mqtt_event.cpp | 54 +++++++ esphome/components/mqtt/mqtt_event.h | 39 +++++ esphome/components/template/event/__init__.py | 24 +++ .../template/event/template_event.h | 12 ++ .../components/web_server/list_entities.cpp | 9 ++ esphome/components/web_server/list_entities.h | 3 + esphome/components/web_server/web_server.cpp | 22 +++ esphome/components/web_server/web_server.h | 7 + esphome/const.py | 5 + esphome/core/application.h | 20 +++ esphome/core/component_iterator.cpp | 15 ++ esphome/core/component_iterator.h | 6 + esphome/core/controller.cpp | 6 + esphome/core/controller.h | 6 + esphome/core/defines.h | 1 + script/ci-custom.py | 1 + tests/components/event/test.esp32-c3-idf.yaml | 9 ++ tests/components/event/test.esp32-c3.yaml | 9 ++ tests/components/event/test.esp32-idf.yaml | 9 ++ tests/components/event/test.esp32.yaml | 9 ++ tests/components/event/test.esp8266.yaml | 9 ++ tests/components/event/test.rp2040.yaml | 9 ++ 41 files changed, 792 insertions(+) create mode 100644 esphome/components/event/__init__.py create mode 100644 esphome/components/event/automation.h create mode 100644 esphome/components/event/event.cpp create mode 100644 esphome/components/event/event.h create mode 100644 esphome/components/mqtt/mqtt_event.cpp create mode 100644 esphome/components/mqtt/mqtt_event.h create mode 100644 esphome/components/template/event/__init__.py create mode 100644 esphome/components/template/event/template_event.h create mode 100644 tests/components/event/test.esp32-c3-idf.yaml create mode 100644 tests/components/event/test.esp32-c3.yaml create mode 100644 tests/components/event/test.esp32-idf.yaml create mode 100644 tests/components/event/test.esp32.yaml create mode 100644 tests/components/event/test.esp8266.yaml create mode 100644 tests/components/event/test.rp2040.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 8da1618636..a60a5c3099 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -119,6 +119,7 @@ esphome/components/esp32_rmt/* @jesserockz esphome/components/esp32_rmt_led_strip/* @jesserockz esphome/components/esp8266/* @esphome/core esphome/components/ethernet_info/* @gtjadsonsantos +esphome/components/event/* @nohat esphome/components/exposure_notifications/* @OttoWinter esphome/components/ezo/* @ssieb esphome/components/ezo_pmp/* @carlos-sarmiento @@ -359,6 +360,7 @@ esphome/components/tee501/* @Stock-M esphome/components/teleinfo/* @0hax esphome/components/template/alarm_control_panel/* @grahambrown11 @hwstar esphome/components/template/datetime/* @rfdarter +esphome/components/template/event/* @nohat esphome/components/template/fan/* @ssieb esphome/components/text/* @mauritskorse esphome/components/thermostat/* @kbx81 diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 12b7ef0958..17826ea7ed 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -1702,6 +1702,32 @@ message TimeCommandRequest { uint32 second = 4; } +// ==================== EVENT ==================== +message ListEntitiesEventResponse { + option (id) = 107; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_EVENT"; + + 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; + string device_class = 8; + + repeated string event_types = 9; +} +message EventResponse { + option (id) = 108; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_EVENT"; + + fixed32 key = 1; + string event_type = 2; +} // ==================== VALVE ==================== message ListEntitiesValveResponse { diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 47136fff98..ec09604d95 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1209,6 +1209,30 @@ void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRe } #endif +#ifdef USE_EVENT +bool APIConnection::send_event(event::Event *event, std::string event_type) { + EventResponse resp{}; + resp.key = event->get_object_id_hash(); + resp.event_type = std::move(event_type); + return this->send_event_response(resp); +} +bool APIConnection::send_event_info(event::Event *event) { + ListEntitiesEventResponse msg; + msg.key = event->get_object_id_hash(); + msg.object_id = event->get_object_id(); + if (event->has_own_name()) + msg.name = event->get_name(); + msg.unique_id = get_default_unique_id("event", event); + msg.icon = event->get_icon(); + msg.disabled_by_default = event->is_disabled_by_default(); + msg.entity_category = static_cast(event->get_entity_category()); + msg.device_class = event->get_device_class(); + for (const auto &event_type : event->get_event_types()) + msg.event_types.push_back(event_type); + return this->send_list_entities_event_response(msg); +} +#endif + bool APIConnection::send_log_message(int level, const char *tag, const char *line) { if (this->log_subscription_ < level) return false; diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index af2dd9e681..2c1d733d3e 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -153,6 +153,11 @@ class APIConnection : public APIServerConnection { void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) override; #endif +#ifdef USE_EVENT + bool send_event(event::Event *event, std::string event_type); + bool send_event_info(event::Event *event); +#endif + void on_disconnect_response(const DisconnectResponse &value) override; void on_ping_response(const PingResponse &value) override { // we initiated ping diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 508947ba24..3f01d88c58 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -7709,6 +7709,157 @@ void TimeCommandRequest::dump_to(std::string &out) const { out.append("}"); } #endif +bool ListEntitiesEventResponse::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(); + return true; + } + default: + return false; + } +} +bool ListEntitiesEventResponse::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; + } + case 8: { + this->device_class = value.as_string(); + return true; + } + case 9: { + this->event_types.push_back(value.as_string()); + return true; + } + default: + return false; + } +} +bool ListEntitiesEventResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 2: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void ListEntitiesEventResponse::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(7, this->entity_category); + buffer.encode_string(8, this->device_class); + for (auto &it : this->event_types) { + buffer.encode_string(9, it, true); + } +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void ListEntitiesEventResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ListEntitiesEventResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + sprintf(buffer, "%" PRIu32, 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(this->entity_category)); + out.append("\n"); + + out.append(" device_class: "); + out.append("'").append(this->device_class).append("'"); + out.append("\n"); + + for (const auto &it : this->event_types) { + out.append(" event_types: "); + out.append("'").append(it).append("'"); + out.append("\n"); + } + out.append("}"); +} +#endif +bool EventResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 2: { + this->event_type = value.as_string(); + return true; + } + default: + return false; + } +} +bool EventResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void EventResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_string(2, this->event_type); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void EventResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("EventResponse {\n"); + out.append(" key: "); + sprintf(buffer, "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" event_type: "); + out.append("'").append(this->event_type).append("'"); + out.append("\n"); + out.append("}"); +} +#endif bool ListEntitiesValveResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 6: { diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 950ffcdc88..9a6aab254d 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -1974,6 +1974,40 @@ class TimeCommandRequest : public ProtoMessage { bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; +class ListEntitiesEventResponse : 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{}; + std::string device_class{}; + std::vector event_types{}; + 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 EventResponse : public ProtoMessage { + public: + uint32_t key{0}; + std::string event_type{}; + 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; +}; class ListEntitiesValveResponse : public ProtoMessage { public: std::string object_id{}; diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index 4b8b8cf5ae..ced81fa643 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -557,6 +557,22 @@ bool APIServerConnectionBase::send_time_state_response(const TimeStateResponse & #endif #ifdef USE_DATETIME_TIME #endif +#ifdef USE_EVENT +bool APIServerConnectionBase::send_list_entities_event_response(const ListEntitiesEventResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_list_entities_event_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 107); +} +#endif +#ifdef USE_EVENT +bool APIServerConnectionBase::send_event_response(const EventResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_event_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 108); +} +#endif #ifdef USE_VALVE bool APIServerConnectionBase::send_list_entities_valve_response(const ListEntitiesValveResponse &msg) { #ifdef HAS_PROTO_MESSAGE_DUMP diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 9f1d711257..c8b2bc5789 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -280,6 +280,12 @@ class APIServerConnectionBase : public ProtoService { #ifdef USE_DATETIME_TIME virtual void on_time_command_request(const TimeCommandRequest &value){}; #endif +#ifdef USE_EVENT + bool send_list_entities_event_response(const ListEntitiesEventResponse &msg); +#endif +#ifdef USE_EVENT + bool send_event_response(const EventResponse &msg); +#endif #ifdef USE_VALVE bool send_list_entities_valve_response(const ListEntitiesValveResponse &msg); #endif diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 457eeb1229..6d4e4db1e8 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -318,6 +318,13 @@ void APIServer::on_media_player_update(media_player::MediaPlayer *obj) { } #endif +#ifdef USE_EVENT +void APIServer::on_event(event::Event *obj, const std::string &event_type) { + for (auto &c : this->clients_) + c->send_event(obj, event_type); +} +#endif + float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; } void APIServer::set_port(uint16_t port) { this->port_ = port; } APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index d64643b961..e9e03cde0d 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -96,6 +96,9 @@ class APIServer : public Component, public Controller { #ifdef USE_ALARM_CONTROL_PANEL void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) override; #endif +#ifdef USE_EVENT + void on_event(event::Event *obj, const std::string &event_type) override; +#endif bool is_connected() const; diff --git a/esphome/components/api/list_entities.cpp b/esphome/components/api/list_entities.cpp index d6ff8e5557..82bfd45333 100644 --- a/esphome/components/api/list_entities.cpp +++ b/esphome/components/api/list_entities.cpp @@ -89,6 +89,9 @@ bool ListEntitiesIterator::on_alarm_control_panel(alarm_control_panel::AlarmCont return this->client_->send_alarm_control_panel_info(a_alarm_control_panel); } #endif +#ifdef USE_EVENT +bool ListEntitiesIterator::on_event(event::Event *event) { return this->client_->send_event_info(event); } +#endif } // namespace api } // namespace esphome diff --git a/esphome/components/api/list_entities.h b/esphome/components/api/list_entities.h index 5d0c243f4a..19cd99ea01 100644 --- a/esphome/components/api/list_entities.h +++ b/esphome/components/api/list_entities.h @@ -69,6 +69,9 @@ class ListEntitiesIterator : public ComponentIterator { #endif #ifdef USE_ALARM_CONTROL_PANEL bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) override; +#endif +#ifdef USE_EVENT + bool on_event(event::Event *event) override; #endif bool on_end() override; diff --git a/esphome/components/api/subscribe_state.h b/esphome/components/api/subscribe_state.h index 8d50e0d89a..17d444c441 100644 --- a/esphome/components/api/subscribe_state.h +++ b/esphome/components/api/subscribe_state.h @@ -66,6 +66,9 @@ class InitialStateIterator : public ComponentIterator { #endif #ifdef USE_ALARM_CONTROL_PANEL bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) override; +#endif +#ifdef USE_EVENT + bool on_event(event::Event *event) override { return true; }; #endif protected: APIConnection *client_; diff --git a/esphome/components/event/__init__.py b/esphome/components/event/__init__.py new file mode 100644 index 0000000000..789f121cf3 --- /dev/null +++ b/esphome/components/event/__init__.py @@ -0,0 +1,134 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.components import mqtt +from esphome.const import ( + CONF_DEVICE_CLASS, + CONF_ENTITY_CATEGORY, + CONF_ICON, + CONF_ID, + CONF_ON_EVENT, + CONF_TRIGGER_ID, + CONF_MQTT_ID, + CONF_EVENT_TYPE, + DEVICE_CLASS_BUTTON, + DEVICE_CLASS_DOORBELL, + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_MOTION, +) +from esphome.core import CORE, coroutine_with_priority +from esphome.cpp_helpers import setup_entity +from esphome.cpp_generator import MockObjClass + +CODEOWNERS = ["@nohat"] +IS_PLATFORM_COMPONENT = True + +DEVICE_CLASSES = [ + DEVICE_CLASS_BUTTON, + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_DOORBELL, + DEVICE_CLASS_MOTION, +] + +event_ns = cg.esphome_ns.namespace("event") +Event = event_ns.class_("Event", cg.EntityBase) +EventPtr = Event.operator("ptr") + +TriggerEventAction = event_ns.class_("TriggerEventAction", automation.Action) + +EventTrigger = event_ns.class_("EventTrigger", automation.Trigger.template()) + +validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") + +EVENT_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend( + { + cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTEventComponent), + cv.GenerateID(): cv.declare_id(Event), + cv.Optional(CONF_DEVICE_CLASS): validate_device_class, + cv.Optional(CONF_ON_EVENT): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(EventTrigger), + } + ), + } +) + +_UNDEF = object() + + +def event_schema( + class_: MockObjClass = _UNDEF, + *, + icon: str = _UNDEF, + entity_category: str = _UNDEF, + device_class: str = _UNDEF, +) -> cv.Schema: + schema = {} + + if class_ is not _UNDEF: + schema[cv.GenerateID()] = cv.declare_id(class_) + + for key, default, validator in [ + (CONF_ICON, icon, cv.icon), + (CONF_ENTITY_CATEGORY, entity_category, cv.entity_category), + (CONF_DEVICE_CLASS, device_class, validate_device_class), + ]: + if default is not _UNDEF: + schema[cv.Optional(key, default=default)] = validator + + return EVENT_SCHEMA.extend(schema) + + +async def setup_event_core_(var, config, *, event_types: list[str]): + await setup_entity(var, config) + + for conf in config.get(CONF_ON_EVENT, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation( + trigger, [(cg.std_string, "event_type")], conf + ) + + cg.add(var.set_event_types(event_types)) + + if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: + cg.add(var.set_device_class(device_class)) + + if mqtt_id := config.get(CONF_MQTT_ID): + mqtt_ = cg.new_Pvariable(mqtt_id, var) + await mqtt.register_mqtt_component(mqtt_, config) + + +async def register_event(var, config, *, event_types: list[str]): + if not CORE.has_id(config[CONF_ID]): + var = cg.Pvariable(config[CONF_ID], var) + cg.add(cg.App.register_event(var)) + await setup_event_core_(var, config, event_types=event_types) + + +async def new_event(config, *, event_types: list[str]): + var = cg.new_Pvariable(config[CONF_ID]) + await register_event(var, config, event_types=event_types) + return var + + +TRIGGER_EVENT_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(Event), + cv.Required(CONF_EVENT_TYPE): cv.templatable(cv.string_strict), + } +) + + +@automation.register_action("event.trigger", TriggerEventAction, TRIGGER_EVENT_SCHEMA) +async def event_fire_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + templ = await cg.templatable(config[CONF_EVENT_TYPE], args, cg.std_string) + cg.add(var.set_event_type(templ)) + return var + + +@coroutine_with_priority(100.0) +async def to_code(config): + cg.add_define("USE_EVENT") + cg.add_global(event_ns.using) diff --git a/esphome/components/event/automation.h b/esphome/components/event/automation.h new file mode 100644 index 0000000000..9ebcb654a0 --- /dev/null +++ b/esphome/components/event/automation.h @@ -0,0 +1,25 @@ +#pragma once + +#include "esphome/components/event/event.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace event { + +template class TriggerEventAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(std::string, event_type) + + void play(Ts... x) override { this->parent_->trigger(this->event_type_.value(x...)); } +}; + +class EventTrigger : public Trigger { + public: + EventTrigger(Event *event) { + event->add_on_event_callback([this](const std::string &event_type) { this->trigger(event_type); }); + } +}; + +} // namespace event +} // namespace esphome diff --git a/esphome/components/event/event.cpp b/esphome/components/event/event.cpp new file mode 100644 index 0000000000..061afcb026 --- /dev/null +++ b/esphome/components/event/event.cpp @@ -0,0 +1,24 @@ +#include "event.h" + +#include "esphome/core/log.h" + +namespace esphome { +namespace event { + +static const char *const TAG = "event"; + +void Event::trigger(const std::string &event_type) { + if (types_.find(event_type) == types_.end()) { + ESP_LOGE(TAG, "'%s': invalid event type for trigger(): %s", this->get_name().c_str(), event_type.c_str()); + return; + } + ESP_LOGD(TAG, "'%s' Triggered event '%s'", this->get_name().c_str(), event_type.c_str()); + this->event_callback_.call(event_type); +} + +void Event::add_on_event_callback(std::function &&callback) { + this->event_callback_.add(std::move(callback)); +} + +} // namespace event +} // namespace esphome diff --git a/esphome/components/event/event.h b/esphome/components/event/event.h new file mode 100644 index 0000000000..067a867360 --- /dev/null +++ b/esphome/components/event/event.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include + +#include "esphome/core/component.h" +#include "esphome/core/entity_base.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace event { + +#define LOG_EVENT(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()); \ + } \ + if (!(obj)->get_device_class().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class().c_str()); \ + } \ + } + +class Event : public EntityBase, public EntityBase_DeviceClass { + public: + void trigger(const std::string &event_type); + void set_event_types(const std::set &event_types) { this->types_ = event_types; } + std::set get_event_types() const { return this->types_; } + void add_on_event_callback(std::function &&callback); + + protected: + CallbackManager event_callback_; + std::set types_; +}; + +} // namespace event +} // namespace esphome diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 72ee81dbbc..7a42140ef6 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -119,6 +119,7 @@ MQTTTextComponent = mqtt_ns.class_("MQTTTextComponent", MQTTComponent) MQTTSelectComponent = mqtt_ns.class_("MQTTSelectComponent", MQTTComponent) MQTTButtonComponent = mqtt_ns.class_("MQTTButtonComponent", MQTTComponent) MQTTLockComponent = mqtt_ns.class_("MQTTLockComponent", MQTTComponent) +MQTTEventComponent = mqtt_ns.class_("MQTTEventComponent", MQTTComponent) MQTTValveComponent = mqtt_ns.class_("MQTTValveComponent", MQTTComponent) MQTTDiscoveryUniqueIdGenerator = mqtt_ns.enum("MQTTDiscoveryUniqueIdGenerator") diff --git a/esphome/components/mqtt/mqtt_const.h b/esphome/components/mqtt/mqtt_const.h index 2209f96e7a..66872680bb 100644 --- a/esphome/components/mqtt/mqtt_const.h +++ b/esphome/components/mqtt/mqtt_const.h @@ -73,6 +73,8 @@ constexpr const char *const MQTT_ENABLED_BY_DEFAULT = "en"; constexpr const char *const MQTT_ENTITY_CATEGORY = "ent_cat"; constexpr const char *const MQTT_ERROR_TEMPLATE = "err_tpl"; constexpr const char *const MQTT_ERROR_TOPIC = "err_t"; +constexpr const char *const MQTT_EVENT_TYPE = "event_type"; +constexpr const char *const MQTT_EVENT_TYPES = "evt_typ"; constexpr const char *const MQTT_EXPIRE_AFTER = "exp_aft"; constexpr const char *const MQTT_FAN_MODE_COMMAND_TEMPLATE = "fan_mode_cmd_tpl"; constexpr const char *const MQTT_FAN_MODE_COMMAND_TOPIC = "fan_mode_cmd_t"; @@ -330,6 +332,8 @@ constexpr const char *const MQTT_ENABLED_BY_DEFAULT = "enabled_by_default"; constexpr const char *const MQTT_ENTITY_CATEGORY = "entity_category"; constexpr const char *const MQTT_ERROR_TEMPLATE = "error_template"; constexpr const char *const MQTT_ERROR_TOPIC = "error_topic"; +constexpr const char *const MQTT_EVENT_TYPE = "event_type"; +constexpr const char *const MQTT_EVENT_TYPES = "event_types"; constexpr const char *const MQTT_EXPIRE_AFTER = "expire_after"; constexpr const char *const MQTT_FAN_MODE_COMMAND_TEMPLATE = "fan_mode_command_template"; constexpr const char *const MQTT_FAN_MODE_COMMAND_TOPIC = "fan_mode_command_topic"; diff --git a/esphome/components/mqtt/mqtt_event.cpp b/esphome/components/mqtt/mqtt_event.cpp new file mode 100644 index 0000000000..cf0b90e3d6 --- /dev/null +++ b/esphome/components/mqtt/mqtt_event.cpp @@ -0,0 +1,54 @@ +#include "mqtt_event.h" +#include "esphome/core/log.h" + +#include "mqtt_const.h" + +#ifdef USE_MQTT +#ifdef USE_EVENT + +namespace esphome { +namespace mqtt { + +static const char *const TAG = "mqtt.event"; + +using namespace esphome::event; + +MQTTEventComponent::MQTTEventComponent(event::Event *event) : event_(event) {} + +void MQTTEventComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { + JsonArray event_types = root.createNestedArray(MQTT_EVENT_TYPES); + for (const auto &event_type : this->event_->get_event_types()) + event_types.add(event_type); + + if (!this->event_->get_device_class().empty()) + root[MQTT_DEVICE_CLASS] = this->event_->get_device_class(); + + config.command_topic = false; +} + +void MQTTEventComponent::setup() { + this->event_->add_on_event_callback([this](const std::string &event_type) { this->publish_event_(event_type); }); +} + +void MQTTEventComponent::dump_config() { + ESP_LOGCONFIG(TAG, "MQTT Event '%s': ", this->event_->get_name().c_str()); + ESP_LOGCONFIG(TAG, "Event Types: "); + for (const auto &event_type : this->event_->get_event_types()) { + ESP_LOGCONFIG(TAG, "- %s", event_type.c_str()); + } + LOG_MQTT_COMPONENT(true, true); +} + +bool MQTTEventComponent::publish_event_(const std::string &event_type) { + return this->publish_json(this->get_state_topic_(), + [event_type](JsonObject root) { root[MQTT_EVENT_TYPE] = event_type; }); +} + +std::string MQTTEventComponent::component_type() const { return "event"; } +const EntityBase *MQTTEventComponent::get_entity() const { return this->event_; } + +} // namespace mqtt +} // namespace esphome + +#endif +#endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_event.h b/esphome/components/mqtt/mqtt_event.h new file mode 100644 index 0000000000..4335820e53 --- /dev/null +++ b/esphome/components/mqtt/mqtt_event.h @@ -0,0 +1,39 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_MQTT +#ifdef USE_EVENT + +#include "esphome/components/event/event.h" +#include "mqtt_component.h" + +namespace esphome { +namespace mqtt { + +class MQTTEventComponent : public mqtt::MQTTComponent { + public: + explicit MQTTEventComponent(event::Event *event); + + void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override; + + void setup() override; + + void dump_config() override; + + /// Events do not send a state so just return true. + bool send_initial_state() override { return true; } + + protected: + bool publish_event_(const std::string &event_type); + std::string component_type() const override; + const EntityBase *get_entity() const override; + + event::Event *event_; +}; + +} // namespace mqtt +} // namespace esphome + +#endif +#endif // USE_MQTT diff --git a/esphome/components/template/event/__init__.py b/esphome/components/template/event/__init__.py new file mode 100644 index 0000000000..2a948cfdfd --- /dev/null +++ b/esphome/components/template/event/__init__.py @@ -0,0 +1,24 @@ +import esphome.config_validation as cv + +from esphome.components import event + +import esphome.codegen as cg + +from esphome.const import CONF_EVENT_TYPES + +from .. import template_ns + +CODEOWNERS = ["@nohat"] + +TemplateEvent = template_ns.class_("TemplateEvent", event.Event, cg.Component) + +CONFIG_SCHEMA = event.event_schema(TemplateEvent).extend( + { + cv.Required(CONF_EVENT_TYPES): cv.ensure_list(cv.string_strict), + } +) + + +async def to_code(config): + var = await event.new_event(config, event_types=config[CONF_EVENT_TYPES]) + await cg.register_component(var, config) diff --git a/esphome/components/template/event/template_event.h b/esphome/components/template/event/template_event.h new file mode 100644 index 0000000000..251ae9299b --- /dev/null +++ b/esphome/components/template/event/template_event.h @@ -0,0 +1,12 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/event/event.h" + +namespace esphome { +namespace template_ { + +class TemplateEvent : public Component, public event::Event {}; + +} // namespace template_ +} // namespace esphome diff --git a/esphome/components/web_server/list_entities.cpp b/esphome/components/web_server/list_entities.cpp index 13e396dc82..8d08783c8c 100644 --- a/esphome/components/web_server/list_entities.cpp +++ b/esphome/components/web_server/list_entities.cpp @@ -159,5 +159,14 @@ bool ListEntitiesIterator::on_alarm_control_panel(alarm_control_panel::AlarmCont } #endif +#ifdef USE_EVENT +bool ListEntitiesIterator::on_event(event::Event *event) { + // Null event type, since we are just iterating over entities + const std::string null_event_type = ""; + this->web_server_->events_.send(this->web_server_->event_json(event, null_event_type, DETAIL_ALL).c_str(), "state"); + return true; +} +#endif + } // namespace web_server } // namespace esphome diff --git a/esphome/components/web_server/list_entities.h b/esphome/components/web_server/list_entities.h index d0b8dda233..af84cb1d2b 100644 --- a/esphome/components/web_server/list_entities.h +++ b/esphome/components/web_server/list_entities.h @@ -62,6 +62,9 @@ class ListEntitiesIterator : public ComponentIterator { #ifdef USE_ALARM_CONTROL_PANEL bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) override; #endif +#ifdef USE_EVENT + bool on_event(event::Event *event) override; +#endif protected: WebServer *web_server_; diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 412f8816ee..0202038ffc 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1352,6 +1352,28 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques } #endif +#ifdef USE_EVENT +void WebServer::on_event(event::Event *obj, const std::string &event_type) { + this->events_.send(this->event_json(obj, event_type, DETAIL_STATE).c_str(), "state"); +} + +std::string WebServer::event_json(event::Event *obj, const std::string &event_type, JsonDetail start_config) { + return json::build_json([obj, event_type, start_config](JsonObject root) { + set_json_id(root, obj, "event-" + obj->get_object_id(), start_config); + if (!event_type.empty()) { + root["event_type"] = event_type; + } + if (start_config == DETAIL_ALL) { + JsonArray event_types = root.createNestedArray("event_types"); + for (auto const &event_type : obj->get_event_types()) { + event_types.add(event_type); + } + root["device_class"] = obj->get_device_class(); + } + }); +} +#endif + bool WebServer::canHandle(AsyncWebServerRequest *request) { if (request->url() == "/") return true; diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 9f807efc43..5e8f3f8236 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -297,6 +297,13 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { alarm_control_panel::AlarmControlPanelState value, JsonDetail start_config); #endif +#ifdef USE_EVENT + void on_event(event::Event *obj, const std::string &event_type) override; + + /// Dump the event details with its value as a JSON string. + std::string event_json(event::Event *obj, const std::string &event_type, JsonDetail start_config); +#endif + /// Override the web handler's canHandle method. bool canHandle(AsyncWebServerRequest *request) override; /// Override the web handler's handleRequest method. diff --git a/esphome/const.py b/esphome/const.py index 8410d36708..dfb7fe464c 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -251,6 +251,8 @@ CONF_ESP8266_DISABLE_SSL_SUPPORT = "esp8266_disable_ssl_support" CONF_ESPHOME = "esphome" CONF_ETHERNET = "ethernet" CONF_EVENT = "event" +CONF_EVENT_TYPE = "event_type" +CONF_EVENT_TYPES = "event_types" CONF_EXPIRE_AFTER = "expire_after" CONF_EXPORT_ACTIVE_ENERGY = "export_active_energy" CONF_EXPORT_REACTIVE_ENERGY = "export_reactive_energy" @@ -517,6 +519,7 @@ CONF_ON_DOUBLE_CLICK = "on_double_click" CONF_ON_ENROLLMENT_DONE = "on_enrollment_done" CONF_ON_ENROLLMENT_FAILED = "on_enrollment_failed" CONF_ON_ENROLLMENT_SCAN = "on_enrollment_scan" +CONF_ON_EVENT = "on_event" CONF_ON_FINGER_SCAN_INVALID = "on_finger_scan_invalid" CONF_ON_FINGER_SCAN_MATCHED = "on_finger_scan_matched" CONF_ON_FINGER_SCAN_MISPLACED = "on_finger_scan_misplaced" @@ -1024,6 +1027,7 @@ DEVICE_CLASS_AWNING = "awning" DEVICE_CLASS_BATTERY = "battery" DEVICE_CLASS_BATTERY_CHARGING = "battery_charging" DEVICE_CLASS_BLIND = "blind" +DEVICE_CLASS_BUTTON = "button" DEVICE_CLASS_CARBON_DIOXIDE = "carbon_dioxide" DEVICE_CLASS_CARBON_MONOXIDE = "carbon_monoxide" DEVICE_CLASS_COLD = "cold" @@ -1036,6 +1040,7 @@ DEVICE_CLASS_DATA_SIZE = "data_size" DEVICE_CLASS_DATE = "date" DEVICE_CLASS_DISTANCE = "distance" DEVICE_CLASS_DOOR = "door" +DEVICE_CLASS_DOORBELL = "doorbell" DEVICE_CLASS_DURATION = "duration" DEVICE_CLASS_EMPTY = "" DEVICE_CLASS_ENERGY = "energy" diff --git a/esphome/core/application.h b/esphome/core/application.h index ee931282a6..35df350ec3 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -63,6 +63,9 @@ #ifdef USE_ALARM_CONTROL_PANEL #include "esphome/components/alarm_control_panel/alarm_control_panel.h" #endif +#ifdef USE_EVENT +#include "esphome/components/event/event.h" +#endif namespace esphome { @@ -164,6 +167,10 @@ class Application { } #endif +#ifdef USE_EVENT + void register_event(event::Event *event) { this->events_.push_back(event); } +#endif + /// Register the component in this Application instance. template C *register_component(C *c) { static_assert(std::is_base_of::value, "Only Component subclasses can be registered"); @@ -386,6 +393,16 @@ class Application { } #endif +#ifdef USE_EVENT + const std::vector &get_events() { return this->events_; } + event::Event *get_event_by_key(uint32_t key, bool include_internal = false) { + for (auto *obj : this->events_) + if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) + return obj; + return nullptr; + } +#endif + Scheduler scheduler; protected: @@ -409,6 +426,9 @@ class Application { #ifdef USE_BUTTON std::vector buttons_{}; #endif +#ifdef USE_EVENT + std::vector events_{}; +#endif #ifdef USE_SENSOR std::vector sensors_{}; #endif diff --git a/esphome/core/component_iterator.cpp b/esphome/core/component_iterator.cpp index b00154c685..687f1f6e23 100644 --- a/esphome/core/component_iterator.cpp +++ b/esphome/core/component_iterator.cpp @@ -321,6 +321,21 @@ void ComponentIterator::advance() { } } break; +#endif +#ifdef USE_EVENT + case IteratorState::EVENT: + if (this->at_ >= App.get_events().size()) { + advance_platform = true; + } else { + auto *event = App.get_events()[this->at_]; + if (event->is_internal() && !this->include_internal_) { + success = true; + break; + } else { + success = this->on_event(event); + } + } + break; #endif case IteratorState::MAX: if (this->on_end()) { diff --git a/esphome/core/component_iterator.h b/esphome/core/component_iterator.h index 3fbbc0bc19..8f0398cbb3 100644 --- a/esphome/core/component_iterator.h +++ b/esphome/core/component_iterator.h @@ -80,6 +80,9 @@ class ComponentIterator { #endif #ifdef USE_ALARM_CONTROL_PANEL virtual bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) = 0; +#endif +#ifdef USE_EVENT + virtual bool on_event(event::Event *event) = 0; #endif virtual bool on_end(); @@ -146,6 +149,9 @@ class ComponentIterator { #endif #ifdef USE_ALARM_CONTROL_PANEL ALARM_CONTROL_PANEL, +#endif +#ifdef USE_EVENT + EVENT, #endif MAX, } state_{IteratorState::NONE}; diff --git a/esphome/core/controller.cpp b/esphome/core/controller.cpp index eab818bdb4..eb975eaf6f 100644 --- a/esphome/core/controller.cpp +++ b/esphome/core/controller.cpp @@ -109,6 +109,12 @@ void Controller::setup_controller(bool include_internal) { obj->add_on_state_callback([this, obj]() { this->on_alarm_control_panel_update(obj); }); } #endif +#ifdef USE_EVENT + for (auto *obj : App.get_events()) { + if (include_internal || !obj->is_internal()) + obj->add_on_event_callback([this, obj](const std::string &event_type) { this->on_event(obj, event_type); }); + } +#endif } } // namespace esphome diff --git a/esphome/core/controller.h b/esphome/core/controller.h index 94a4acb7c7..da9dbc00a6 100644 --- a/esphome/core/controller.h +++ b/esphome/core/controller.h @@ -55,6 +55,9 @@ #ifdef USE_ALARM_CONTROL_PANEL #include "esphome/components/alarm_control_panel/alarm_control_panel.h" #endif +#ifdef USE_EVENT +#include "esphome/components/event/event.h" +#endif namespace esphome { @@ -112,6 +115,9 @@ class Controller { #ifdef USE_ALARM_CONTROL_PANEL virtual void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj){}; #endif +#ifdef USE_EVENT + virtual void on_event(event::Event *obj, const std::string &event_type){}; +#endif }; } // namespace esphome diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 2064ca1356..fed73098d2 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -24,6 +24,7 @@ #define USE_CLIMATE #define USE_COVER #define USE_DEEP_SLEEP +#define USE_EVENT #define USE_FAN #define USE_GRAPH #define USE_HOMEASSISTANT_TIME diff --git a/script/ci-custom.py b/script/ci-custom.py index c591bfe5c3..27fcd480f5 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -624,6 +624,7 @@ def lint_trailing_whitespace(fname, match): "esphome/components/datetime/date_entity.h", "esphome/components/datetime/time_entity.h", "esphome/components/display/display.h", + "esphome/components/event/event.h", "esphome/components/fan/fan.h", "esphome/components/i2c/i2c.h", "esphome/components/lock/lock.h", diff --git a/tests/components/event/test.esp32-c3-idf.yaml b/tests/components/event/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..71cc19a6b0 --- /dev/null +++ b/tests/components/event/test.esp32-c3-idf.yaml @@ -0,0 +1,9 @@ +event: + - platform: template + name: Event + id: some_event + event_types: + - template_event_type1 + - template_event_type2 + on_event: + - logger.log: Event fired diff --git a/tests/components/event/test.esp32-c3.yaml b/tests/components/event/test.esp32-c3.yaml new file mode 100644 index 0000000000..71cc19a6b0 --- /dev/null +++ b/tests/components/event/test.esp32-c3.yaml @@ -0,0 +1,9 @@ +event: + - platform: template + name: Event + id: some_event + event_types: + - template_event_type1 + - template_event_type2 + on_event: + - logger.log: Event fired diff --git a/tests/components/event/test.esp32-idf.yaml b/tests/components/event/test.esp32-idf.yaml new file mode 100644 index 0000000000..71cc19a6b0 --- /dev/null +++ b/tests/components/event/test.esp32-idf.yaml @@ -0,0 +1,9 @@ +event: + - platform: template + name: Event + id: some_event + event_types: + - template_event_type1 + - template_event_type2 + on_event: + - logger.log: Event fired diff --git a/tests/components/event/test.esp32.yaml b/tests/components/event/test.esp32.yaml new file mode 100644 index 0000000000..71cc19a6b0 --- /dev/null +++ b/tests/components/event/test.esp32.yaml @@ -0,0 +1,9 @@ +event: + - platform: template + name: Event + id: some_event + event_types: + - template_event_type1 + - template_event_type2 + on_event: + - logger.log: Event fired diff --git a/tests/components/event/test.esp8266.yaml b/tests/components/event/test.esp8266.yaml new file mode 100644 index 0000000000..71cc19a6b0 --- /dev/null +++ b/tests/components/event/test.esp8266.yaml @@ -0,0 +1,9 @@ +event: + - platform: template + name: Event + id: some_event + event_types: + - template_event_type1 + - template_event_type2 + on_event: + - logger.log: Event fired diff --git a/tests/components/event/test.rp2040.yaml b/tests/components/event/test.rp2040.yaml new file mode 100644 index 0000000000..71cc19a6b0 --- /dev/null +++ b/tests/components/event/test.rp2040.yaml @@ -0,0 +1,9 @@ +event: + - platform: template + name: Event + id: some_event + event_types: + - template_event_type1 + - template_event_type2 + on_event: + - logger.log: Event fired