From 19df4527713e32b02aaf56d19d3e4e1a4c7e5032 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 26 Apr 2026 21:33:57 -0500 Subject: [PATCH 1/5] [light] Use bitmask template parameter for LightControlAction unused fields Parameterize LightControlAction on a uint16_t Fields bitmask encoding which of its 14 templatable fields are configured. Unset fields are elided from the instance via [[no_unique_address]] and skipped at compile time in play() via if constexpr. Real-world configs typically use only 1-5 of the 14 fields. Codegen computes the bitmask from the YAML and passes it as the leading template argument, so each unique field combination produces its own type with only the storage and play() branches it actually needs. Measured on apollo-pump-1-5d9bdc.yaml (7 instances, 3 unique masks): - state-only (2 instances): 72 B -> 20 B = 52 B saved each - state+RGB (2 instances): 72 B -> 32 B = 40 B saved each - state+brightness+RGB (3 instances): 72 B -> 36 B = 36 B saved each - Total: ~292 B RAM saved - Flash cost: ~30-70 B for 3 play() variants + vtables --- esphome/components/light/automation.h | 111 ++++++++++++++++++------- esphome/components/light/automation.py | 43 ++++++---- 2 files changed, 109 insertions(+), 45 deletions(-) diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index f6a2ca52d45..2511612b57d 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -24,60 +24,113 @@ template class ToggleAction : public Action { LightState *state_; }; -template class LightControlAction : public Action { +// Bitmask of fields configured on a LightControlAction. Used as a non-type +// template parameter so unset fields can be elided via [[no_unique_address]] +// and skipped at compile time via if constexpr in play(). +namespace LightControlField { +constexpr uint16_t COLOR_MODE = 1 << 0; +constexpr uint16_t STATE = 1 << 1; +constexpr uint16_t TRANSITION_LENGTH = 1 << 2; +constexpr uint16_t FLASH_LENGTH = 1 << 3; +constexpr uint16_t BRIGHTNESS = 1 << 4; +constexpr uint16_t COLOR_BRIGHTNESS = 1 << 5; +constexpr uint16_t RED = 1 << 6; +constexpr uint16_t GREEN = 1 << 7; +constexpr uint16_t BLUE = 1 << 8; +constexpr uint16_t WHITE = 1 << 9; +constexpr uint16_t COLOR_TEMPERATURE = 1 << 10; +constexpr uint16_t COLD_WHITE = 1 << 11; +constexpr uint16_t WARM_WHITE = 1 << 12; +constexpr uint16_t EFFECT = 1 << 13; +} // namespace LightControlField + +namespace light_control_detail { +// Distinct empty types per field so [[no_unique_address]] always coalesces +// (same empty type used multiple times is not guaranteed to share an address). +template struct Empty {}; +} // namespace light_control_detail + +template class LightControlAction : public Action { public: explicit LightControlAction(LightState *parent) : parent_(parent) {} - TEMPLATABLE_VALUE(ColorMode, color_mode) - TEMPLATABLE_VALUE(bool, state) - TEMPLATABLE_VALUE(uint32_t, transition_length) - TEMPLATABLE_VALUE(uint32_t, flash_length) - TEMPLATABLE_VALUE(float, brightness) - TEMPLATABLE_VALUE(float, color_brightness) - TEMPLATABLE_VALUE(float, red) - TEMPLATABLE_VALUE(float, green) - TEMPLATABLE_VALUE(float, blue) - TEMPLATABLE_VALUE(float, white) - TEMPLATABLE_VALUE(float, color_temperature) - TEMPLATABLE_VALUE(float, cold_white) - TEMPLATABLE_VALUE(float, warm_white) - TEMPLATABLE_VALUE(uint32_t, effect) +#define LIGHT_CONTROL_FIELD(field_bit, type, name, tag) \ + template void set_##name(V value) requires((Fields & LightControlField::field_bit) != 0) { \ + this->name##_ = value; \ + } + + LIGHT_CONTROL_FIELD(COLOR_MODE, ColorMode, color_mode, 0) + LIGHT_CONTROL_FIELD(STATE, bool, state, 1) + LIGHT_CONTROL_FIELD(TRANSITION_LENGTH, uint32_t, transition_length, 2) + LIGHT_CONTROL_FIELD(FLASH_LENGTH, uint32_t, flash_length, 3) + LIGHT_CONTROL_FIELD(BRIGHTNESS, float, brightness, 4) + LIGHT_CONTROL_FIELD(COLOR_BRIGHTNESS, float, color_brightness, 5) + LIGHT_CONTROL_FIELD(RED, float, red, 6) + LIGHT_CONTROL_FIELD(GREEN, float, green, 7) + LIGHT_CONTROL_FIELD(BLUE, float, blue, 8) + LIGHT_CONTROL_FIELD(WHITE, float, white, 9) + LIGHT_CONTROL_FIELD(COLOR_TEMPERATURE, float, color_temperature, 10) + LIGHT_CONTROL_FIELD(COLD_WHITE, float, cold_white, 11) + LIGHT_CONTROL_FIELD(WARM_WHITE, float, warm_white, 12) + LIGHT_CONTROL_FIELD(EFFECT, uint32_t, effect, 13) +#undef LIGHT_CONTROL_FIELD void play(const Ts &...x) override { auto call = this->parent_->make_call(); - if (this->color_mode_.has_value()) + if constexpr ((Fields & LightControlField::COLOR_MODE) != 0) call.set_color_mode(this->color_mode_.value(x...)); - if (this->state_.has_value()) + if constexpr ((Fields & LightControlField::STATE) != 0) call.set_state(this->state_.value(x...)); - if (this->transition_length_.has_value()) + if constexpr ((Fields & LightControlField::TRANSITION_LENGTH) != 0) call.set_transition_length(this->transition_length_.value(x...)); - if (this->flash_length_.has_value()) + if constexpr ((Fields & LightControlField::FLASH_LENGTH) != 0) call.set_flash_length(this->flash_length_.value(x...)); - if (this->brightness_.has_value()) + if constexpr ((Fields & LightControlField::BRIGHTNESS) != 0) call.set_brightness(this->brightness_.value(x...)); - if (this->color_brightness_.has_value()) + if constexpr ((Fields & LightControlField::COLOR_BRIGHTNESS) != 0) call.set_color_brightness(this->color_brightness_.value(x...)); - if (this->red_.has_value()) + if constexpr ((Fields & LightControlField::RED) != 0) call.set_red(this->red_.value(x...)); - if (this->green_.has_value()) + if constexpr ((Fields & LightControlField::GREEN) != 0) call.set_green(this->green_.value(x...)); - if (this->blue_.has_value()) + if constexpr ((Fields & LightControlField::BLUE) != 0) call.set_blue(this->blue_.value(x...)); - if (this->white_.has_value()) + if constexpr ((Fields & LightControlField::WHITE) != 0) call.set_white(this->white_.value(x...)); - if (this->color_temperature_.has_value()) + if constexpr ((Fields & LightControlField::COLOR_TEMPERATURE) != 0) call.set_color_temperature(this->color_temperature_.value(x...)); - if (this->cold_white_.has_value()) + if constexpr ((Fields & LightControlField::COLD_WHITE) != 0) call.set_cold_white(this->cold_white_.value(x...)); - if (this->warm_white_.has_value()) + if constexpr ((Fields & LightControlField::WARM_WHITE) != 0) call.set_warm_white(this->warm_white_.value(x...)); - if (this->effect_.has_value()) + if constexpr ((Fields & LightControlField::EFFECT) != 0) call.set_effect(this->effect_.value(x...)); call.perform(); } protected: LightState *parent_; + +#define LIGHT_CONTROL_STORAGE(field_bit, type, name, tag) \ + [[no_unique_address]] std::conditional_t<(Fields & LightControlField::field_bit) != 0, TemplatableFn, \ + light_control_detail::Empty> \ + name##_{}; + + LIGHT_CONTROL_STORAGE(COLOR_MODE, ColorMode, color_mode, 0) + LIGHT_CONTROL_STORAGE(STATE, bool, state, 1) + LIGHT_CONTROL_STORAGE(TRANSITION_LENGTH, uint32_t, transition_length, 2) + LIGHT_CONTROL_STORAGE(FLASH_LENGTH, uint32_t, flash_length, 3) + LIGHT_CONTROL_STORAGE(BRIGHTNESS, float, brightness, 4) + LIGHT_CONTROL_STORAGE(COLOR_BRIGHTNESS, float, color_brightness, 5) + LIGHT_CONTROL_STORAGE(RED, float, red, 6) + LIGHT_CONTROL_STORAGE(GREEN, float, green, 7) + LIGHT_CONTROL_STORAGE(BLUE, float, blue, 8) + LIGHT_CONTROL_STORAGE(WHITE, float, white, 9) + LIGHT_CONTROL_STORAGE(COLOR_TEMPERATURE, float, color_temperature, 10) + LIGHT_CONTROL_STORAGE(COLD_WHITE, float, cold_white, 11) + LIGHT_CONTROL_STORAGE(WARM_WHITE, float, warm_white, 12) + LIGHT_CONTROL_STORAGE(EFFECT, uint32_t, effect, 13) +#undef LIGHT_CONTROL_STORAGE }; template class DimRelativeAction : public Action { diff --git a/esphome/components/light/automation.py b/esphome/components/light/automation.py index 46d37239e5e..ddd8948a649 100644 --- a/esphome/components/light/automation.py +++ b/esphome/components/light/automation.py @@ -178,25 +178,36 @@ def _resolve_effect_index(config: ConfigType) -> int: ) async def light_control_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) - # (config_key, setter_name, c++ type) + # (config_key, setter_name, c++ type, field bit) + # Bit values must match LightControlField in automation.h FIELDS = ( - (CONF_COLOR_MODE, "set_color_mode", ColorMode), - (CONF_STATE, "set_state", cg.bool_), - (CONF_TRANSITION_LENGTH, "set_transition_length", cg.uint32), - (CONF_FLASH_LENGTH, "set_flash_length", cg.uint32), - (CONF_BRIGHTNESS, "set_brightness", cg.float_), - (CONF_COLOR_BRIGHTNESS, "set_color_brightness", cg.float_), - (CONF_RED, "set_red", cg.float_), - (CONF_GREEN, "set_green", cg.float_), - (CONF_BLUE, "set_blue", cg.float_), - (CONF_WHITE, "set_white", cg.float_), - (CONF_COLOR_TEMPERATURE, "set_color_temperature", cg.float_), - (CONF_COLD_WHITE, "set_cold_white", cg.float_), - (CONF_WARM_WHITE, "set_warm_white", cg.float_), + (CONF_COLOR_MODE, "set_color_mode", ColorMode, 1 << 0), + (CONF_STATE, "set_state", cg.bool_, 1 << 1), + (CONF_TRANSITION_LENGTH, "set_transition_length", cg.uint32, 1 << 2), + (CONF_FLASH_LENGTH, "set_flash_length", cg.uint32, 1 << 3), + (CONF_BRIGHTNESS, "set_brightness", cg.float_, 1 << 4), + (CONF_COLOR_BRIGHTNESS, "set_color_brightness", cg.float_, 1 << 5), + (CONF_RED, "set_red", cg.float_, 1 << 6), + (CONF_GREEN, "set_green", cg.float_, 1 << 7), + (CONF_BLUE, "set_blue", cg.float_, 1 << 8), + (CONF_WHITE, "set_white", cg.float_, 1 << 9), + (CONF_COLOR_TEMPERATURE, "set_color_temperature", cg.float_, 1 << 10), + (CONF_COLD_WHITE, "set_cold_white", cg.float_, 1 << 11), + (CONF_WARM_WHITE, "set_warm_white", cg.float_, 1 << 12), ) - for conf_key, setter, type_ in FIELDS: + field_mask = 0 + for conf_key, _, _, bit in FIELDS: + if conf_key in config: + field_mask |= bit + if CONF_EFFECT in config: + field_mask |= 1 << 13 # EFFECT bit + + fields_arg = cg.RawExpression(f"static_cast({field_mask})") + control_template_arg = cg.TemplateArguments(fields_arg, *template_arg) + var = cg.new_Pvariable(action_id, control_template_arg, paren) + + for conf_key, setter, type_, _ in FIELDS: if conf_key in config: template_ = await cg.templatable(config[conf_key], args, type_) cg.add(getattr(var, setter)(template_)) From 3f0c669b4cedc7377d447586123bfe91d2c9f686 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 26 Apr 2026 21:37:55 -0500 Subject: [PATCH 2/5] [light] Reduce LOC via X-macro pattern, drop unused tag/BIT args MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply the X-macro pattern from #15132 so the field list is declared once and expanded into setters, play(), and storage. Drop the unused BIT_NAME and tag arguments — only (type, name, idx) is needed since idx doubles as both the bit position and the Empty<> tag. Net change vs upstream is +10 LOC instead of +64. --- esphome/components/light/automation.h | 124 +++++++------------------ esphome/components/light/automation.py | 48 +++++----- 2 files changed, 59 insertions(+), 113 deletions(-) diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index 2511612b57d..2dc8c08ee05 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -24,113 +24,59 @@ template class ToggleAction : public Action { LightState *state_; }; -// Bitmask of fields configured on a LightControlAction. Used as a non-type -// template parameter so unset fields can be elided via [[no_unique_address]] -// and skipped at compile time via if constexpr in play(). -namespace LightControlField { -constexpr uint16_t COLOR_MODE = 1 << 0; -constexpr uint16_t STATE = 1 << 1; -constexpr uint16_t TRANSITION_LENGTH = 1 << 2; -constexpr uint16_t FLASH_LENGTH = 1 << 3; -constexpr uint16_t BRIGHTNESS = 1 << 4; -constexpr uint16_t COLOR_BRIGHTNESS = 1 << 5; -constexpr uint16_t RED = 1 << 6; -constexpr uint16_t GREEN = 1 << 7; -constexpr uint16_t BLUE = 1 << 8; -constexpr uint16_t WHITE = 1 << 9; -constexpr uint16_t COLOR_TEMPERATURE = 1 << 10; -constexpr uint16_t COLD_WHITE = 1 << 11; -constexpr uint16_t WARM_WHITE = 1 << 12; -constexpr uint16_t EFFECT = 1 << 13; -} // namespace LightControlField - +// Distinct empty types per field tag so [[no_unique_address]] always coalesces +// (the same empty type used multiple times is not guaranteed to share an address). namespace light_control_detail { -// Distinct empty types per field so [[no_unique_address]] always coalesces -// (same empty type used multiple times is not guaranteed to share an address). template struct Empty {}; } // namespace light_control_detail +// X-macro: (type, field_name, bit_index). Order and bit values must match +// the FIELDS table in automation.py. +#define LIGHT_CONTROL_FIELDS(X) \ + X(ColorMode, color_mode, 0) \ + X(bool, state, 1) \ + X(uint32_t, transition_length, 2) \ + X(uint32_t, flash_length, 3) \ + X(float, brightness, 4) \ + X(float, color_brightness, 5) \ + X(float, red, 6) \ + X(float, green, 7) \ + X(float, blue, 8) \ + X(float, white, 9) \ + X(float, color_temperature, 10) \ + X(float, cold_white, 11) \ + X(float, warm_white, 12) \ + X(uint32_t, effect, 13) + template class LightControlAction : public Action { public: explicit LightControlAction(LightState *parent) : parent_(parent) {} -#define LIGHT_CONTROL_FIELD(field_bit, type, name, tag) \ - template void set_##name(V value) requires((Fields & LightControlField::field_bit) != 0) { \ - this->name##_ = value; \ - } +#define LIGHT_FIELD_SETTER_(type, name, idx) \ + template void set_##name(V value) requires((Fields & (1 << (idx))) != 0) { this->name##_ = value; } +#define LIGHT_FIELD_APPLY_(type, name, idx) \ + if constexpr ((Fields & (1 << (idx))) != 0) \ + call.set_##name(this->name##_.value(x...)); +#define LIGHT_FIELD_DECL_(type, name, idx) \ + [[no_unique_address]] std::conditional_t<(Fields & (1 << (idx))) != 0, TemplatableFn, \ + light_control_detail::Empty<(idx)>> \ + name##_{}; - LIGHT_CONTROL_FIELD(COLOR_MODE, ColorMode, color_mode, 0) - LIGHT_CONTROL_FIELD(STATE, bool, state, 1) - LIGHT_CONTROL_FIELD(TRANSITION_LENGTH, uint32_t, transition_length, 2) - LIGHT_CONTROL_FIELD(FLASH_LENGTH, uint32_t, flash_length, 3) - LIGHT_CONTROL_FIELD(BRIGHTNESS, float, brightness, 4) - LIGHT_CONTROL_FIELD(COLOR_BRIGHTNESS, float, color_brightness, 5) - LIGHT_CONTROL_FIELD(RED, float, red, 6) - LIGHT_CONTROL_FIELD(GREEN, float, green, 7) - LIGHT_CONTROL_FIELD(BLUE, float, blue, 8) - LIGHT_CONTROL_FIELD(WHITE, float, white, 9) - LIGHT_CONTROL_FIELD(COLOR_TEMPERATURE, float, color_temperature, 10) - LIGHT_CONTROL_FIELD(COLD_WHITE, float, cold_white, 11) - LIGHT_CONTROL_FIELD(WARM_WHITE, float, warm_white, 12) - LIGHT_CONTROL_FIELD(EFFECT, uint32_t, effect, 13) -#undef LIGHT_CONTROL_FIELD + LIGHT_CONTROL_FIELDS(LIGHT_FIELD_SETTER_) void play(const Ts &...x) override { auto call = this->parent_->make_call(); - if constexpr ((Fields & LightControlField::COLOR_MODE) != 0) - call.set_color_mode(this->color_mode_.value(x...)); - if constexpr ((Fields & LightControlField::STATE) != 0) - call.set_state(this->state_.value(x...)); - if constexpr ((Fields & LightControlField::TRANSITION_LENGTH) != 0) - call.set_transition_length(this->transition_length_.value(x...)); - if constexpr ((Fields & LightControlField::FLASH_LENGTH) != 0) - call.set_flash_length(this->flash_length_.value(x...)); - if constexpr ((Fields & LightControlField::BRIGHTNESS) != 0) - call.set_brightness(this->brightness_.value(x...)); - if constexpr ((Fields & LightControlField::COLOR_BRIGHTNESS) != 0) - call.set_color_brightness(this->color_brightness_.value(x...)); - if constexpr ((Fields & LightControlField::RED) != 0) - call.set_red(this->red_.value(x...)); - if constexpr ((Fields & LightControlField::GREEN) != 0) - call.set_green(this->green_.value(x...)); - if constexpr ((Fields & LightControlField::BLUE) != 0) - call.set_blue(this->blue_.value(x...)); - if constexpr ((Fields & LightControlField::WHITE) != 0) - call.set_white(this->white_.value(x...)); - if constexpr ((Fields & LightControlField::COLOR_TEMPERATURE) != 0) - call.set_color_temperature(this->color_temperature_.value(x...)); - if constexpr ((Fields & LightControlField::COLD_WHITE) != 0) - call.set_cold_white(this->cold_white_.value(x...)); - if constexpr ((Fields & LightControlField::WARM_WHITE) != 0) - call.set_warm_white(this->warm_white_.value(x...)); - if constexpr ((Fields & LightControlField::EFFECT) != 0) - call.set_effect(this->effect_.value(x...)); + LIGHT_CONTROL_FIELDS(LIGHT_FIELD_APPLY_) call.perform(); } protected: LightState *parent_; + LIGHT_CONTROL_FIELDS(LIGHT_FIELD_DECL_) -#define LIGHT_CONTROL_STORAGE(field_bit, type, name, tag) \ - [[no_unique_address]] std::conditional_t<(Fields & LightControlField::field_bit) != 0, TemplatableFn, \ - light_control_detail::Empty> \ - name##_{}; - - LIGHT_CONTROL_STORAGE(COLOR_MODE, ColorMode, color_mode, 0) - LIGHT_CONTROL_STORAGE(STATE, bool, state, 1) - LIGHT_CONTROL_STORAGE(TRANSITION_LENGTH, uint32_t, transition_length, 2) - LIGHT_CONTROL_STORAGE(FLASH_LENGTH, uint32_t, flash_length, 3) - LIGHT_CONTROL_STORAGE(BRIGHTNESS, float, brightness, 4) - LIGHT_CONTROL_STORAGE(COLOR_BRIGHTNESS, float, color_brightness, 5) - LIGHT_CONTROL_STORAGE(RED, float, red, 6) - LIGHT_CONTROL_STORAGE(GREEN, float, green, 7) - LIGHT_CONTROL_STORAGE(BLUE, float, blue, 8) - LIGHT_CONTROL_STORAGE(WHITE, float, white, 9) - LIGHT_CONTROL_STORAGE(COLOR_TEMPERATURE, float, color_temperature, 10) - LIGHT_CONTROL_STORAGE(COLD_WHITE, float, cold_white, 11) - LIGHT_CONTROL_STORAGE(WARM_WHITE, float, warm_white, 12) - LIGHT_CONTROL_STORAGE(EFFECT, uint32_t, effect, 13) -#undef LIGHT_CONTROL_STORAGE +#undef LIGHT_FIELD_DECL_ +#undef LIGHT_FIELD_APPLY_ +#undef LIGHT_FIELD_SETTER_ }; template class DimRelativeAction : public Action { diff --git a/esphome/components/light/automation.py b/esphome/components/light/automation.py index ddd8948a649..a655cecc446 100644 --- a/esphome/components/light/automation.py +++ b/esphome/components/light/automation.py @@ -179,35 +179,35 @@ def _resolve_effect_index(config: ConfigType) -> int: async def light_control_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) - # (config_key, setter_name, c++ type, field bit) - # Bit values must match LightControlField in automation.h + # (config_key, setter_name, c++ type) — order and bit position must match + # LIGHT_CONTROL_FIELDS in automation.h. CONF_EFFECT is the last bit. FIELDS = ( - (CONF_COLOR_MODE, "set_color_mode", ColorMode, 1 << 0), - (CONF_STATE, "set_state", cg.bool_, 1 << 1), - (CONF_TRANSITION_LENGTH, "set_transition_length", cg.uint32, 1 << 2), - (CONF_FLASH_LENGTH, "set_flash_length", cg.uint32, 1 << 3), - (CONF_BRIGHTNESS, "set_brightness", cg.float_, 1 << 4), - (CONF_COLOR_BRIGHTNESS, "set_color_brightness", cg.float_, 1 << 5), - (CONF_RED, "set_red", cg.float_, 1 << 6), - (CONF_GREEN, "set_green", cg.float_, 1 << 7), - (CONF_BLUE, "set_blue", cg.float_, 1 << 8), - (CONF_WHITE, "set_white", cg.float_, 1 << 9), - (CONF_COLOR_TEMPERATURE, "set_color_temperature", cg.float_, 1 << 10), - (CONF_COLD_WHITE, "set_cold_white", cg.float_, 1 << 11), - (CONF_WARM_WHITE, "set_warm_white", cg.float_, 1 << 12), + (CONF_COLOR_MODE, "set_color_mode", ColorMode), + (CONF_STATE, "set_state", cg.bool_), + (CONF_TRANSITION_LENGTH, "set_transition_length", cg.uint32), + (CONF_FLASH_LENGTH, "set_flash_length", cg.uint32), + (CONF_BRIGHTNESS, "set_brightness", cg.float_), + (CONF_COLOR_BRIGHTNESS, "set_color_brightness", cg.float_), + (CONF_RED, "set_red", cg.float_), + (CONF_GREEN, "set_green", cg.float_), + (CONF_BLUE, "set_blue", cg.float_), + (CONF_WHITE, "set_white", cg.float_), + (CONF_COLOR_TEMPERATURE, "set_color_temperature", cg.float_), + (CONF_COLD_WHITE, "set_cold_white", cg.float_), + (CONF_WARM_WHITE, "set_warm_white", cg.float_), ) - field_mask = 0 - for conf_key, _, _, bit in FIELDS: - if conf_key in config: - field_mask |= bit - if CONF_EFFECT in config: - field_mask |= 1 << 13 # EFFECT bit + EFFECT_BIT = len(FIELDS) - fields_arg = cg.RawExpression(f"static_cast({field_mask})") - control_template_arg = cg.TemplateArguments(fields_arg, *template_arg) + field_mask = sum(1 << i for i, (k, _, _) in enumerate(FIELDS) if k in config) + if CONF_EFFECT in config: + field_mask |= 1 << EFFECT_BIT + + control_template_arg = cg.TemplateArguments( + cg.RawExpression(f"static_cast({field_mask})"), *template_arg + ) var = cg.new_Pvariable(action_id, control_template_arg, paren) - for conf_key, setter, type_, _ in FIELDS: + for conf_key, setter, type_ in FIELDS: if conf_key in config: template_ = await cg.templatable(config[conf_key], args, type_) cg.add(getattr(var, setter)(template_)) From 0b8c04983ef33b1ec2ed2bc793b1530b1c8cc9c9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 26 Apr 2026 21:40:05 -0500 Subject: [PATCH 3/5] [light] Fold CONF_EFFECT into FIELDS table --- esphome/components/light/automation.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/esphome/components/light/automation.py b/esphome/components/light/automation.py index a655cecc446..04a79f1148a 100644 --- a/esphome/components/light/automation.py +++ b/esphome/components/light/automation.py @@ -180,7 +180,8 @@ async def light_control_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) # (config_key, setter_name, c++ type) — order and bit position must match - # LIGHT_CONTROL_FIELDS in automation.h. CONF_EFFECT is the last bit. + # LIGHT_CONTROL_FIELDS in automation.h. CONF_EFFECT has special-case + # handling below (lambda or static name resolution), so its setter is None. FIELDS = ( (CONF_COLOR_MODE, "set_color_mode", ColorMode), (CONF_STATE, "set_state", cg.bool_), @@ -195,20 +196,17 @@ async def light_control_to_code(config, action_id, template_arg, args): (CONF_COLOR_TEMPERATURE, "set_color_temperature", cg.float_), (CONF_COLD_WHITE, "set_cold_white", cg.float_), (CONF_WARM_WHITE, "set_warm_white", cg.float_), + (CONF_EFFECT, None, cg.uint32), ) - EFFECT_BIT = len(FIELDS) field_mask = sum(1 << i for i, (k, _, _) in enumerate(FIELDS) if k in config) - if CONF_EFFECT in config: - field_mask |= 1 << EFFECT_BIT - control_template_arg = cg.TemplateArguments( cg.RawExpression(f"static_cast({field_mask})"), *template_arg ) var = cg.new_Pvariable(action_id, control_template_arg, paren) for conf_key, setter, type_ in FIELDS: - if conf_key in config: + if conf_key in config and setter is not None: template_ = await cg.templatable(config[conf_key], args, type_) cg.add(getattr(var, setter)(template_)) From 9f776eb517797ae3c61bbc4a67bba08347c688d8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 26 Apr 2026 21:40:52 -0500 Subject: [PATCH 4/5] [light] Shorten FIELDS comment --- esphome/components/light/automation.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/esphome/components/light/automation.py b/esphome/components/light/automation.py index 04a79f1148a..1062d6de3a2 100644 --- a/esphome/components/light/automation.py +++ b/esphome/components/light/automation.py @@ -179,9 +179,8 @@ def _resolve_effect_index(config: ConfigType) -> int: async def light_control_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) - # (config_key, setter_name, c++ type) — order and bit position must match - # LIGHT_CONTROL_FIELDS in automation.h. CONF_EFFECT has special-case - # handling below (lambda or static name resolution), so its setter is None. + # Order/bits must match LIGHT_CONTROL_FIELDS in automation.h. + # EFFECT has special handling below; setter=None skips the generic loop. FIELDS = ( (CONF_COLOR_MODE, "set_color_mode", ColorMode), (CONF_STATE, "set_state", cg.bool_), From dae15301e15e218c076f2731c506bf01266dff1f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 26 Apr 2026 21:41:24 -0500 Subject: [PATCH 5/5] [light] Shorten Empty comment to one line --- esphome/components/light/automation.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index 2dc8c08ee05..817c0e32960 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -24,8 +24,7 @@ template class ToggleAction : public Action { LightState *state_; }; -// Distinct empty types per field tag so [[no_unique_address]] always coalesces -// (the same empty type used multiple times is not guaranteed to share an address). +// Unique Empty per field so [[no_unique_address]] is guaranteed to coalesce. namespace light_control_detail { template struct Empty {}; } // namespace light_control_detail