mirror of
https://github.com/esphome/esphome.git
synced 2026-10-07 11:26:39 +00:00
[core] Tidy the controller dispatch after review
Move ControllerContract next to its only user in controller_dispatch.h, collapse the contract check into one static_assert, and let codegen build the tuple with ArrayInitializer after emitting the tuple include. Share one esphome.h exclusion set between the writer and the clang-tidy all-headers file so the dispatch header is skipped by both. Add unit tests for the generated dispatch.
This commit is contained in:
@@ -484,7 +484,7 @@ class WebServer final : public Component, public AsyncWebHandler {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_MEDIA_PLAYER
|
#ifdef USE_MEDIA_PLAYER
|
||||||
void on_media_player_update(media_player::MediaPlayer *obj) {}
|
void on_media_player_update(media_player::MediaPlayer *) {}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_EVENT
|
#ifdef USE_EVENT
|
||||||
|
|||||||
@@ -1210,12 +1210,7 @@ class EsphomeCore:
|
|||||||
self.platform_counts[platform_name] = 1
|
self.platform_counts[platform_name] = 1
|
||||||
|
|
||||||
def register_controller(self, controller: "MockObj") -> None:
|
def register_controller(self, controller: "MockObj") -> None:
|
||||||
"""Register a controller that receives every entity state update.
|
"""Register a controller that receives every entity state update."""
|
||||||
|
|
||||||
Code generation defines the ControllerRegistry notify functions in
|
|
||||||
main.cpp as direct calls on each registered controller, so the C++
|
|
||||||
class only needs the on_*_update methods, not a base class.
|
|
||||||
"""
|
|
||||||
self.data.setdefault(KEY_CONTROLLER_REGISTRY_CONTROLLERS, []).append(controller)
|
self.data.setdefault(KEY_CONTROLLER_REGISTRY_CONTROLLERS, []).append(controller)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -676,15 +676,17 @@ async def _add_platform_defines() -> None:
|
|||||||
@coroutine_with_priority(CoroPriority.FINAL)
|
@coroutine_with_priority(CoroPriority.FINAL)
|
||||||
async def _add_controller_registry_dispatch() -> None:
|
async def _add_controller_registry_dispatch() -> None:
|
||||||
# controller_dispatch.h defines ControllerRegistry::notify_*() as direct
|
# controller_dispatch.h defines ControllerRegistry::notify_*() as direct
|
||||||
# calls on the controllers returned by esphome_controllers().
|
# 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)
|
controllers = CORE.data.get(KEY_CONTROLLER_REGISTRY_CONTROLLERS)
|
||||||
if not controllers:
|
if not controllers:
|
||||||
return
|
return
|
||||||
cg.add_define("USE_CONTROLLER_REGISTRY")
|
cg.add_define("USE_CONTROLLER_REGISTRY")
|
||||||
entries = ", ".join(str(var) for var in controllers)
|
controllers = cg.ArrayInitializer(*controllers)
|
||||||
|
cg.add_global(cg.RawStatement("#include <tuple>"))
|
||||||
cg.add_global(
|
cg.add_global(
|
||||||
cg.RawStatement(
|
cg.RawStatement(
|
||||||
f"static auto esphome_controllers() {{ return std::tuple{{{entries}}}; }}"
|
f"static auto esphome_controllers() {{ return std::tuple{controllers}; }}"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
cg.add_global(cg.RawStatement('#include "esphome/core/controller_dispatch.h"'))
|
cg.add_global(cg.RawStatement('#include "esphome/core/controller_dispatch.h"'))
|
||||||
|
|||||||
@@ -6,26 +6,36 @@
|
|||||||
// #include "esphome/core/controller_dispatch.h"
|
// #include "esphome/core/controller_dispatch.h"
|
||||||
//
|
//
|
||||||
// Defines ControllerRegistry::notify_*() as direct calls on those controllers. Excluded from
|
// Defines ControllerRegistry::notify_*() as direct calls on those controllers. Excluded from
|
||||||
// esphome.h so nothing else includes it.
|
// esphome.h and the clang-tidy all-headers file, so nothing else includes it.
|
||||||
|
|
||||||
#include "esphome/core/controller_registry.h"
|
|
||||||
|
|
||||||
#ifdef USE_CONTROLLER_REGISTRY
|
|
||||||
|
|
||||||
#include <tuple>
|
#include <tuple>
|
||||||
#include <type_traits>
|
#include <type_traits>
|
||||||
|
|
||||||
|
#include "esphome/core/controller_registry.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
|
|
||||||
template<typename... Controllers> constexpr bool controllers_satisfy_contract(std::tuple<Controllers...> *) {
|
|
||||||
static_assert((ControllerContract<std::remove_pointer_t<Controllers>> && ...),
|
|
||||||
"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<decltype(esphome_controllers()) *>(nullptr)));
|
|
||||||
|
|
||||||
// NOLINTBEGIN(bugprone-macro-parentheses)
|
// 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_TYPE_(type, singular, plural, count, upper) // no controller callback
|
||||||
#define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) \
|
#define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) \
|
||||||
void ControllerRegistry::notify_##callback(type *obj) { \
|
void ControllerRegistry::notify_##callback(type *obj) { \
|
||||||
@@ -34,8 +44,7 @@ static_assert(controllers_satisfy_contract(static_cast<decltype(esphome_controll
|
|||||||
#include "esphome/core/entity_types.h"
|
#include "esphome/core/entity_types.h"
|
||||||
#undef ENTITY_TYPE_
|
#undef ENTITY_TYPE_
|
||||||
#undef ENTITY_CONTROLLER_TYPE_
|
#undef ENTITY_CONTROLLER_TYPE_
|
||||||
|
|
||||||
// NOLINTEND(bugprone-macro-parentheses)
|
// NOLINTEND(bugprone-macro-parentheses)
|
||||||
|
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
|
||||||
#endif // USE_CONTROLLER_REGISTRY
|
|
||||||
|
|||||||
@@ -8,28 +8,12 @@
|
|||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
|
|
||||||
/// A controller provides a plain on_*_update() member for every entity type in the build.
|
|
||||||
/// Checked by a static_assert in the generated main.cpp for each registered controller.
|
|
||||||
// NOLINTBEGIN(bugprone-macro-parentheses)
|
|
||||||
template<typename T>
|
|
||||||
concept ControllerContract = requires(T &controller) {
|
|
||||||
controller;
|
|
||||||
#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_
|
|
||||||
};
|
|
||||||
// NOLINTEND(bugprone-macro-parentheses)
|
|
||||||
|
|
||||||
/** Fan-out of entity state updates to the controllers (APIServer, WebServer).
|
/** Fan-out of entity state updates to the controllers (APIServer, WebServer).
|
||||||
*
|
*
|
||||||
* Entities call ControllerRegistry::notify_*_update() instead of holding
|
* Entities call ControllerRegistry::notify_*_update() instead of holding
|
||||||
* per-entity controller callbacks. The notify functions are only declared here;
|
* per-entity controller callbacks. The functions are only declared here;
|
||||||
* code generation defines them in main.cpp as direct calls on each controller
|
* controller_dispatch.h, included by the generated main.cpp, defines them as
|
||||||
* that registered through CORE.register_controller(), so there is no virtual
|
* direct calls on the controllers registered through CORE.register_controller().
|
||||||
* dispatch, no controller base class and no runtime list of controllers.
|
|
||||||
*/
|
*/
|
||||||
class ControllerRegistry {
|
class ControllerRegistry {
|
||||||
public:
|
public:
|
||||||
|
|||||||
+11
-16
@@ -211,7 +211,16 @@ VERSION_H_TARGET = "esphome/core/version.h"
|
|||||||
BUILD_INFO_DATA_H_TARGET = "esphome/core/build_info_data.h"
|
BUILD_INFO_DATA_H_TARGET = "esphome/core/build_info_data.h"
|
||||||
BUILD_INFO_DATA_CPP_TARGET = "esphome/core/build_info_data.cpp"
|
BUILD_INFO_DATA_CPP_TARGET = "esphome/core/build_info_data.cpp"
|
||||||
ENTITY_TYPES_H_TARGET = "esphome/core/entity_types.h"
|
ENTITY_TYPES_H_TARGET = "esphome/core/entity_types.h"
|
||||||
CONTROLLER_DISPATCH_H_TARGET = "esphome/core/controller_dispatch.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 = """
|
ESPHOME_README_TXT = """
|
||||||
THIS DIRECTORY IS AUTO-GENERATED, DO NOT MODIFY
|
THIS DIRECTORY IS AUTO-GENERATED, DO NOT MODIFY
|
||||||
|
|
||||||
@@ -237,23 +246,9 @@ def copy_src_tree():
|
|||||||
source_files_l.sort()
|
source_files_l.sort()
|
||||||
|
|
||||||
# Build #include list for esphome.h
|
# 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(
|
|
||||||
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
|
|
||||||
}
|
|
||||||
include_l = []
|
include_l = []
|
||||||
for target, _ in source_files_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(f'#include "{target}"')
|
||||||
include_l.append("")
|
include_l.append("")
|
||||||
include_s = "\n".join(include_l)
|
include_s = "\n".join(include_l)
|
||||||
|
|||||||
+2
-4
@@ -429,11 +429,9 @@ def build_all_include(header_files: list[str] | None = None) -> None:
|
|||||||
if line
|
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
|
exclude = {str(path) for path in ESPHOME_H_EXCLUDE}
|
||||||
# and must not be included bare in the all-include header
|
|
||||||
exclude = {ENTITY_TYPES_H_TARGET}
|
|
||||||
headers = [f'#include "{h}"' for h in header_files if h not in exclude]
|
headers = [f'#include "{h}"' for h in header_files if h not in exclude]
|
||||||
headers.sort()
|
headers.sort()
|
||||||
headers.append("")
|
headers.append("")
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from unittest.mock import MagicMock, Mock, patch
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from esphome import config_validation as cv, core
|
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.components.safe_mode import to_code as safe_mode_to_code
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_AREA,
|
CONF_AREA,
|
||||||
@@ -23,7 +24,7 @@ from esphome.const import (
|
|||||||
KEY_TARGET_PLATFORM,
|
KEY_TARGET_PLATFORM,
|
||||||
Toolchain,
|
Toolchain,
|
||||||
)
|
)
|
||||||
from esphome.core import CORE, config
|
from esphome.core import CORE, KEY_CONTROLLER_REGISTRY_CONTROLLERS, config
|
||||||
from esphome.core.config import (
|
from esphome.core.config import (
|
||||||
Area,
|
Area,
|
||||||
make_app_name_cpp,
|
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
|
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:
|
def test_valid_include_with_angle_brackets() -> None:
|
||||||
"""Test valid_include accepts angle bracket includes."""
|
"""Test valid_include accepts angle bracket includes."""
|
||||||
assert valid_include("<ArduinoJson.h>") == "<ArduinoJson.h>"
|
assert valid_include("<ArduinoJson.h>") == "<ArduinoJson.h>"
|
||||||
|
|||||||
Reference in New Issue
Block a user