From 19df4527713e32b02aaf56d19d3e4e1a4c7e5032 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 26 Apr 2026 21:33:57 -0500 Subject: [PATCH] [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 f6a2ca52d4..2511612b57 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 46d37239e5..ddd8948a64 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_))