From 0073d37bf93322df2ab5550fa0322384aaa20f39 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Mar 2026 20:36:55 -1000 Subject: [PATCH] [light] Replace initial_state storage with flash-resident callback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the inline optional (~44 bytes) with a function pointer callback (4 bytes) that populates the initial state during setup. The callback is a stateless lambda whose values live in flash as code, not in the LightState object. Saves ~40 bytes per LightState instance unconditionally — the optional reserved space for the struct even when initial_state was not configured (the common case). --- esphome/components/light/__init__.py | 17 ++++++++++++++--- esphome/components/light/light_state.cpp | 7 ++++--- esphome/components/light/light_state.h | 10 ++++++---- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py index 64452e4282..739f695908 100644 --- a/esphome/components/light/__init__.py +++ b/esphome/components/light/__init__.py @@ -38,7 +38,7 @@ from esphome.const import ( CONF_WEB_SERVER, CONF_WHITE, ) -from esphome.core import CORE, ID, CoroPriority, HexInt, coroutine_with_priority +from esphome.core import CORE, ID, CoroPriority, HexInt, Lambda, coroutine_with_priority from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass @@ -52,6 +52,7 @@ from .effects import ( validate_effects, ) from .types import ( # noqa + COLOR_MODES, AddressableLight, AddressableLightState, ColorMode, @@ -262,8 +263,12 @@ async def setup_light_core_(light_var, config, output_var): cg.add(light_var.set_restore_mode(config[CONF_RESTORE_MODE])) if (initial_state_config := config.get(CONF_INITIAL_STATE)) is not None: + # Emit a stateless lambda that constructs the initial state — values live + # in flash as code, not stored in the LightState object (~40 bytes saved). + color_mode_str = initial_state_config.get(CONF_COLOR_MODE) + color_mode_expr = COLOR_MODES.get(color_mode_str, ColorMode.UNKNOWN) initial_state = LightStateRTCState( - initial_state_config.get(CONF_COLOR_MODE, ColorMode.UNKNOWN), + color_mode_expr, initial_state_config.get(CONF_STATE, False), initial_state_config.get(CONF_BRIGHTNESS, 1.0), initial_state_config.get(CONF_COLOR_BRIGHTNESS, 1.0), @@ -275,7 +280,13 @@ async def setup_light_core_(light_var, config, output_var): initial_state_config.get(CONF_COLD_WHITE, 1.0), initial_state_config.get(CONF_WARM_WHITE, 1.0), ) - cg.add(light_var.set_initial_state(initial_state)) + args = [(LightStateRTCState.operator("ref"), "s")] + lamb = await cg.process_lambda( + Lambda(f"s = {initial_state};"), + args, + return_type=cg.void, + ) + cg.add(light_var.set_initial_state(lamb)) if ( default_transition_length := config.get(CONF_DEFAULT_TRANSITION_LENGTH) diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 1b736d84f6..bd778926d5 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -37,8 +37,9 @@ void LightState::setup() { auto call = this->make_call(); LightStateRTCState recovered{}; - if (this->initial_state_.has_value()) { - recovered = *this->initial_state_; + if (this->initial_state_callback_) { + this->initial_state_callback_(recovered); + this->initial_state_callback_ = nullptr; // One-shot — no longer needed } switch (this->restore_mode_) { case LIGHT_RESTORE_DEFAULT_OFF: @@ -195,7 +196,7 @@ void LightState::set_flash_transition_length(uint32_t flash_transition_length) { uint32_t LightState::get_flash_transition_length() const { return this->flash_transition_length_; } void LightState::set_gamma_correct(float gamma_correct) { this->gamma_correct_ = gamma_correct; } void LightState::set_restore_mode(LightRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } -void LightState::set_initial_state(const LightStateRTCState &initial_state) { this->initial_state_ = initial_state; } +void LightState::set_initial_state(void (*callback)(LightStateRTCState &)) { this->initial_state_callback_ = callback; } bool LightState::supports_effects() { return !this->effects_.empty(); } const FixedVector &LightState::get_effects() const { return this->effects_; } void LightState::add_effects(const std::initializer_list &effects) { diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index ab7f2e4df8..5efc05358b 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -188,8 +188,9 @@ class LightState : public EntityBase, public Component { /// Set the restore mode of this light void set_restore_mode(LightRestoreMode restore_mode); - /// Set the initial state of this light - void set_initial_state(const LightStateRTCState &initial_state); + /// Set a callback to populate the initial state defaults during setup. + /// The callback is called once, then cleared. Values live in flash as code. + void set_initial_state(void (*callback)(LightStateRTCState &)); /// Return whether the light has any effects that meet the trait requirements. bool supports_effects(); @@ -342,8 +343,9 @@ class LightState : public EntityBase, public Component { */ std::unique_ptr> target_state_reached_listeners_; - /// Initial state of the light. - optional initial_state_{}; + /// Callback to populate initial state defaults — called once during setup, then cleared. + /// Values live in flash as function body; no per-instance data storage beyond this pointer. + void (*initial_state_callback_)(LightStateRTCState &){nullptr}; /// Value for storing the index of the currently active effect. 0 if no effect is active uint32_t active_effect_index_{};