diff --git a/esphome/codegen.py b/esphome/codegen.py index 5debb52b4e..3dfec65f23 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -64,7 +64,6 @@ from esphome.cpp_types import ( # noqa: F401 Application, Component, ComponentPtr, - Controller, EntityBase, EntityCategory, ESPTime, diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 854bceecfa..2291dc54f8 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -81,7 +81,7 @@ def AUTO_LOAD(config: ConfigType) -> list[str]: api_ns = cg.esphome_ns.namespace("api") -APIServer = api_ns.class_("APIServer", cg.Component, cg.Controller) +APIServer = api_ns.class_("APIServer", cg.Component) HomeAssistantServiceCallAction = api_ns.class_( "HomeAssistantServiceCallAction", automation.Action ) @@ -462,8 +462,7 @@ async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - # Track controller registration for StaticVector sizing - CORE.register_controller() + CORE.register_controller(var) # Request a log listener slot for API log streaming request_log_listener() diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 310cdcf67b..2a89d31a74 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -5,7 +5,6 @@ #include "api_connection.h" #include "esphome/components/network/util.h" #include "esphome/core/application.h" -#include "esphome/core/controller_registry.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" @@ -61,8 +60,6 @@ void APIServer::socket_failed_(const LogString *msg) { } void APIServer::setup() { - ControllerRegistry::register_controller(this); - #ifdef USE_API_NOISE // Always reserve the slot: flash preferences are positional on esp8266, so // a yaml key build must keep the layout of a runtime key build diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index b8a71c93e7..49cf77ca5a 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -14,7 +14,7 @@ #include "esphome/components/socket/socket.h" #include "esphome/core/automation.h" #include "esphome/core/component.h" -#include "esphome/core/controller.h" +#include "esphome/core/entity_includes.h" #include "esphome/core/log.h" #include "esphome/core/string_ref.h" #ifdef USE_PROVISIONING @@ -49,8 +49,7 @@ struct SavedNoisePsk { bool load_saved_noise_psk(noise::psk_t &out); #endif -class APIServer final : public Component, - public Controller +class APIServer final : public Component #ifdef USE_CAMERA , public camera::CameraListener @@ -94,58 +93,58 @@ class APIServer final : public Component, void handle_disconnect(APIConnection *conn); #ifdef USE_BINARY_SENSOR - void on_binary_sensor_update(binary_sensor::BinarySensor *obj) override; + void on_binary_sensor_update(binary_sensor::BinarySensor *obj); #endif #ifdef USE_COVER - void on_cover_update(cover::Cover *obj) override; + void on_cover_update(cover::Cover *obj); #endif #ifdef USE_FAN - void on_fan_update(fan::Fan *obj) override; + void on_fan_update(fan::Fan *obj); #endif #ifdef USE_LIGHT - void on_light_update(light::LightState *obj) override; + void on_light_update(light::LightState *obj); #endif #ifdef USE_SENSOR - void on_sensor_update(sensor::Sensor *obj) override; + void on_sensor_update(sensor::Sensor *obj); #endif #ifdef USE_SWITCH - void on_switch_update(switch_::Switch *obj) override; + void on_switch_update(switch_::Switch *obj); #endif #ifdef USE_TEXT_SENSOR - void on_text_sensor_update(text_sensor::TextSensor *obj) override; + void on_text_sensor_update(text_sensor::TextSensor *obj); #endif #ifdef USE_CLIMATE - void on_climate_update(climate::Climate *obj) override; + void on_climate_update(climate::Climate *obj); #endif #ifdef USE_NUMBER - void on_number_update(number::Number *obj) override; + void on_number_update(number::Number *obj); #endif #ifdef USE_DATETIME_DATE - void on_date_update(datetime::DateEntity *obj) override; + void on_date_update(datetime::DateEntity *obj); #endif #ifdef USE_DATETIME_TIME - void on_time_update(datetime::TimeEntity *obj) override; + void on_time_update(datetime::TimeEntity *obj); #endif #ifdef USE_DATETIME_DATETIME - void on_datetime_update(datetime::DateTimeEntity *obj) override; + void on_datetime_update(datetime::DateTimeEntity *obj); #endif #ifdef USE_TEXT - void on_text_update(text::Text *obj) override; + void on_text_update(text::Text *obj); #endif #ifdef USE_SELECT - void on_select_update(select::Select *obj) override; + void on_select_update(select::Select *obj); #endif #ifdef USE_LOCK - void on_lock_update(lock::Lock *obj) override; + void on_lock_update(lock::Lock *obj); #endif #ifdef USE_VALVE - void on_valve_update(valve::Valve *obj) override; + void on_valve_update(valve::Valve *obj); #endif #ifdef USE_MEDIA_PLAYER - void on_media_player_update(media_player::MediaPlayer *obj) override; + void on_media_player_update(media_player::MediaPlayer *obj); #endif #ifdef USE_WATER_HEATER - void on_water_heater_update(water_heater::WaterHeater *obj) override; + void on_water_heater_update(water_heater::WaterHeater *obj); #endif #ifdef USE_API_HOMEASSISTANT_SERVICES void send_homeassistant_action(const HomeassistantActionRequest &call); @@ -188,13 +187,13 @@ class APIServer final : public Component, #endif #ifdef USE_ALARM_CONTROL_PANEL - void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) override; + void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj); #endif #ifdef USE_EVENT - void on_event(event::Event *obj) override; + void on_event(event::Event *obj); #endif #ifdef USE_UPDATE - void on_update(update::UpdateEntity *obj) override; + void on_update(update::UpdateEntity *obj); #endif #ifdef USE_ZWAVE_PROXY void on_zwave_proxy_request(const ZWaveProxyRequest &msg); diff --git a/esphome/components/api/subscribe_state.h b/esphome/components/api/subscribe_state.h index 6b1ae9651d..36e96e1081 100644 --- a/esphome/components/api/subscribe_state.h +++ b/esphome/components/api/subscribe_state.h @@ -4,7 +4,6 @@ #ifdef USE_API #include "esphome/core/component.h" #include "esphome/core/component_iterator.h" -#include "esphome/core/controller.h" namespace esphome::api { class APIConnection; diff --git a/esphome/components/prometheus/prometheus_handler.h b/esphome/components/prometheus/prometheus_handler.h index 5574039e41..9dcbf7e686 100644 --- a/esphome/components/prometheus/prometheus_handler.h +++ b/esphome/components/prometheus/prometheus_handler.h @@ -6,8 +6,8 @@ #include "esphome/components/web_server_base/web_server_base.h" #include "esphome/core/component.h" -#include "esphome/core/controller.h" #include "esphome/core/entity_base.h" +#include "esphome/core/entity_includes.h" #include "esphome/core/progmem.h" #ifdef USE_CLIMATE #include "esphome/core/log.h" diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 2459163786..3ec365cf38 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -62,7 +62,7 @@ DEFAULT_PORT = 80 web_server_ns = cg.esphome_ns.namespace("web_server") -WebServer = web_server_ns.class_("WebServer", cg.Component, cg.Controller) +WebServer = web_server_ns.class_("WebServer", cg.Component) sorting_groups = {} @@ -378,8 +378,7 @@ async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID], paren) await cg.register_component(var, config) - # Track controller registration for StaticVector sizing - CORE.register_controller() + CORE.register_controller(var) version = config[CONF_VERSION] diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 49943f8a71..4906a2050a 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -5,7 +5,6 @@ #include "esphome/components/network/util.h" #include "esphome/core/application.h" #include "esphome/core/defines.h" -#include "esphome/core/controller_registry.h" #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -365,7 +364,6 @@ json::SerializationBuffer<> WebServer::get_config_json() { } void WebServer::setup() { - ControllerRegistry::register_controller(this); this->base_->init(); #ifdef USE_LOGGER diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 3e50eeeec0..3236ccfd3c 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -6,8 +6,8 @@ #include "esphome/components/web_server_base/web_server_base.h" #ifdef USE_WEBSERVER #include "esphome/core/component.h" -#include "esphome/core/controller.h" #include "esphome/core/entity_base.h" +#include "esphome/core/entity_includes.h" #include "esphome/core/progmem.h" #ifdef USE_LOGGER #include "esphome/components/logger/logger.h" @@ -187,7 +187,7 @@ class DeferredUpdateEventSourceList final : public std::list'. void handle_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -301,7 +301,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand #endif #ifdef USE_SWITCH - void on_switch_update(switch_::Switch *obj) override; + void on_switch_update(switch_::Switch *obj); /// Handle a switch request under '/switch//'. void handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -319,7 +319,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand #endif #ifdef USE_BINARY_SENSOR - void on_binary_sensor_update(binary_sensor::BinarySensor *obj) override; + void on_binary_sensor_update(binary_sensor::BinarySensor *obj); /// Handle a binary sensor request under '/binary_sensor/'. void handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -329,7 +329,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand #endif #ifdef USE_FAN - void on_fan_update(fan::Fan *obj) override; + void on_fan_update(fan::Fan *obj); /// Handle a fan request under '/fan//'. void handle_fan_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -339,7 +339,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand #endif #ifdef USE_LIGHT - void on_light_update(light::LightState *obj) override; + void on_light_update(light::LightState *obj); /// Handle a light request under '/light//'. void handle_light_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -349,7 +349,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand #endif #ifdef USE_TEXT_SENSOR - void on_text_sensor_update(text_sensor::TextSensor *obj) override; + void on_text_sensor_update(text_sensor::TextSensor *obj); /// Handle a text sensor request under '/text_sensor/'. void handle_text_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -359,7 +359,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand #endif #ifdef USE_COVER - void on_cover_update(cover::Cover *obj) override; + void on_cover_update(cover::Cover *obj); /// Handle a cover request under '/cover//'. void handle_cover_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -369,7 +369,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand #endif #ifdef USE_NUMBER - void on_number_update(number::Number *obj) override; + void on_number_update(number::Number *obj); /// Handle a number request under '/number/'. void handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -378,7 +378,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand #endif #ifdef USE_DATETIME_DATE - void on_date_update(datetime::DateEntity *obj) override; + void on_date_update(datetime::DateEntity *obj); /// Handle a date request under '/date/'. void handle_date_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -387,7 +387,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand #endif #ifdef USE_DATETIME_TIME - void on_time_update(datetime::TimeEntity *obj) override; + void on_time_update(datetime::TimeEntity *obj); /// Handle a time request under '/time/'. void handle_time_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -396,7 +396,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand #endif #ifdef USE_DATETIME_DATETIME - void on_datetime_update(datetime::DateTimeEntity *obj) override; + void on_datetime_update(datetime::DateTimeEntity *obj); /// Handle a datetime request under '/datetime/'. void handle_datetime_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -405,7 +405,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand #endif #ifdef USE_TEXT - void on_text_update(text::Text *obj) override; + void on_text_update(text::Text *obj); /// Handle a text input request under '/text/'. void handle_text_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -414,7 +414,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand #endif #ifdef USE_SELECT - void on_select_update(select::Select *obj) override; + void on_select_update(select::Select *obj); /// Handle a select request under '/select/'. void handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -423,7 +423,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand #endif #ifdef USE_CLIMATE - void on_climate_update(climate::Climate *obj) override; + void on_climate_update(climate::Climate *obj); /// Handle a climate request under '/climate/'. void handle_climate_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -432,7 +432,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand #endif #ifdef USE_LOCK - void on_lock_update(lock::Lock *obj) override; + void on_lock_update(lock::Lock *obj); /// Handle a lock request under '/lock//'. void handle_lock_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -442,7 +442,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand #endif #ifdef USE_VALVE - void on_valve_update(valve::Valve *obj) override; + void on_valve_update(valve::Valve *obj); /// Handle a valve request under '/valve//'. void handle_valve_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -452,7 +452,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand #endif #ifdef USE_ALARM_CONTROL_PANEL - void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) override; + void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj); /// Handle a alarm_control_panel request under '/alarm_control_panel/'. void handle_alarm_control_panel_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -462,7 +462,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand #endif #ifdef USE_WATER_HEATER - void on_water_heater_update(water_heater::WaterHeater *obj) override; + void on_water_heater_update(water_heater::WaterHeater *obj); /// Handle a water_heater request under '/water_heater//'. void handle_water_heater_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -484,8 +484,13 @@ class WebServer final : public Controller, public Component, public AsyncWebHand static json::SerializationBuffer<> radio_frequency_all_json_generator(WebServer *web_server, void *source); #endif +#ifdef USE_MEDIA_PLAYER + // Not exposed over HTTP; the stub only satisfies ControllerContract + void on_media_player_update(media_player::MediaPlayer *) {} +#endif + #ifdef USE_EVENT - void on_event(event::Event *obj) override; + void on_event(event::Event *obj); static json::SerializationBuffer<> event_state_json_generator(WebServer *web_server, void *source); static json::SerializationBuffer<> event_all_json_generator(WebServer *web_server, void *source); @@ -495,7 +500,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand #endif #ifdef USE_UPDATE - void on_update(update::UpdateEntity *obj) override; + void on_update(update::UpdateEntity *obj); /// Handle a update request under '/update/'. void handle_update_request(AsyncWebServerRequest *request, const UrlMatch &match); diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 5fcad90a81..fb75285081 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -52,8 +52,8 @@ if TYPE_CHECKING: _LOGGER = logging.getLogger(__name__) -# Key for tracking controller count in CORE.data for ControllerRegistry StaticVector sizing -KEY_CONTROLLER_REGISTRY_COUNT = "controller_registry_count" +# Key for the controllers (APIServer, WebServer) that receive entity state updates +KEY_CONTROLLER_REGISTRY_CONTROLLERS = "controller_registry_controllers" # CORE.data key for the "is_rp2040 deprecation warning already fired this # run" flag. Mirrors the ``cv.only_on_rp2040`` dedupe pattern; cleared @@ -1209,10 +1209,9 @@ class EsphomeCore: if not self.platform_counts[platform_name]: self.platform_counts[platform_name] = 1 - def register_controller(self) -> None: - """Track registration of a Controller for ControllerRegistry StaticVector sizing.""" - controller_count = self.data.setdefault(KEY_CONTROLLER_REGISTRY_COUNT, 0) - self.data[KEY_CONTROLLER_REGISTRY_COUNT] = controller_count + 1 + def register_controller(self, controller: "MockObj") -> None: + """Register a controller that receives every entity state update.""" + self.data.setdefault(KEY_CONTROLLER_REGISTRY_CONTROLLERS, []).append(controller) @property def cpp_main_section(self): diff --git a/esphome/core/component_iterator.h b/esphome/core/component_iterator.h index fac09e9e14..3c11d4a880 100644 --- a/esphome/core/component_iterator.h +++ b/esphome/core/component_iterator.h @@ -1,7 +1,7 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/controller.h" +#include "esphome/core/entity_includes.h" #include "esphome/core/helpers.h" #ifdef USE_CAMERA diff --git a/esphome/core/config.py b/esphome/core/config.py index 051a136aab..4d3fb83312 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -46,7 +46,7 @@ from esphome.const import ( ) from esphome.core import ( CORE, - KEY_CONTROLLER_REGISTRY_COUNT, + KEY_CONTROLLER_REGISTRY_CONTROLLERS, CoroPriority, coroutine_with_priority, ) @@ -674,12 +674,22 @@ async def _add_platform_defines() -> None: @coroutine_with_priority(CoroPriority.FINAL) -async def _add_controller_registry_define() -> None: - # Generate StaticVector size for ControllerRegistry - controller_count = CORE.data.get(KEY_CONTROLLER_REGISTRY_COUNT, 0) - if controller_count > 0: - cg.add_define("USE_CONTROLLER_REGISTRY") - cg.add_define("CONTROLLER_REGISTRY_MAX", controller_count) +async def _add_controller_registry_dispatch() -> None: + # controller_dispatch.h defines ControllerRegistry::notify_*() as direct + # calls on the controllers returned by esphome_controllers(), emitted as + # static auto esphome_controllers() { return std::tuple{a, b}; } + controllers = CORE.data.get(KEY_CONTROLLER_REGISTRY_CONTROLLERS) + if not controllers: + return + cg.add_define("USE_CONTROLLER_REGISTRY") + controllers = cg.ArrayInitializer(*controllers) + cg.add_global(cg.RawStatement("#include ")) + cg.add_global( + cg.RawStatement( + f"static auto esphome_controllers() {{ return std::tuple{controllers}; }}" + ) + ) + cg.add_global(cg.RawStatement('#include "esphome/core/controller_dispatch.h"')) @coroutine_with_priority(CoroPriority.FINAL) @@ -755,7 +765,7 @@ async def to_code(config: ConfigType) -> None: ) CORE.add_job(_add_platform_defines) - CORE.add_job(_add_controller_registry_define) + CORE.add_job(_add_controller_registry_dispatch) CORE.add_job(_add_looping_components) CORE.add_job(_add_automations, config) diff --git a/esphome/core/controller.h b/esphome/core/controller.h deleted file mode 100644 index 09975b465f..0000000000 --- a/esphome/core/controller.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include "esphome/core/entity_includes.h" - -namespace esphome { - -class Controller { - public: -// Controller virtual methods (generated from entity_types.h) -// NOLINTBEGIN(bugprone-macro-parentheses) -#define ENTITY_TYPE_(type, singular, plural, count, upper) // no controller callback -#define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) virtual void on_##callback(type *obj){}; -#include "esphome/core/entity_types.h" -#undef ENTITY_TYPE_ -#undef ENTITY_CONTROLLER_TYPE_ - // NOLINTEND(bugprone-macro-parentheses) -}; - -} // namespace esphome diff --git a/esphome/core/controller_dispatch.h b/esphome/core/controller_dispatch.h new file mode 100644 index 0000000000..fc6a03556d --- /dev/null +++ b/esphome/core/controller_dispatch.h @@ -0,0 +1,50 @@ +#pragma once + +// Included once by the generated main.cpp, after it returns the registered controllers as a tuple: +// +// static auto esphome_controllers() { return std::tuple{api_apiserver_id, web_server_webserver_id}; } +// #include "esphome/core/controller_dispatch.h" +// +// Defines ControllerRegistry::notify_*() as direct calls on those controllers. Excluded from +// esphome.h and the clang-tidy all-headers file, so nothing else includes it. + +#include +#include + +#include "esphome/core/controller_registry.h" + +namespace esphome { + +// NOLINTBEGIN(bugprone-macro-parentheses) + +/// A controller provides a plain on_*_update() member for every entity type in the build. +template +concept ControllerContract = requires(T &controller) { + controller; // keeps the requirement list non-empty when no entity type has a callback +#define ENTITY_TYPE_(type, singular, plural, count, upper) // no controller callback +#define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) \ + controller.on_##callback(static_cast(nullptr)); +#include "esphome/core/entity_types.h" +#undef ENTITY_TYPE_ +#undef ENTITY_CONTROLLER_TYPE_ +}; + +template constexpr bool controllers_satisfy_contract(std::tuple *) { + return (ControllerContract> && ...); +} +static_assert(controllers_satisfy_contract(static_cast(nullptr)), + "A registered controller is missing an on_*_update() callback for an entity type in this build " + "(ControllerContract in esphome/core/controller_dispatch.h)"); + +#define ENTITY_TYPE_(type, singular, plural, count, upper) // no controller callback +#define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) \ + void ControllerRegistry::notify_##callback(type *obj) { \ + std::apply([obj](auto *...controller) { (controller->on_##callback(obj), ...); }, esphome_controllers()); \ + } +#include "esphome/core/entity_types.h" +#undef ENTITY_TYPE_ +#undef ENTITY_CONTROLLER_TYPE_ + +// NOLINTEND(bugprone-macro-parentheses) + +} // namespace esphome diff --git a/esphome/core/controller_registry.cpp b/esphome/core/controller_registry.cpp deleted file mode 100644 index 907e0f923d..0000000000 --- a/esphome/core/controller_registry.cpp +++ /dev/null @@ -1,11 +0,0 @@ -#include "esphome/core/controller_registry.h" - -#ifdef USE_CONTROLLER_REGISTRY - -namespace esphome { - -StaticVector ControllerRegistry::controllers; - -} // namespace esphome - -#endif // USE_CONTROLLER_REGISTRY diff --git a/esphome/core/controller_registry.h b/esphome/core/controller_registry.h index c6113116ff..8b1b6c7f9d 100644 --- a/esphome/core/controller_registry.h +++ b/esphome/core/controller_registry.h @@ -5,37 +5,18 @@ #ifdef USE_CONTROLLER_REGISTRY #include "esphome/core/entity_includes.h" -#include "esphome/core/helpers.h" namespace esphome { -class Controller; - -/** Global registry for Controllers to receive entity state updates. +/** Fan-out of entity state updates to the controllers (APIServer, WebServer). * - * This singleton registry allows Controllers (APIServer, WebServer) to receive - * entity state change notifications without storing per-entity callbacks. - * - * Instead of each entity maintaining controller callbacks (32 bytes overhead per entity), - * entities call ControllerRegistry::notify_*_update() which iterates the small list - * of registered controllers (typically 2: API and WebServer). - * - * Each notify method directly iterates controllers and calls the virtual method, - * avoiding function pointer indirection for minimal dispatch overhead. - * - * Memory savings: 32 bytes per entity (2 controllers × 16 bytes std::function overhead) - * Typical config (25 entities): ~780 bytes saved - * Large config (80 entities): ~2,540 bytes saved + * Entities call ControllerRegistry::notify_*_update() instead of holding + * per-entity controller callbacks. The functions are only declared here; + * controller_dispatch.h, included by the generated main.cpp, defines them as + * direct calls on the controllers registered through CORE.register_controller(). */ class ControllerRegistry { public: - /** Register a controller to receive entity state updates. - * - * Controllers should call this in their setup() method. - * Typically only APIServer and WebServer register. - */ - static void register_controller(Controller *controller) { controllers.push_back(controller); } - // Notify method declarations (generated from entity_types.h) // NOLINTBEGIN(bugprone-macro-parentheses) #define ENTITY_TYPE_(type, singular, plural, count, upper) // no controller callback @@ -45,38 +26,8 @@ class ControllerRegistry { #undef ENTITY_TYPE_ #undef ENTITY_CONTROLLER_TYPE_ // NOLINTEND(bugprone-macro-parentheses) - - protected: - static StaticVector controllers; }; } // namespace esphome -// Include controller.h AFTER the class definition so notify methods can be -// defined inline. This is safe because controller_registry.h is only ever -// included from .cpp files, never from other headers. -#include "esphome/core/controller.h" - -namespace esphome { - -// Inline notify methods — each is a tiny loop over 1-2 controllers. -// Defining them here (rather than in controller_registry.cpp) allows the -// compiler to inline them into the single call site in each entity's -// notify_frontend_(), eliminating an unnecessary function-call frame. - -// NOLINTBEGIN(bugprone-macro-parentheses) -#define ENTITY_TYPE_(type, singular, plural, count, upper) // no controller callback -#define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) \ - inline void ControllerRegistry::notify_##callback(type *obj) { \ - for (auto *controller : controllers) { \ - controller->on_##callback(obj); \ - } \ - } -#include "esphome/core/entity_types.h" -#undef ENTITY_TYPE_ -#undef ENTITY_CONTROLLER_TYPE_ -// NOLINTEND(bugprone-macro-parentheses) - -} // namespace esphome - #endif // USE_CONTROLLER_REGISTRY diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 7bc08ea577..e1ae1d5fb6 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -629,7 +629,6 @@ #define USE_DASHBOARD_IMPORT // Default counts for static analysis -#define CONTROLLER_REGISTRY_MAX 2 #define ESPHOME_AREA_COUNT 10 #define ESPHOME_COMPONENT_COUNT 50 #define ESPHOME_DEVICE_COUNT 10 diff --git a/esphome/cpp_types.py b/esphome/cpp_types.py index 45d6559b3f..04892f5fea 100644 --- a/esphome/cpp_types.py +++ b/esphome/cpp_types.py @@ -37,7 +37,6 @@ optional = global_ns.namespace("std").class_("optional") arduino_json_ns = global_ns.namespace("ArduinoJson") JsonObject = arduino_json_ns.class_("JsonObject") JsonObjectConst = arduino_json_ns.class_("JsonObjectConst") -Controller = esphome_ns.class_("Controller") GPIOPin = esphome_ns.class_("GPIOPin") InternalGPIOPin = esphome_ns.class_("InternalGPIOPin", GPIOPin) gpio_ns = esphome_ns.namespace("gpio") diff --git a/esphome/writer.py b/esphome/writer.py index 0b9e7669ef..9ab5d644c5 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -211,6 +211,16 @@ VERSION_H_TARGET = "esphome/core/version.h" BUILD_INFO_DATA_H_TARGET = "esphome/core/build_info_data.h" BUILD_INFO_DATA_CPP_TARGET = "esphome/core/build_info_data.cpp" ENTITY_TYPES_H_TARGET = "esphome/core/entity_types.h" +# Headers that must not be included bare from esphome.h or the clang-tidy +# all-headers file: X-macro files, headers main.cpp includes itself, and +# deprecated headers that only resolve when their new component is loaded. +ESPHOME_H_EXCLUDE = { + Path(ENTITY_TYPES_H_TARGET), + # main.cpp includes it after defining esphome_controllers() + Path("esphome/core/controller_dispatch.h"), + # moved to components/ring_buffer/, removed in 2026.11.0 + Path("esphome/core/ring_buffer.h"), +} ESPHOME_README_TXT = """ THIS DIRECTORY IS AUTO-GENERATED, DO NOT MODIFY @@ -236,20 +246,9 @@ def copy_src_tree(): source_files_l.sort() # Build #include list for esphome.h - # X-macro files are included multiple times with different macro definitions - # and must not be included bare in esphome.h - # Deprecated headers that re-export from a relocated component must not be - # auto-included, since their #include of the new path only resolves when the - # new component is loaded by a consumer. - esphome_h_exclude = { - Path(ENTITY_TYPES_H_TARGET), - Path( - "esphome/core/ring_buffer.h" - ), # moved to components/ring_buffer/, removed in 2026.11.0 - } include_l = [] for target, _ in source_files_l: - if target.suffix in HEADER_FILE_EXTENSIONS and target not in esphome_h_exclude: + if target.suffix in HEADER_FILE_EXTENSIONS and target not in ESPHOME_H_EXCLUDE: include_l.append(f'#include "{target}"') include_l.append("") include_s = "\n".join(include_l) diff --git a/script/helpers.py b/script/helpers.py index a8a237118f..fb2c964bdd 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -429,11 +429,9 @@ def build_all_include(header_files: list[str] | None = None) -> None: if line ] - from esphome.writer import ENTITY_TYPES_H_TARGET + from esphome.writer import ESPHOME_H_EXCLUDE - # X-macro files are included multiple times with different macro definitions - # and must not be included bare in the all-include header - exclude = {ENTITY_TYPES_H_TARGET} + exclude = {path.as_posix() for path in ESPHOME_H_EXCLUDE} headers = [f'#include "{h}"' for h in header_files if h not in exclude] headers.sort() headers.append("") diff --git a/tests/components/api/__init__.py b/tests/components/api/__init__.py index 2aa558726c..a5e3731c75 100644 --- a/tests/components/api/__init__.py +++ b/tests/components/api/__init__.py @@ -1,5 +1,4 @@ import esphome.codegen as cg -from esphome.core import CORE from tests.testing_helpers import ComponentManifestOverride @@ -12,6 +11,5 @@ def override_manifest(manifest: ComponentManifestOverride) -> None: cg.add_define("API_MAX_SEND_QUEUE", 8) cg.add_define("MAX_API_CONNECTIONS", 1) cg.add_define("USE_SOCKET_IMPL_BSD_SOCKETS") - CORE.register_controller() # api_server registers with the controller registry manifest.to_code = to_code_testing diff --git a/tests/unit_tests/core/test_config.py b/tests/unit_tests/core/test_config.py index 07cff003cd..0db7039668 100644 --- a/tests/unit_tests/core/test_config.py +++ b/tests/unit_tests/core/test_config.py @@ -9,6 +9,7 @@ from unittest.mock import MagicMock, Mock, patch import pytest from esphome import config_validation as cv, core +import esphome.codegen as cg from esphome.components.safe_mode import to_code as safe_mode_to_code from esphome.const import ( CONF_AREA, @@ -23,7 +24,7 @@ from esphome.const import ( KEY_TARGET_PLATFORM, Toolchain, ) -from esphome.core import CORE, config +from esphome.core import CORE, KEY_CONTROLLER_REGISTRY_CONTROLLERS, config from esphome.core.config import ( Area, make_app_name_cpp, @@ -455,6 +456,35 @@ async def test_add_looping_components_with_entries() -> None: assert "(1 * HasLoopOverride::value)" in text +@pytest.mark.asyncio +async def test_add_controller_registry_dispatch_without_controllers() -> None: + """Nothing is emitted when no controller registered.""" + CORE.data.pop(KEY_CONTROLLER_REGISTRY_CONTROLLERS, None) + + await config._add_controller_registry_dispatch() + + assert "USE_CONTROLLER_REGISTRY" not in {d.name for d in CORE.defines} + assert not [s for s in CORE.global_statements if "controller" in str(s)] + + +@pytest.mark.asyncio +async def test_add_controller_registry_dispatch_with_controllers() -> None: + """Registered controllers become one tuple plus the dispatch include.""" + CORE.register_controller(cg.MockObj("api_apiserver_id")) + CORE.register_controller(cg.MockObj("web_server_webserver_id")) + + await config._add_controller_registry_dispatch() + + assert "USE_CONTROLLER_REGISTRY" in {d.name for d in CORE.defines} + statements = [str(s) for s in CORE.global_statements] + assert "#include " in statements + assert ( + "static auto esphome_controllers() { return std::tuple{api_apiserver_id, web_server_webserver_id}; }" + in statements + ) + assert '#include "esphome/core/controller_dispatch.h"' in statements + + def test_valid_include_with_angle_brackets() -> None: """Test valid_include accepts angle bracket includes.""" assert valid_include("") == "" diff --git a/tests/unit_tests/test_codegen.py b/tests/unit_tests/test_codegen.py index 8d01fef7c2..c5c971b22d 100644 --- a/tests/unit_tests/test_codegen.py +++ b/tests/unit_tests/test_codegen.py @@ -70,7 +70,6 @@ from esphome.cpp_generator import _extract_component_ns "arduino_json_ns", "JsonObject", "JsonObjectConst", - "Controller", "GPIOPin", ), )