From 4b9906c5c9662786397185d50dbd33794b2b9dc6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 24 Sep 2026 17:03:03 +0100 Subject: [PATCH] [core] Move the controller dispatch into controller_dispatch.h Code generation now emits one function returning the registered controllers as a tuple plus an include; the header defines the ControllerRegistry notify functions with a fold expression and checks ControllerContract for every tuple element. --- esphome/core/config.py | 25 ++++-------------- esphome/core/controller_dispatch.h | 41 ++++++++++++++++++++++++++++++ esphome/writer.py | 4 +++ 3 files changed, 50 insertions(+), 20 deletions(-) create mode 100644 esphome/core/controller_dispatch.h diff --git a/esphome/core/config.py b/esphome/core/config.py index 9a647f72dd..2d27ebce6f 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -675,34 +675,19 @@ async def _add_platform_defines() -> None: @coroutine_with_priority(CoroPriority.FINAL) async def _add_controller_registry_dispatch() -> None: - # Define ControllerRegistry::notify_*() in main.cpp as direct calls on each - # registered controller. entity_types.h expands one definition per entity - # type that is compiled in, so only the callbacks in use are emitted. + # controller_dispatch.h defines ControllerRegistry::notify_*() as direct + # calls on the controllers returned by esphome_controllers(). controllers = CORE.data.get(KEY_CONTROLLER_REGISTRY_CONTROLLERS) if not controllers: return cg.add_define("USE_CONTROLLER_REGISTRY") - for var in controllers: - cg.add_global( - cg.RawStatement( - f"static_assert(ControllerContract>, " - f'"{var} is missing an on_*_update() callback for an entity type in this build ' - '(esphome/core/controller_registry.h)");' - ) - ) - calls = " \\\n".join(f" {var}->on_##callback(obj);" for var in controllers) + entries = ", ".join(str(var) for var in controllers) cg.add_global( cg.RawStatement( - "#define ENTITY_TYPE_(type, singular, plural, count, upper)\n" - "#define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) \\\n" - " void ControllerRegistry::notify_##callback(type *obj) { \\\n" - f"{calls} \\\n" - " }\n" - '#include "esphome/core/entity_types.h"\n' - "#undef ENTITY_TYPE_\n" - "#undef ENTITY_CONTROLLER_TYPE_" + f"static auto esphome_controllers() {{ return std::tuple{{{entries}}}; }}" ) ) + cg.add_global(cg.RawStatement('#include "esphome/core/controller_dispatch.h"')) @coroutine_with_priority(CoroPriority.FINAL) diff --git a/esphome/core/controller_dispatch.h b/esphome/core/controller_dispatch.h new file mode 100644 index 0000000000..631de50236 --- /dev/null +++ b/esphome/core/controller_dispatch.h @@ -0,0 +1,41 @@ +#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 so nothing else includes it. + +#include "esphome/core/controller_registry.h" + +#ifdef USE_CONTROLLER_REGISTRY + +#include +#include + +namespace esphome { + +template constexpr bool controllers_satisfy_contract(std::tuple *) { + static_assert((ControllerContract> && ...), + "A registered controller is missing an on_*_update() callback for an entity type in this build " + "(esphome/core/controller_registry.h)"); + return true; +} +static_assert(controllers_satisfy_contract(static_cast(nullptr))); + +// 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) \ + 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 + +#endif // USE_CONTROLLER_REGISTRY diff --git a/esphome/writer.py b/esphome/writer.py index 0b9e7669ef..3a1dc3ecf4 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -211,6 +211,7 @@ 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" +CONTROLLER_DISPATCH_H_TARGET = "esphome/core/controller_dispatch.h" ESPHOME_README_TXT = """ THIS DIRECTORY IS AUTO-GENERATED, DO NOT MODIFY @@ -243,6 +244,9 @@ def copy_src_tree(): # new component is loaded by a consumer. esphome_h_exclude = { Path(ENTITY_TYPES_H_TARGET), + Path( + CONTROLLER_DISPATCH_H_TARGET + ), # included by main.cpp once ESPHOME_CONTROLLERS is defined Path( "esphome/core/ring_buffer.h" ), # moved to components/ring_buffer/, removed in 2026.11.0