From a6b9dd321d5a6b07b33541c9cd599c90f53a0f0a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 19:55:34 -1000 Subject: [PATCH 1/6] [core] Shrink Component from 12 to 8 bytes per instance Replace 4-byte component_source_ pointer with a 9-bit index into a PROGMEM lookup table generated by Python codegen. Source names are deduplicated at codegen time (same pattern as EntityStringPool). Shrink warn_if_blocking_over_ from uint16_t (ms) to uint8_t (centiseconds), saturating at 255 (2550ms). Saves 4 bytes per component instance (typical configs: 120-200 bytes). --- esphome/core/component.cpp | 21 ++++--- esphome/core/component.h | 50 +++++++++++---- esphome/cpp_helpers.py | 121 ++++++++++++++++++++++++++++++++++++- 3 files changed, 169 insertions(+), 23 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index caaea89143..d3e9389efc 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -84,6 +84,8 @@ void store_component_error_message(const Component *component, const char *messa static constexpr uint16_t WARN_IF_BLOCKING_INCREMENT_MS = 10U; ///< How long the blocking time must be larger to warn again +// Threshold in ms (computed from centiseconds constant in component.h) +static constexpr uint32_t WARN_IF_BLOCKING_OVER_MS = static_cast(WARN_IF_BLOCKING_OVER_CS) * 10U; #ifdef USE_LOOP_PRIORITY float Component::get_loop_priority() const { return 0.0f; } @@ -273,14 +275,14 @@ void Component::call() { } } bool Component::should_warn_of_blocking(uint32_t blocking_time) { - if (blocking_time > this->warn_if_blocking_over_) { - // Prevent overflow when adding increment - if we're about to overflow, just max out - if (blocking_time + WARN_IF_BLOCKING_INCREMENT_MS < blocking_time || - blocking_time + WARN_IF_BLOCKING_INCREMENT_MS > std::numeric_limits::max()) { - this->warn_if_blocking_over_ = std::numeric_limits::max(); - } else { - this->warn_if_blocking_over_ = static_cast(blocking_time + WARN_IF_BLOCKING_INCREMENT_MS); - } + // Convert centisecond threshold to milliseconds for comparison + uint32_t threshold_ms = static_cast(this->warn_if_blocking_over_) * 10U; + if (blocking_time > threshold_ms) { + // Set new threshold: blocking_time + increment, converted back to centiseconds + uint32_t new_threshold_ms = blocking_time + WARN_IF_BLOCKING_INCREMENT_MS; + uint32_t new_cs = new_threshold_ms / 10U; + // Saturate at uint8_t max (255 = 2550ms) + this->warn_if_blocking_over_ = static_cast(new_cs > 255U ? 255U : new_cs); return true; } return false; @@ -541,4 +543,7 @@ void clear_setup_priority_overrides() { } #endif +// Weak default for component_source_lookup - overridden by generated code +__attribute__((weak)) const LogString *component_source_lookup(uint16_t) { return LOG_STR(""); } + } // namespace esphome diff --git a/esphome/core/component.h b/esphome/core/component.h index 46cd77b034..6b69190d07 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -79,11 +79,19 @@ inline constexpr uint8_t STATUS_LED_WARNING = 0x08; inline constexpr uint8_t STATUS_LED_ERROR = 0x10; // Component loop override flag uses bit 5 (set at registration time) inline constexpr uint8_t COMPONENT_HAS_LOOP = 0x20; +// High bit of 9-bit component_source index (bit 6 of component_state_) +inline constexpr uint8_t COMPONENT_SOURCE_HIGH_BIT = 0x40; +// Mask for the 9th bit of the component source index (bit 8) +inline constexpr uint16_t COMPONENT_SOURCE_INDEX_HIGH = 0x100; // Remove before 2026.8.0 enum class RetryResult { DONE, RETRY }; -inline constexpr uint16_t WARN_IF_BLOCKING_OVER_MS = 50U; +inline constexpr uint8_t WARN_IF_BLOCKING_OVER_CS = 5U; // 50ms in centiseconds (1cs = 10ms) + +/// Lookup component source name by index (1-based). Generated by Python codegen. +/// Weak default returns "" so builds without codegen still link. +const LogString *component_source_lookup(uint16_t index); class Component { public: @@ -143,7 +151,10 @@ class Component { */ virtual void on_powerdown() {} - uint8_t get_component_state() const { return this->component_state_; } + uint8_t get_component_state() const { + // Mask out COMPONENT_SOURCE_HIGH_BIT — it's internal to component source indexing + return this->component_state_ & ~COMPONENT_SOURCE_HIGH_BIT; + } /** Reset this component back to the construction state to allow setup to run again. * @@ -285,17 +296,15 @@ class Component { bool has_overridden_loop() const { return (this->component_state_ & COMPONENT_HAS_LOOP) != 0; } - /** Set where this component was loaded from for some debug messages. - * - * This is set by the ESPHome core, and should not be called manually. - */ - void set_component_source(const LogString *source) { component_source_ = source; } /** Get the integration where this component was declared as a LogString for logging. * * Returns LOG_STR("") if source not set */ const LogString *get_component_log_str() const { - return this->component_source_ == nullptr ? LOG_STR("") : this->component_source_; + uint16_t idx = this->component_source_index_; + if (this->component_state_ & COMPONENT_SOURCE_HIGH_BIT) + idx |= COMPONENT_SOURCE_INDEX_HIGH; + return idx == 0 ? LOG_STR("") : component_source_lookup(idx); } bool should_warn_of_blocking(uint32_t blocking_time); @@ -303,6 +312,20 @@ class Component { protected: friend class Application; + /** Set where this component was loaded from for some debug messages. + * + * This is set by the ESPHome core during setup, and should not be called manually. + * @param index 1-based index into the component source lookup table (0 = not set) + */ + void set_component_source_(uint16_t index) { + this->component_source_index_ = static_cast(index & 0xFF); + if (index & COMPONENT_SOURCE_INDEX_HIGH) { + this->component_state_ |= COMPONENT_SOURCE_HIGH_BIT; + } else { + this->component_state_ &= ~COMPONENT_SOURCE_HIGH_BIT; + } + } + virtual void call_setup(); void call_dump_config_(); @@ -519,15 +542,16 @@ class Component { void status_clear_warning_slow_path_(); void status_clear_error_slow_path_(); - // Ordered for optimal packing on 32-bit systems - const LogString *component_source_{nullptr}; - uint16_t warn_if_blocking_over_{WARN_IF_BLOCKING_OVER_MS}; ///< Warn if blocked for this many ms (max 65.5s) + // Ordered for optimal packing on 32-bit systems (8 bytes total with vtable) + uint8_t component_source_index_{0}; ///< Lower 8 bits of 9-bit component source index + uint8_t warn_if_blocking_over_{WARN_IF_BLOCKING_OVER_CS}; ///< Warn threshold in centiseconds (max 2550ms) /// State of this component - each bit has a purpose: /// Bits 0-2: Component state (0x00=CONSTRUCTION, 0x01=SETUP, 0x02=LOOP, 0x03=FAILED, 0x04=LOOP_DONE) /// Bit 3: STATUS_LED_WARNING /// Bit 4: STATUS_LED_ERROR /// Bit 5: Has overridden loop() (set at registration time) - /// Bits 6-7: Unused - reserved for future expansion + /// Bit 6: High bit of 9-bit component_source index + /// Bit 7: Unused - reserved for future expansion uint8_t component_state_{0x00}; volatile bool pending_enable_loop_{false}; ///< ISR-safe flag for enable_loop_soon_any_context }; @@ -598,6 +622,8 @@ class WarnIfComponentBlockingGuard { this->record_runtime_stats_(); #endif #ifndef USE_BENCHMARK + // Fast path: compare against constant threshold in ms (computed at compile time from centiseconds) + static constexpr uint32_t WARN_IF_BLOCKING_OVER_MS = static_cast(WARN_IF_BLOCKING_OVER_CS) * 10U; if (blocking_time > WARN_IF_BLOCKING_OVER_MS) [[unlikely]] { warn_blocking(this->component_, blocking_time); } diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index 8f8c693140..22d26ef3ba 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -1,3 +1,4 @@ +from dataclasses import dataclass, field import logging from esphome.const import ( @@ -7,15 +8,128 @@ from esphome.const import ( CONF_UPDATE_INTERVAL, KEY_PAST_SAFE_MODE, ) -from esphome.core import CORE, ID, coroutine +from esphome.core import CORE, ID, CoroPriority, coroutine, coroutine_with_priority from esphome.coroutine import FakeAwaitable -from esphome.cpp_generator import LogStringLiteral, add, add_define, get_variable +from esphome.cpp_generator import ( + RawStatement, + add, + add_define, + add_global, + get_variable, +) from esphome.cpp_types import App +from esphome.helpers import cpp_string_escape from esphome.types import ConfigFragmentType, ConfigType from esphome.util import Registry, RegistryEntry _LOGGER = logging.getLogger(__name__) +_COMPONENT_SOURCE_DOMAIN = "component_source_pool" + +# Maximum unique component source names (9-bit index, 0 = not set) +_MAX_COMPONENT_SOURCES = 510 + + +@dataclass +class ComponentSourcePool: + """Pool of component source names for PROGMEM lookup table. + + Source names are registered during to_code() and assigned 1-based indices. + Index 0 means "not set" (returns LOG_STR("")). At render time, + the pool generates a C++ PROGMEM table + lookup function. + """ + + sources: dict[str, int] = field(default_factory=dict) + table_registered: bool = False + + +def _get_source_pool() -> ComponentSourcePool: + """Get or create the component source pool from CORE.data.""" + if _COMPONENT_SOURCE_DOMAIN not in CORE.data: + CORE.data[_COMPONENT_SOURCE_DOMAIN] = ComponentSourcePool() + return CORE.data[_COMPONENT_SOURCE_DOMAIN] + + +def _ensure_source_table_registered() -> None: + """Schedule the table generation job (once).""" + pool = _get_source_pool() + if pool.table_registered: + return + pool.table_registered = True + CORE.add_job(_generate_component_source_table) + + +def register_component_source(name: str) -> int: + """Register a component source name and return its 1-based index. + + Deduplicates: multiple components from the same source share one index. + """ + if not name: + return 0 + pool = _get_source_pool() + if name in pool.sources: + return pool.sources[name] + idx = len(pool.sources) + 1 + if idx > _MAX_COMPONENT_SOURCES: + raise ValueError( + f"Too many unique component source names (max {_MAX_COMPONENT_SOURCES}), got {idx}: '{name}'" + ) + pool.sources[name] = idx + _ensure_source_table_registered() + return idx + + +def _generate_source_table_code( + table_var: str, + lookup_fn: str, + strings: dict[str, int], +) -> str: + """Generate C++ PROGMEM table + LogString* lookup for component sources. + + Same pattern as entity_helpers._generate_category_code but returns + const LogString* instead of const char* (needed for LOG_STR_ARG). + """ + if not strings: + return "" + + sorted_strings = sorted(strings.items(), key=lambda x: x[1]) + count = len(sorted_strings) + + # Emit individual PROGMEM char arrays so string data lives in flash on ESP8266 + lines: list[str] = [] + var_names: list[str] = [] + for i, (s, _) in enumerate(sorted_strings): + var_name = f"{table_var}_STR_{i}" + var_names.append(var_name) + lines.append( + f"static const char {var_name}[] PROGMEM = {cpp_string_escape(s)};" + ) + + entries = ", ".join(var_names) + lines.append(f"static const char *const {table_var}[] PROGMEM = {{{entries}}};") + lines.append(f"const LogString *{lookup_fn}(uint16_t index) {{") + lines.append(f' if (index == 0 || index > {count}) return LOG_STR("");') + lines.append(" return reinterpret_cast(") + lines.append(f" progmem_read_ptr(&{table_var}[index - 1]));") + lines.append("}") + return "\n".join(lines) + "\n" + + +@coroutine_with_priority(CoroPriority.FINAL) +async def _generate_component_source_table() -> None: + """Generate the component source lookup table as a FINAL-priority job. + + Runs after all component to_code() calls have registered their sources. + """ + pool = _get_source_pool() + code = _generate_source_table_code( + "COMP_SRC_TABLE", "component_source_lookup", pool.sources + ) + if code: + add_global( + RawStatement(f"namespace esphome {{\n{code}}} // namespace esphome") + ) + async def gpio_pin_expression(conf): """Generate an expression for the given pin option. @@ -77,7 +191,8 @@ async def register_component(var, config): "Error while finding name of component, please report this", exc_info=e ) if name is not None: - add(var.set_component_source(LogStringLiteral(name))) + idx = register_component_source(name) + add(var.set_component_source_(idx)) add(App.register_component_(var)) From 33bb87b813155f765b0088cfe6cdcd066dd06b0a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 21:16:18 -1000 Subject: [PATCH 2/6] Simplify to uint8_t index, add friend declarations, fix tests - Drop 9-bit index scheme in favor of plain uint8_t (max 255 unique source names, overflow warns in Python and returns 0) - Add friend declarations for setup()/original_setup() so generated code can call protected set_component_source_() - Move get_component_log_str() out of line - Fix test mocks to provide CORE.data dict --- esphome/core/component.cpp | 6 ++++- esphome/core/component.h | 37 +++++++++------------------- esphome/cpp_helpers.py | 13 ++++++---- tests/unit_tests/test_cpp_helpers.py | 4 +-- 4 files changed, 26 insertions(+), 34 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index d3e9389efc..44194a18a8 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -274,6 +274,10 @@ void Component::call() { break; } } +const LogString *Component::get_component_log_str() const { + return this->component_source_index_ == 0 ? LOG_STR("") + : component_source_lookup(this->component_source_index_); +} bool Component::should_warn_of_blocking(uint32_t blocking_time) { // Convert centisecond threshold to milliseconds for comparison uint32_t threshold_ms = static_cast(this->warn_if_blocking_over_) * 10U; @@ -544,6 +548,6 @@ void clear_setup_priority_overrides() { #endif // Weak default for component_source_lookup - overridden by generated code -__attribute__((weak)) const LogString *component_source_lookup(uint16_t) { return LOG_STR(""); } +__attribute__((weak)) const LogString *component_source_lookup(uint8_t) { return LOG_STR(""); } } // namespace esphome diff --git a/esphome/core/component.h b/esphome/core/component.h index 6b69190d07..81824d9b8f 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -11,6 +11,10 @@ #include "esphome/core/log.h" #include "esphome/core/optional.h" +// Forward declarations for friend access from codegen-generated setup() +void setup(); // NOLINT(readability-redundant-declaration) - may be declared in Arduino.h +void original_setup(); // NOLINT(readability-redundant-declaration) + namespace esphome { // Forward declaration for LogString @@ -79,11 +83,6 @@ inline constexpr uint8_t STATUS_LED_WARNING = 0x08; inline constexpr uint8_t STATUS_LED_ERROR = 0x10; // Component loop override flag uses bit 5 (set at registration time) inline constexpr uint8_t COMPONENT_HAS_LOOP = 0x20; -// High bit of 9-bit component_source index (bit 6 of component_state_) -inline constexpr uint8_t COMPONENT_SOURCE_HIGH_BIT = 0x40; -// Mask for the 9th bit of the component source index (bit 8) -inline constexpr uint16_t COMPONENT_SOURCE_INDEX_HIGH = 0x100; - // Remove before 2026.8.0 enum class RetryResult { DONE, RETRY }; @@ -91,7 +90,7 @@ inline constexpr uint8_t WARN_IF_BLOCKING_OVER_CS = 5U; // 50ms in centiseconds /// Lookup component source name by index (1-based). Generated by Python codegen. /// Weak default returns "" so builds without codegen still link. -const LogString *component_source_lookup(uint16_t index); +const LogString *component_source_lookup(uint8_t index); class Component { public: @@ -151,10 +150,7 @@ class Component { */ virtual void on_powerdown() {} - uint8_t get_component_state() const { - // Mask out COMPONENT_SOURCE_HIGH_BIT — it's internal to component source indexing - return this->component_state_ & ~COMPONENT_SOURCE_HIGH_BIT; - } + uint8_t get_component_state() const { return this->component_state_; } /** Reset this component back to the construction state to allow setup to run again. * @@ -300,31 +296,21 @@ class Component { * * Returns LOG_STR("") if source not set */ - const LogString *get_component_log_str() const { - uint16_t idx = this->component_source_index_; - if (this->component_state_ & COMPONENT_SOURCE_HIGH_BIT) - idx |= COMPONENT_SOURCE_INDEX_HIGH; - return idx == 0 ? LOG_STR("") : component_source_lookup(idx); - } + const LogString *get_component_log_str() const; bool should_warn_of_blocking(uint32_t blocking_time); protected: friend class Application; + friend void ::setup(); + friend void ::original_setup(); /** Set where this component was loaded from for some debug messages. * * This is set by the ESPHome core during setup, and should not be called manually. * @param index 1-based index into the component source lookup table (0 = not set) */ - void set_component_source_(uint16_t index) { - this->component_source_index_ = static_cast(index & 0xFF); - if (index & COMPONENT_SOURCE_INDEX_HIGH) { - this->component_state_ |= COMPONENT_SOURCE_HIGH_BIT; - } else { - this->component_state_ &= ~COMPONENT_SOURCE_HIGH_BIT; - } - } + void set_component_source_(uint8_t index) { this->component_source_index_ = index; } virtual void call_setup(); void call_dump_config_(); @@ -550,8 +536,7 @@ class Component { /// Bit 3: STATUS_LED_WARNING /// Bit 4: STATUS_LED_ERROR /// Bit 5: Has overridden loop() (set at registration time) - /// Bit 6: High bit of 9-bit component_source index - /// Bit 7: Unused - reserved for future expansion + /// Bits 6-7: Unused - reserved for future expansion uint8_t component_state_{0x00}; volatile bool pending_enable_loop_{false}; ///< ISR-safe flag for enable_loop_soon_any_context }; diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index 22d26ef3ba..a688dbdb20 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -26,8 +26,8 @@ _LOGGER = logging.getLogger(__name__) _COMPONENT_SOURCE_DOMAIN = "component_source_pool" -# Maximum unique component source names (9-bit index, 0 = not set) -_MAX_COMPONENT_SOURCES = 510 +# Maximum unique component source names (8-bit index, 0 = not set) +_MAX_COMPONENT_SOURCES = 0xFF # 255 @dataclass @@ -71,9 +71,12 @@ def register_component_source(name: str) -> int: return pool.sources[name] idx = len(pool.sources) + 1 if idx > _MAX_COMPONENT_SOURCES: - raise ValueError( - f"Too many unique component source names (max {_MAX_COMPONENT_SOURCES}), got {idx}: '{name}'" + _LOGGER.warning( + "Too many unique component source names (max %d), '%s' will show as ''", + _MAX_COMPONENT_SOURCES, + name, ) + return 0 pool.sources[name] = idx _ensure_source_table_registered() return idx @@ -107,7 +110,7 @@ def _generate_source_table_code( entries = ", ".join(var_names) lines.append(f"static const char *const {table_var}[] PROGMEM = {{{entries}}};") - lines.append(f"const LogString *{lookup_fn}(uint16_t index) {{") + lines.append(f"const LogString *{lookup_fn}(uint8_t index) {{") lines.append(f' if (index == 0 || index > {count}) return LOG_STR("");') lines.append(" return reinterpret_cast(") lines.append(f" progmem_read_ptr(&{table_var}[index - 1]));") diff --git a/tests/unit_tests/test_cpp_helpers.py b/tests/unit_tests/test_cpp_helpers.py index 5b6eed156f..b2b0d14a3d 100644 --- a/tests/unit_tests/test_cpp_helpers.py +++ b/tests/unit_tests/test_cpp_helpers.py @@ -23,7 +23,7 @@ async def test_register_component(monkeypatch): app_mock = Mock(register_component_=Mock(return_value=var)) monkeypatch.setattr(ch, "App", app_mock) - core_mock = Mock(component_ids=["foo.bar"]) + core_mock = Mock(component_ids=["foo.bar"], data={}) monkeypatch.setattr(ch, "CORE", core_mock) add_mock = Mock() @@ -59,7 +59,7 @@ async def test_register_component__with_setup_priority(monkeypatch): app_mock = Mock(register_component_=Mock(return_value=var)) monkeypatch.setattr(ch, "App", app_mock) - core_mock = Mock(component_ids=["foo.bar"]) + core_mock = Mock(component_ids=["foo.bar"], data={}) monkeypatch.setattr(ch, "CORE", core_mock) add_mock = Mock() From a92281f899e57b80f9aee5e4bea473c051940a0e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 21:24:01 -1000 Subject: [PATCH 3/6] Add tests for register_component_source, remove redundant index check - Test empty name returns 0 - Test deduplication returns same index - Test overflow warns and returns 0 - Remove duplicate index==0 check in get_component_log_str() since component_source_lookup() already handles it --- esphome/core/component.cpp | 3 +-- tests/unit_tests/test_cpp_helpers.py | 31 ++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 44194a18a8..c81e357808 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -275,8 +275,7 @@ void Component::call() { } } const LogString *Component::get_component_log_str() const { - return this->component_source_index_ == 0 ? LOG_STR("") - : component_source_lookup(this->component_source_index_); + return component_source_lookup(this->component_source_index_); } bool Component::should_warn_of_blocking(uint32_t blocking_time) { // Convert centisecond threshold to milliseconds for comparison diff --git a/tests/unit_tests/test_cpp_helpers.py b/tests/unit_tests/test_cpp_helpers.py index b2b0d14a3d..75b96bb9e0 100644 --- a/tests/unit_tests/test_cpp_helpers.py +++ b/tests/unit_tests/test_cpp_helpers.py @@ -1,8 +1,10 @@ +import logging from unittest.mock import Mock import pytest from esphome import const, cpp_helpers as ch +from esphome.cpp_helpers import ComponentSourcePool, register_component_source @pytest.mark.asyncio @@ -78,3 +80,32 @@ async def test_register_component__with_setup_priority(monkeypatch): assert add_mock.call_count == 4 app_mock.register_component_.assert_called_with(var) assert core_mock.component_ids == [] + + +def test_register_component_source_empty_name(monkeypatch): + monkeypatch.setattr(ch, "CORE", Mock(data={})) + assert register_component_source("") == 0 + + +def test_register_component_source_deduplicates(monkeypatch): + monkeypatch.setattr(ch, "CORE", Mock(data={})) + idx1 = register_component_source("wifi") + idx2 = register_component_source("api") + idx3 = register_component_source("wifi") + assert idx1 == 1 + assert idx2 == 2 + assert idx3 == 1 # deduplicated + + +def test_register_component_source_overflow_warns(monkeypatch, caplog): + # Pre-fill pool to max + pool = ComponentSourcePool( + sources={f"comp_{i}": i + 1 for i in range(0xFF)}, + table_registered=True, + ) + monkeypatch.setattr(ch, "CORE", Mock(data={ch._COMPONENT_SOURCE_DOMAIN: pool})) + with caplog.at_level(logging.WARNING): + idx = register_component_source("overflow_component") + assert idx == 0 + assert "Too many unique component source names" in caplog.text + assert "overflow_component" in caplog.text From ebb11206e35e7da2ec3d183320dfd9642dcddcf9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 21:25:24 -1000 Subject: [PATCH 4/6] Fix stale comment on component_source_index_ field --- esphome/core/component.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/core/component.h b/esphome/core/component.h index 81824d9b8f..86c25cf09c 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -529,7 +529,7 @@ class Component { void status_clear_error_slow_path_(); // Ordered for optimal packing on 32-bit systems (8 bytes total with vtable) - uint8_t component_source_index_{0}; ///< Lower 8 bits of 9-bit component source index + uint8_t component_source_index_{0}; ///< Index into component source PROGMEM lookup table (0 = not set) uint8_t warn_if_blocking_over_{WARN_IF_BLOCKING_OVER_CS}; ///< Warn threshold in centiseconds (max 2550ms) /// State of this component - each bit has a purpose: /// Bits 0-2: Component state (0x00=CONSTRUCTION, 0x01=SETUP, 0x02=LOOP, 0x03=FAILED, 0x04=LOOP_DONE) From 099bea00f69837b42c0947ef4bb4f6be2508d7c4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 21:30:22 -1000 Subject: [PATCH 5/6] Add test for _generate_source_table_code empty input --- tests/unit_tests/test_cpp_helpers.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/unit_tests/test_cpp_helpers.py b/tests/unit_tests/test_cpp_helpers.py index 75b96bb9e0..efb3ca5e07 100644 --- a/tests/unit_tests/test_cpp_helpers.py +++ b/tests/unit_tests/test_cpp_helpers.py @@ -97,6 +97,12 @@ def test_register_component_source_deduplicates(monkeypatch): assert idx3 == 1 # deduplicated +def test_generate_source_table_code_empty(): + from esphome.cpp_helpers import _generate_source_table_code + + assert _generate_source_table_code("TBL", "lookup", {}) == "" + + def test_register_component_source_overflow_warns(monkeypatch, caplog): # Pre-fill pool to max pool = ComponentSourcePool( From 010d7699f28d4f4c13d348559f8ff6e4ff716f3a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 21:30:42 -1000 Subject: [PATCH 6/6] Use walrus operator in _generate_component_source_table --- esphome/cpp_helpers.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index a688dbdb20..e7ff2965c8 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -125,10 +125,9 @@ async def _generate_component_source_table() -> None: Runs after all component to_code() calls have registered their sources. """ pool = _get_source_pool() - code = _generate_source_table_code( + if code := _generate_source_table_code( "COMP_SRC_TABLE", "component_source_lookup", pool.sources - ) - if code: + ): add_global( RawStatement(f"namespace esphome {{\n{code}}} // namespace esphome") )