[core] Dispatch controller updates through generated direct calls (#19588)

This commit is contained in:
J. Nick Koston
2026-09-24 17:24:10 +00:00
committed by GitHub
parent 9df7e825d3
commit 211fd3ce54
23 changed files with 178 additions and 181 deletions
-1
View File
@@ -64,7 +64,6 @@ from esphome.cpp_types import ( # noqa: F401
Application,
Component,
ComponentPtr,
Controller,
EntityBase,
EntityCategory,
ESPTime,
+2 -3
View File
@@ -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()
-3
View File
@@ -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
+23 -24
View File
@@ -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);
-1
View File
@@ -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;
@@ -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"
+2 -3
View File
@@ -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]
@@ -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
+27 -22
View File
@@ -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<DeferredUpdateEvent
* under the '/light/...', '/sensor/...', ... URLs. A full documentation for this API
* can be found under https://esphome.io/web-api/.
*/
class WebServer final : public Controller, public Component, public AsyncWebHandler {
class WebServer final : public Component, public AsyncWebHandler {
#if !defined(USE_ESP32) && defined(USE_ARDUINO)
friend class DeferredUpdateEventSourceList;
#endif
@@ -292,7 +292,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand
#endif
#ifdef USE_SENSOR
void on_sensor_update(sensor::Sensor *obj) override;
void on_sensor_update(sensor::Sensor *obj);
/// Handle a sensor request under '/sensor/<id>'.
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/<id>/</turn_on/turn_off/toggle>'.
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/<id>'.
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/<id>/</turn_on/turn_off/toggle>'.
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/<id>/</turn_on/turn_off/toggle>'.
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/<id>'.
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/<id>/<open/close/stop/set>'.
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/<id>'.
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/<id>'.
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/<id>'.
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/<id>'.
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/<id>'.
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/<id>'.
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/<id>'.
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/<id>/</lock/unlock/open>'.
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/<id>/<open/close/stop/set>'.
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/<id>'.
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/<id>/<mode/set>'.
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/<id>'.
void handle_update_request(AsyncWebServerRequest *request, const UrlMatch &match);
+5 -6
View File
@@ -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):
+1 -1
View File
@@ -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
+18 -8
View File
@@ -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 <tuple>"))
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)
-19
View File
@@ -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
+50
View File
@@ -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 <tuple>
#include <type_traits>
#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<typename T>
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<type *>(nullptr));
#include "esphome/core/entity_types.h"
#undef ENTITY_TYPE_
#undef ENTITY_CONTROLLER_TYPE_
};
template<typename... Controllers> constexpr bool controllers_satisfy_contract(std::tuple<Controllers...> *) {
return (ControllerContract<std::remove_pointer_t<Controllers>> && ...);
}
static_assert(controllers_satisfy_contract(static_cast<decltype(esphome_controllers()) *>(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
-11
View File
@@ -1,11 +0,0 @@
#include "esphome/core/controller_registry.h"
#ifdef USE_CONTROLLER_REGISTRY
namespace esphome {
StaticVector<Controller *, CONTROLLER_REGISTRY_MAX> ControllerRegistry::controllers;
} // namespace esphome
#endif // USE_CONTROLLER_REGISTRY
+5 -54
View File
@@ -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<Controller *, CONTROLLER_REGISTRY_MAX> 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
-1
View File
@@ -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
-1
View File
@@ -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")
+11 -12
View File
@@ -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)
+2 -4
View File
@@ -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("")
-2
View File
@@ -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
+31 -1
View File
@@ -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<esphome::logger::Logger>::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 <tuple>" 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("<ArduinoJson.h>") == "<ArduinoJson.h>"
-1
View File
@@ -70,7 +70,6 @@ from esphome.cpp_generator import _extract_component_ns
"arduino_json_ns",
"JsonObject",
"JsonObjectConst",
"Controller",
"GPIOPin",
),
)