diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 1b06348e2c..f8f31d81df 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -2570,7 +2570,7 @@ message ListEntitiesEventResponse { EntityCategory entity_category = 7; string device_class = 8 [(max_data_length) = 47]; - repeated string event_types = 9 [(container_pointer_no_template) = "FixedVector"]; + repeated string event_types = 9 [(container_pointer_no_template) = "ConstVector"]; uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"]; } message EventResponse { diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 17436d33aa..14204e97aa 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -3460,7 +3460,7 @@ class ListEntitiesEventResponse final : public InfoResponseProtoMessage { const LogString *message_name() const override { return LOG_STR("list_entities_event_response"); } #endif StringRef device_class{nullptr, 0}; // null until set, encode only - const FixedVector *event_types{}; + const ConstVector *event_types{}; static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM); uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG); diff --git a/esphome/components/event/__init__.py b/esphome/components/event/__init__.py index c40130634f..b09d979141 100644 --- a/esphome/components/event/__init__.py +++ b/esphome/components/event/__init__.py @@ -98,7 +98,9 @@ async def setup_event_core_( ) -> None: await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) - cg.add(var.set_event_types(event_types)) + if event_types: + table = cg.shared_progmem_array("event_types", cg.const_char_ptr, event_types) + cg.add(var.set_event_types_static(table, len(event_types))) setup_device_class(config) diff --git a/esphome/components/event/event.cpp b/esphome/components/event/event.cpp index 3170c7a8dd..671e11e7d2 100644 --- a/esphome/components/event/event.cpp +++ b/esphome/components/event/event.cpp @@ -28,11 +28,8 @@ void Event::trigger(const char *event_type) { #endif } -void Event::set_event_types(const FixedVector &event_types) { - this->types_.init(event_types.size()); - for (const char *type : event_types) { - this->types_.push_back(type); - } +void Event::set_event_types_copy_(const char *const *event_types, size_t count) { + this->types_.assign_copy(event_types, count); this->last_event_type_ = nullptr; // Reset when types change } diff --git a/esphome/components/event/event.h b/esphome/components/event/event.h index f57327e33f..203ee52ac8 100644 --- a/esphome/components/event/event.h +++ b/esphome/components/event/event.h @@ -11,6 +11,9 @@ namespace esphome::event { +/// A codegen table in flash, or a heap copy of a runtime list (see ConstVector). +using EventTypes = ConstVector; + #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()); \ @@ -24,20 +27,29 @@ class Event : public EntityBase { void trigger(const char *event_type); void trigger(const std::string &event_type) { this->trigger(event_type.c_str()); } - /// Set the event types supported by this event; called by generated code with string literals. + /// Codegen only: points at a table that outlives the event (see ConstVector::assign_static). + void set_event_types_static(const char *const *event_types, size_t count) { + this->types_.assign_static(event_types, count); + this->last_event_type_ = nullptr; + } + /// Runtime lists are copied; the strings must still outlive the event. void set_event_types(std::initializer_list event_types) { - this->types_ = event_types; - this->last_event_type_ = nullptr; // Reset when types change + this->set_event_types_copy_(event_types.begin(), event_types.size()); } /// Copy the event types of another event, for components that wrap one. - void set_event_types(const FixedVector &event_types); + void set_event_types(const EventTypes &event_types) { + this->set_event_types_copy_(event_types.data(), event_types.size()); + } + void set_event_types(const FixedVector &event_types) { + this->set_event_types_copy_(event_types.begin(), event_types.size()); + } // Deleted overloads to catch incorrect std::string usage at compile time with clear error messages void set_event_types(std::initializer_list event_types) = delete; void set_event_types(const FixedVector &event_types) = delete; /// Return the event types supported by this event. - const FixedVector &get_event_types() const { return this->types_; } + const EventTypes &get_event_types() const { return this->types_; } /// Return the last triggered event type, or empty StringRef if no event triggered yet. StringRef get_last_event_type() const { return StringRef::from_maybe_nullptr(this->last_event_type_); } @@ -68,8 +80,10 @@ class Event : public EntityBase { } protected: + void set_event_types_copy_(const char *const *event_types, size_t count); + LazyCallbackManager event_callback_; - FixedVector types_; + EventTypes types_; private: /// Last triggered event type - must point to entry in types_ to ensure valid lifetime. diff --git a/tests/component_tests/event/__init__.py b/tests/component_tests/event/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/event/config/event_types.yaml b/tests/component_tests/event/config/event_types.yaml new file mode 100644 index 0000000000..8e849d4a78 --- /dev/null +++ b/tests/component_tests/event/config/event_types.yaml @@ -0,0 +1,19 @@ +esphome: + name: test + +esp8266: + board: d1_mini + +event: + - platform: template + id: event_a + name: Event A + event_types: [pressed, held] + - platform: template + id: event_b + name: Event B + event_types: [pressed, held] + - platform: template + id: event_c + name: Event C + event_types: [ring] diff --git a/tests/component_tests/event/test_event_types.py b/tests/component_tests/event/test_event_types.py new file mode 100644 index 0000000000..30e653bc70 --- /dev/null +++ b/tests/component_tests/event/test_event_types.py @@ -0,0 +1,25 @@ +"""Event types are shared PROGMEM tables of type pointers.""" + +from collections.abc import Callable +from pathlib import Path +import re + + +def test_event_types_use_shared_tables( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + main_cpp = generate_main(component_config_path("event_types.yaml")) + + calls = { + var: table + for var, table, _ in re.findall( + r"(\w+)->set_event_types_static\((\w+), (\d+)\);", main_cpp + ) + } + assert set(calls) == {"event_a", "event_b", "event_c"} + assert calls["event_a"] == calls["event_b"] != calls["event_c"] + assert ( + f'static constexpr const char * {calls["event_a"]}[] PROGMEM = {{"pressed", "held"}};' + in main_cpp + ) diff --git a/tests/components/event/event_types_test.cpp b/tests/components/event/event_types_test.cpp new file mode 100644 index 0000000000..0bc99e1a8e --- /dev/null +++ b/tests/components/event/event_types_test.cpp @@ -0,0 +1,51 @@ +#include + +#include "esphome/components/event/event.h" + +namespace esphome::event::testing { + +static const char *const TYPES[] = {"pressed", "held"}; + +TEST(EventTypes, StaticTableIsViewed) { + Event event; + event.set_event_types_static(TYPES, 2); + EXPECT_EQ(event.get_event_types().data(), TYPES); + EXPECT_EQ(event.get_event_types().size(), 2u); +} + +TEST(EventTypes, CopiesAnotherEventAndResetsLastType) { + Event source; + source.set_event_types({"x", "y"}); + Event copy; + copy.set_event_types_static(TYPES, 2); + copy.trigger("held"); + ASSERT_TRUE(copy.has_event()); + copy.set_event_types(source.get_event_types()); + EXPECT_FALSE(copy.has_event()); + EXPECT_NE(copy.get_event_types().data(), TYPES); + EXPECT_NE(copy.get_event_types().data(), source.get_event_types().data()); + EXPECT_EQ(copy.get_event_types().size(), 2u); + EXPECT_STREQ(copy.get_event_types()[1], "y"); +} + +TEST(EventTypes, CopiesItsOwnList) { + Event event; + event.set_event_types({"a", "b"}); + event.set_event_types(event.get_event_types()); // copies before freeing the old list + EXPECT_EQ(event.get_event_types().size(), 2u); + EXPECT_STREQ(event.get_event_types()[1], "b"); +} + +TEST(EventTypes, CopiesFromFixedVector) { + FixedVector types; + types.init(2); + types.push_back("on"); + types.push_back("off"); + Event event; + event.set_event_types(types); + EXPECT_NE(event.get_event_types().data(), types.begin()); + EXPECT_EQ(event.get_event_types().size(), 2u); + EXPECT_STREQ(event.get_event_types()[0], "on"); +} + +} // namespace esphome::event::testing