diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index 552b18d8c8..a5c9220a23 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -24,119 +24,50 @@ template class ToggleAction : public Action { LightState *state_; }; -/** Compact light control action using per-field union storage. - * - * Each field stores either a constant value or a function pointer in the same - * word. A 2-bit-per-field type tag in a uint32_t tracks whether each field is - * unset, a constant, or a lambda (stateless function pointer — ESPHome codegen - * always produces empty-capture lambdas). - * - * Size: ~76 bytes on ESP32 (vs ~128 bytes with TemplatableValue per field). - * No per-field heap allocation. - */ +/// Compact light control action — each field is a function pointer (nullptr = unset). +/// Codegen wraps constants in stateless lambdas. 72 bytes vs 128 with TemplatableValue. template class LightControlAction : public Action { public: explicit LightControlAction(LightState *parent) : parent_(parent) {} -// clang-format off -// Single field list — drives enum, setters, and play(). #define LIGHT_CONTROL_FIELDS(X) \ - X(ColorMode, color_mode) \ - X(bool, state) \ - X(uint32_t, transition_length)\ - X(uint32_t, flash_length) \ - X(float, brightness) \ - X(float, color_brightness) \ - X(float, red) \ - X(float, green) \ - X(float, blue) \ - X(float, white) \ - X(float, color_temperature) \ - X(float, cold_white) \ - X(float, warm_white) \ + X(ColorMode, color_mode) \ + X(bool, state) \ + X(uint32_t, transition_length) \ + X(uint32_t, flash_length) \ + X(float, brightness) \ + X(float, color_brightness) \ + X(float, red) \ + X(float, green) \ + X(float, blue) \ + X(float, white) \ + X(float, color_temperature) \ + X(float, cold_white) \ + X(float, warm_white) \ X(uint32_t, effect) -#define LIGHT_CONTROL_SETTER_(type, name) \ - void set_##name(type v) { this->set_constant_(FIELD_##name, v); } \ - template void set_##name(F f) requires std::invocable { \ - this->template set_lambda_(FIELD_##name, std::forward(f)); \ - } +#define LIGHT_FIELD_SETTER_(type, name) \ + void set_##name(type (*f)(Ts...)) { this->name##_ = f; } +#define LIGHT_FIELD_APPLY_(type, name) \ + if (this->name##_) \ + call.set_##name(this->name##_(x...)); +#define LIGHT_FIELD_DECL_(type, name) type (*name##_)(Ts...){nullptr}; -#define APPLY_LIGHT_FIELD_(type, name) \ - if (auto t = this->get_field_type_(FIELD_##name); t) \ - call.set_##name(this->get_value_(FIELD_##name, t, x...)); - // clang-format on - - protected: - enum FieldIndex : uint8_t { -#define FIELD_ENUM_(type, name) FIELD_##name, - LIGHT_CONTROL_FIELDS(FIELD_ENUM_) -#undef FIELD_ENUM_ - NUM_FIELDS - }; - - /// 2-bit field type encoded in field_types_ - enum FieldType : uint8_t { TYPE_NONE = 0, TYPE_CONSTANT = 1, TYPE_LAMBDA = 2 }; - static_assert(NUM_FIELDS * 2 <= 32, "Too many fields for uint32_t field_types_"); - - /// Per-field storage: constant value or function pointer bytes in the same word. - /// All access is via memcpy to avoid type-punning and void*-to-function-pointer UB. - union FieldValue { - uint8_t raw[sizeof(void *)]; - }; - - FieldType get_field_type_(uint8_t field) const { - return static_cast((this->field_types_ >> (field * 2)) & 0x3); - } - - void set_field_type_(uint8_t field, FieldType type) { - uint32_t shift = field * 2; - this->field_types_ = (this->field_types_ & ~(0x3u << shift)) | (static_cast(type) << shift); - } - - template void set_constant_(uint8_t field, T value) { - static_assert(std::is_trivially_copyable_v); - static_assert(sizeof(T) <= sizeof(FieldValue)); - this->set_field_type_(field, TYPE_CONSTANT); - memcpy(&this->values_[field], &value, sizeof(T)); - } - - template void set_lambda_(uint8_t field, F f) { - // ESPHome codegen always produces stateless lambdas (empty capture list), - // which are convertible to function pointers. - static_assert(std::convertible_to, "LightControlAction only supports stateless lambdas"); - static_assert(sizeof(T(*)(Ts...)) <= sizeof(FieldValue)); - this->set_field_type_(field, TYPE_LAMBDA); - auto fn = static_cast(f); - memcpy(&this->values_[field], &fn, sizeof(fn)); - } - - template T get_value_(uint8_t field, FieldType type, const Ts &...x) const { - if (type == TYPE_CONSTANT) { - T value; - memcpy(&value, &this->values_[field], sizeof(T)); - return value; - } - // TYPE_LAMBDA — function pointer stored via memcpy - T (*fn)(Ts...); - memcpy(&fn, &this->values_[field], sizeof(fn)); - return fn(x...); - } - - LightState *parent_; - uint32_t field_types_{0}; ///< 2 bits per field - FieldValue values_[NUM_FIELDS]{}; - - public: - LIGHT_CONTROL_FIELDS(LIGHT_CONTROL_SETTER_) -#undef LIGHT_CONTROL_SETTER_ + LIGHT_CONTROL_FIELDS(LIGHT_FIELD_SETTER_) void play(const Ts &...x) override { auto call = this->parent_->make_call(); - LIGHT_CONTROL_FIELDS(APPLY_LIGHT_FIELD_) + LIGHT_CONTROL_FIELDS(LIGHT_FIELD_APPLY_) call.perform(); } -#undef APPLY_LIGHT_FIELD_ + + protected: + LightState *parent_; + LIGHT_CONTROL_FIELDS(LIGHT_FIELD_DECL_) + +#undef LIGHT_FIELD_DECL_ +#undef LIGHT_FIELD_APPLY_ +#undef LIGHT_FIELD_SETTER_ #undef LIGHT_CONTROL_FIELDS }; diff --git a/esphome/components/light/automation.py b/esphome/components/light/automation.py index 16e7d72f6b..365a64584c 100644 --- a/esphome/components/light/automation.py +++ b/esphome/components/light/automation.py @@ -1,3 +1,5 @@ +from typing import Any + from esphome import automation import esphome.codegen as cg from esphome.config import path_context @@ -28,7 +30,7 @@ from esphome.const import ( ) from esphome.core import CORE, EsphomeError, Lambda from esphome.cpp_generator import LambdaExpression -from esphome.types import ConfigType +from esphome.types import ConfigType, SafeExpType from .types import ( COLOR_MODES, @@ -141,6 +143,28 @@ LIGHT_TURN_ON_ACTION_SCHEMA = automation.maybe_simple_id( ) +async def _as_lambda( + value: Any, + args: list[tuple[SafeExpType, str]], + output_type: SafeExpType, +) -> LambdaExpression: + """Return a stateless lambda expression for a templatable value. + + If value is already a lambda, process it normally. Otherwise wrap + the constant in a ``[](...) -> T { return ; }`` expression + so that LightControlAction can store every field as a plain + function pointer. + """ + if cg.is_template(value): + return await cg.process_lambda(value, args, return_type=output_type) + return LambdaExpression( + f"return {cg.safe_exp(value)};", + args, + capture="", + return_type=output_type, + ) + + def _resolve_effect_index(config: ConfigType) -> int: """Resolve a static effect name to its 1-based index at codegen time. @@ -179,47 +203,29 @@ 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) - if CONF_COLOR_MODE in config: - template_ = await cg.templatable(config[CONF_COLOR_MODE], args, ColorMode) - cg.add(var.set_color_mode(template_)) - if CONF_STATE in config: - template_ = await cg.templatable(config[CONF_STATE], args, bool) - cg.add(var.set_state(template_)) - if CONF_TRANSITION_LENGTH in config: - template_ = await cg.templatable( - config[CONF_TRANSITION_LENGTH], args, cg.uint32 - ) - cg.add(var.set_transition_length(template_)) - if CONF_FLASH_LENGTH in config: - template_ = await cg.templatable(config[CONF_FLASH_LENGTH], args, cg.uint32) - cg.add(var.set_flash_length(template_)) - if CONF_BRIGHTNESS in config: - template_ = await cg.templatable(config[CONF_BRIGHTNESS], args, float) - cg.add(var.set_brightness(template_)) - if CONF_COLOR_BRIGHTNESS in config: - template_ = await cg.templatable(config[CONF_COLOR_BRIGHTNESS], args, float) - cg.add(var.set_color_brightness(template_)) - if CONF_RED in config: - template_ = await cg.templatable(config[CONF_RED], args, float) - cg.add(var.set_red(template_)) - if CONF_GREEN in config: - template_ = await cg.templatable(config[CONF_GREEN], args, float) - cg.add(var.set_green(template_)) - if CONF_BLUE in config: - template_ = await cg.templatable(config[CONF_BLUE], args, float) - cg.add(var.set_blue(template_)) - if CONF_WHITE in config: - template_ = await cg.templatable(config[CONF_WHITE], args, float) - cg.add(var.set_white(template_)) - if CONF_COLOR_TEMPERATURE in config: - template_ = await cg.templatable(config[CONF_COLOR_TEMPERATURE], args, float) - cg.add(var.set_color_temperature(template_)) - if CONF_COLD_WHITE in config: - template_ = await cg.templatable(config[CONF_COLD_WHITE], args, float) - cg.add(var.set_cold_white(template_)) - if CONF_WARM_WHITE in config: - template_ = await cg.templatable(config[CONF_WARM_WHITE], args, float) - cg.add(var.set_warm_white(template_)) + + # (config_key, setter_name, c++ type) + FIELDS = ( + (CONF_COLOR_MODE, "set_color_mode", ColorMode), + (CONF_STATE, "set_state", bool), + (CONF_TRANSITION_LENGTH, "set_transition_length", cg.uint32), + (CONF_FLASH_LENGTH, "set_flash_length", cg.uint32), + (CONF_BRIGHTNESS, "set_brightness", float), + (CONF_COLOR_BRIGHTNESS, "set_color_brightness", float), + (CONF_RED, "set_red", float), + (CONF_GREEN, "set_green", float), + (CONF_BLUE, "set_blue", float), + (CONF_WHITE, "set_white", float), + (CONF_COLOR_TEMPERATURE, "set_color_temperature", float), + (CONF_COLD_WHITE, "set_cold_white", float), + (CONF_WARM_WHITE, "set_warm_white", float), + ) + for conf_key, setter, type_ in FIELDS: + if conf_key in config: + cg.add( + getattr(var, setter)(await _as_lambda(config[conf_key], args, type_)) + ) + if CONF_EFFECT in config: if isinstance(config[CONF_EFFECT], Lambda): # Lambda returns a string — wrap in a C++ lambda that resolves @@ -242,8 +248,11 @@ async def light_control_to_code(config, action_id, template_arg, args): cg.add(var.set_effect(wrapper)) else: # Static string — resolve effect name to index at codegen time - effect_index = _resolve_effect_index(config) - cg.add(var.set_effect(effect_index)) + cg.add( + var.set_effect( + await _as_lambda(_resolve_effect_index(config), args, cg.uint32) + ) + ) return var