diff --git a/esphome/components/api/user_services.h b/esphome/components/api/user_services.h index d1b8a6ef0d..1f35be7ef9 100644 --- a/esphome/components/api/user_services.h +++ b/esphome/components/api/user_services.h @@ -275,7 +275,7 @@ template class APIRespondAction : public Action { protected: APIServer *parent_; - TemplatableValue success_{true}; + TemplatableValue success_{[](Ts...) -> bool { return true; }}; TemplatableValue error_message_{""}; #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON std::function json_builder_; diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index a5c9220a23..f6a2ca52d4 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -24,51 +24,60 @@ template class ToggleAction : public Action { LightState *state_; }; -/// 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) {} -#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(uint32_t, effect) - -#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}; - - LIGHT_CONTROL_FIELDS(LIGHT_FIELD_SETTER_) + 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) void play(const Ts &...x) override { auto call = this->parent_->make_call(); - LIGHT_CONTROL_FIELDS(LIGHT_FIELD_APPLY_) + if (this->color_mode_.has_value()) + call.set_color_mode(this->color_mode_.value(x...)); + if (this->state_.has_value()) + call.set_state(this->state_.value(x...)); + if (this->transition_length_.has_value()) + call.set_transition_length(this->transition_length_.value(x...)); + if (this->flash_length_.has_value()) + call.set_flash_length(this->flash_length_.value(x...)); + if (this->brightness_.has_value()) + call.set_brightness(this->brightness_.value(x...)); + if (this->color_brightness_.has_value()) + call.set_color_brightness(this->color_brightness_.value(x...)); + if (this->red_.has_value()) + call.set_red(this->red_.value(x...)); + if (this->green_.has_value()) + call.set_green(this->green_.value(x...)); + if (this->blue_.has_value()) + call.set_blue(this->blue_.value(x...)); + if (this->white_.has_value()) + call.set_white(this->white_.value(x...)); + if (this->color_temperature_.has_value()) + call.set_color_temperature(this->color_temperature_.value(x...)); + if (this->cold_white_.has_value()) + call.set_cold_white(this->cold_white_.value(x...)); + if (this->warm_white_.has_value()) + call.set_warm_white(this->warm_white_.value(x...)); + if (this->effect_.has_value()) + call.set_effect(this->effect_.value(x...)); call.perform(); } 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 }; template class DimRelativeAction : public Action { diff --git a/esphome/components/light/automation.py b/esphome/components/light/automation.py index 365a64584c..2400822b31 100644 --- a/esphome/components/light/automation.py +++ b/esphome/components/light/automation.py @@ -1,5 +1,3 @@ -from typing import Any - from esphome import automation import esphome.codegen as cg from esphome.config import path_context @@ -30,7 +28,7 @@ from esphome.const import ( ) from esphome.core import CORE, EsphomeError, Lambda from esphome.cpp_generator import LambdaExpression -from esphome.types import ConfigType, SafeExpType +from esphome.types import ConfigType from .types import ( COLOR_MODES, @@ -143,28 +141,6 @@ 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. @@ -222,9 +198,8 @@ async def light_control_to_code(config, action_id, template_arg, args): ) 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_)) - ) + template_ = await cg.templatable(config[conf_key], args, type_) + cg.add(getattr(var, setter)(template_)) if CONF_EFFECT in config: if isinstance(config[CONF_EFFECT], Lambda): @@ -248,11 +223,10 @@ 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 - cg.add( - var.set_effect( - await _as_lambda(_resolve_effect_index(config), args, cg.uint32) - ) + template_ = await cg.templatable( + _resolve_effect_index(config), args, cg.uint32 ) + cg.add(var.set_effect(template_)) return var diff --git a/esphome/components/number/automation.h b/esphome/components/number/automation.h index a7cd04f083..834998b32b 100644 --- a/esphome/components/number/automation.h +++ b/esphome/components/number/automation.h @@ -63,8 +63,8 @@ class ValueRangeTrigger : public Trigger, public Component { Number *parent_; ESPPreferenceObject rtc_; bool previous_in_range_{false}; - TemplatableValue min_{NAN}; - TemplatableValue max_{NAN}; + TemplatableValue min_{[](float) -> float { return NAN; }}; // NAN = no bound + TemplatableValue max_{[](float) -> float { return NAN; }}; // NAN = no bound }; template class NumberInRangeCondition : public Condition { diff --git a/esphome/components/sensor/automation.h b/esphome/components/sensor/automation.h index b4de712727..989ee2317b 100644 --- a/esphome/components/sensor/automation.h +++ b/esphome/components/sensor/automation.h @@ -79,8 +79,8 @@ class ValueRangeTrigger : public Trigger, public Component { Sensor *parent_; ESPPreferenceObject rtc_; bool previous_in_range_{false}; - TemplatableValue min_{NAN}; - TemplatableValue max_{NAN}; + TemplatableValue min_{[](float) -> float { return NAN; }}; + TemplatableValue max_{[](float) -> float { return NAN; }}; }; template class SensorInRangeCondition : public Condition { diff --git a/esphome/core/automation.h b/esphome/core/automation.h index 05c7f19588..7879478f5b 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -43,61 +43,78 @@ template struct gens<0, S...> { using type = seq; }; #define TEMPLATABLE_VALUE(type, name) TEMPLATABLE_VALUE_(type, name) +/// Primary template: function-pointer-only storage (4 bytes on 32-bit). +/// Codegen wraps constants in stateless lambdas so only a function pointer is needed. +/// Stateful lambdas (std::function) are rejected at compile time. template class TemplatableValue { - // For std::string, store pointer to heap-allocated string to keep union pointer-sized. - // For other types, store value inline. - static constexpr bool USE_HEAP_STORAGE = std::same_as; + public: + TemplatableValue() = default; + // Accept stateless lambdas (convertible to function pointer) + template TemplatableValue(F f) requires std::convertible_to : f_(f) {} + + // Reject stateful lambdas at compile time + template + TemplatableValue(F) requires std::invocable &&(!std::convertible_to) = delete; + + bool has_value() const { return this->f_ != nullptr; } + + T value(X... x) const { return this->f_ ? this->f_(x...) : T{}; } + + optional optional_value(X... x) const { + if (!this->f_) + return {}; + return this->f_(x...); + } + + T value_or(X... x, T default_value) const { return this->f_ ? this->f_(x...) : default_value; } + + protected: + T (*f_)(X...){nullptr}; +}; + +/// Specialization for std::string: supports VALUE, STATIC_STRING, FLASH_STRING, +/// stateless lambdas, and stateful lambdas (std::function). +template class TemplatableValue { public: TemplatableValue() : type_(NONE) {} - // For const char* when T is std::string: store pointer directly, no heap allocation - // String remains in flash and is only converted to std::string when value() is called - TemplatableValue(const char *str) requires std::same_as : type_(STATIC_STRING) { - this->static_str_ = str; - } + // For const char*: store pointer directly, no heap allocation. + // String remains in flash and is only converted to std::string when value() is called. + TemplatableValue(const char *str) : type_(STATIC_STRING) { this->static_str_ = str; } #ifdef USE_ESP8266 // On ESP8266, __FlashStringHelper* is a distinct type from const char*. // ESPHOME_F(s) expands to F(s) which returns __FlashStringHelper* pointing to PROGMEM. - // Store as FLASH_STRING — value()/is_empty()/ref_or_copy_to() use _P functions - // to access the PROGMEM pointer safely. - TemplatableValue(const __FlashStringHelper *str) requires std::same_as : type_(FLASH_STRING) { + // Store as FLASH_STRING — value()/is_empty()/ref_or_copy_to() use _P functions. + TemplatableValue(const __FlashStringHelper *str) : type_(FLASH_STRING) { this->static_str_ = reinterpret_cast(str); } #endif template TemplatableValue(F value) requires(!std::invocable) : type_(VALUE) { - if constexpr (USE_HEAP_STORAGE) { - this->value_ = new T(std::move(value)); - } else { - new (&this->value_) T(std::move(value)); - } + this->value_ = new std::string(std::move(value)); } // For stateless lambdas (convertible to function pointer): use function pointer template - TemplatableValue(F f) requires std::invocable && std::convertible_to + TemplatableValue(F f) requires std::invocable && std::convertible_to : type_(STATELESS_LAMBDA) { - this->stateless_f_ = f; // Implicit conversion to function pointer + this->stateless_f_ = f; } // For stateful lambdas (not convertible to function pointer): use std::function template - TemplatableValue(F f) requires std::invocable &&(!std::convertible_to) : type_(LAMBDA) { - this->f_ = new std::function(std::move(f)); + TemplatableValue(F f) requires std::invocable &&(!std::convertible_to) + : type_(LAMBDA) { + this->f_ = new std::function(std::move(f)); } - // Copy constructor TemplatableValue(const TemplatableValue &other) : type_(other.type_) { if (this->type_ == VALUE) { - if constexpr (USE_HEAP_STORAGE) { - this->value_ = new T(*other.value_); - } else { - new (&this->value_) T(other.value_); - } + this->value_ = new std::string(*other.value_); } else if (this->type_ == LAMBDA) { - this->f_ = new std::function(*other.f_); + this->f_ = new std::function(*other.f_); } else if (this->type_ == STATELESS_LAMBDA) { this->stateless_f_ = other.stateless_f_; } else if (this->type_ == STATIC_STRING || this->type_ == FLASH_STRING) { @@ -105,15 +122,10 @@ template class TemplatableValue { } } - // Move constructor TemplatableValue(TemplatableValue &&other) noexcept : type_(other.type_) { if (this->type_ == VALUE) { - if constexpr (USE_HEAP_STORAGE) { - this->value_ = other.value_; - other.value_ = nullptr; - } else { - new (&this->value_) T(std::move(other.value_)); - } + this->value_ = other.value_; + other.value_ = nullptr; } else if (this->type_ == LAMBDA) { this->f_ = other.f_; other.f_ = nullptr; @@ -125,7 +137,6 @@ template class TemplatableValue { other.type_ = NONE; } - // Assignment operators TemplatableValue &operator=(const TemplatableValue &other) { if (this != &other) { this->~TemplatableValue(); @@ -144,82 +155,58 @@ template class TemplatableValue { ~TemplatableValue() { if (this->type_ == VALUE) { - if constexpr (USE_HEAP_STORAGE) { - delete this->value_; - } else { - this->value_.~T(); - } + delete this->value_; } else if (this->type_ == LAMBDA) { delete this->f_; } - // STATELESS_LAMBDA/STATIC_STRING/FLASH_STRING/NONE: no cleanup needed (pointers, not heap-allocated) } bool has_value() const { return this->type_ != NONE; } - T value(X... x) const { + std::string value(X... x) const { switch (this->type_) { case STATELESS_LAMBDA: - return this->stateless_f_(x...); // Direct function pointer call + return this->stateless_f_(x...); case LAMBDA: - return (*this->f_)(x...); // std::function call + return (*this->f_)(x...); case VALUE: - if constexpr (USE_HEAP_STORAGE) { - return *this->value_; - } else { - return this->value_; - } + return *this->value_; case STATIC_STRING: - // if constexpr required: code must compile for all T, but STATIC_STRING - // can only be set when T is std::string (enforced by constructor constraint) - if constexpr (std::same_as) { - return std::string(this->static_str_); - } - __builtin_unreachable(); + return std::string(this->static_str_); #ifdef USE_ESP8266 - case FLASH_STRING: - // PROGMEM pointer — must use _P functions to access on ESP8266 - if constexpr (std::same_as) { - size_t len = strlen_P(this->static_str_); - std::string result(len, '\0'); - memcpy_P(result.data(), this->static_str_, len); - return result; - } - __builtin_unreachable(); + case FLASH_STRING: { + size_t len = strlen_P(this->static_str_); + std::string result(len, '\0'); + memcpy_P(result.data(), this->static_str_, len); + return result; + } #endif case NONE: default: - return T{}; + return {}; } } - optional optional_value(X... x) { - if (!this->has_value()) { + optional optional_value(X... x) { + if (!this->has_value()) return {}; - } return this->value(x...); } - T value_or(X... x, T default_value) { - if (!this->has_value()) { + std::string value_or(X... x, std::string default_value) { + if (!this->has_value()) return default_value; - } return this->value(x...); } - /// Check if this holds a static string (const char* stored without allocation) - /// The pointer is always directly readable (RAM or flash-mapped). - /// Returns false for FLASH_STRING (PROGMEM on ESP8266, requires _P functions). + /// Check if this holds a static string (const char* stored without allocation). bool is_static_string() const { return this->type_ == STATIC_STRING; } - /// Get the static string pointer (only valid if is_static_string() returns true) - /// The pointer is always directly readable — FLASH_STRING uses a separate type. + /// Get the static string pointer (only valid if is_static_string() returns true). const char *get_static_string() const { return this->static_str_; } - /// Check if the string value is empty without allocating (for std::string specialization). - /// For NONE, returns true. For STATIC_STRING/VALUE, checks without allocation. - /// For LAMBDA/STATELESS_LAMBDA, must call value() which may allocate. - bool is_empty() const requires std::same_as { + /// Check if the string value is empty without allocating. + bool is_empty() const { switch (this->type_) { case NONE: return true; @@ -227,25 +214,18 @@ template class TemplatableValue { return this->static_str_ == nullptr || this->static_str_[0] == '\0'; #ifdef USE_ESP8266 case FLASH_STRING: - // PROGMEM pointer — must use progmem_read_byte on ESP8266 return this->static_str_ == nullptr || progmem_read_byte(reinterpret_cast(this->static_str_)) == '\0'; #endif case VALUE: return this->value_->empty(); - default: // LAMBDA/STATELESS_LAMBDA - must call value() + default: return this->value().empty(); } } - /// Get a StringRef to the string value without heap allocation when possible. - /// For STATIC_STRING/VALUE, returns reference to existing data (no allocation). - /// For FLASH_STRING (ESP8266 PROGMEM), copies to provided buffer via _P functions. - /// For LAMBDA/STATELESS_LAMBDA, calls value(), copies to provided buffer, returns ref to buffer. - /// @param lambda_buf Buffer used only for copy cases (must remain valid while StringRef is used). - /// @param lambda_buf_size Size of the buffer. - /// @return StringRef pointing to the string data. - StringRef ref_or_copy_to(char *lambda_buf, size_t lambda_buf_size) const requires std::same_as { + /// Get a StringRef without heap allocation when possible. + StringRef ref_or_copy_to(char *lambda_buf, size_t lambda_buf_size) const { switch (this->type_) { case NONE: return StringRef(); @@ -258,7 +238,6 @@ template class TemplatableValue { if (this->static_str_ == nullptr) return StringRef(); { - // PROGMEM pointer — copy to buffer via _P functions size_t len = strlen_P(this->static_str_); size_t copy_len = std::min(len, lambda_buf_size - 1); memcpy_P(lambda_buf, this->static_str_, copy_len); @@ -268,7 +247,7 @@ template class TemplatableValue { #endif case VALUE: return StringRef(this->value_->data(), this->value_->size()); - default: { // LAMBDA/STATELESS_LAMBDA - must call value() and copy + default: { std::string result = this->value(); size_t copy_len = std::min(result.size(), lambda_buf_size - 1); memcpy(lambda_buf, result.data(), copy_len); @@ -278,22 +257,20 @@ template class TemplatableValue { } } - protected : enum : uint8_t { - NONE, - VALUE, - LAMBDA, - STATELESS_LAMBDA, - STATIC_STRING, // For const char* when T is std::string - avoids heap allocation - FLASH_STRING, // PROGMEM pointer on ESP8266; never set on other platforms - } type_; - // For std::string, use heap pointer to minimize union size (4 bytes vs 12+). - // For other types, store value inline as before. - using ValueStorage = std::conditional_t; + protected: + enum : uint8_t { + NONE, + VALUE, + LAMBDA, + STATELESS_LAMBDA, + STATIC_STRING, + FLASH_STRING, + } type_; union { - ValueStorage value_; // T for inline storage, T* for heap storage - std::function *f_; - T (*stateless_f_)(X...); - const char *static_str_; // For STATIC_STRING and FLASH_STRING types + std::string *value_; + std::function *f_; + std::string (*stateless_f_)(X...); + const char *static_str_; }; }; diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index a8efe96cce..55f49fcfa7 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -823,7 +823,9 @@ async def templatable( """Generate code for a templatable config option. If `value` is a templated value, the lambda expression is returned. - Otherwise the value is returned as-is (optionally process with to_exp). + For std::string output, constants are returned as-is (with PROGMEM wrapping). + For all other output types, constants are wrapped in stateless lambdas + so that TemplatableValue can store them as function pointers. :param value: The value to process. :param args: The arguments for the lambda expression. @@ -833,20 +835,28 @@ async def templatable( """ if is_template(value): return await process_lambda(value, args, return_type=output_type) - if to_exp is None: + if to_exp is not None: + value = to_exp[value] if isinstance(to_exp, dict) else to_exp(value) + elif isinstance(value, str) and output_type is not None: # Automatically wrap static strings in ESPHOME_F() for PROGMEM storage on ESP8266. # On other platforms ESPHOME_F() is a no-op returning const char*. - # Lazy import to avoid circular dependency (cpp_generator <-> cpp_types). - # Identity check (is) avoids brittle string comparison. - if isinstance(value, str) and output_type is not None: - from esphome.cpp_types import std_string + from esphome.cpp_types import std_string - if output_type is std_string: - return FlashStringLiteral(value) - return value - if isinstance(to_exp, dict): - return to_exp[value] - return to_exp(value) + if output_type is std_string: + return FlashStringLiteral(value) + # For non-string types, wrap constants in stateless lambdas so that + # TemplatableValue stores them as function pointers (4 bytes vs 8). + if output_type is not None: + from esphome.cpp_types import std_string + + if output_type is not std_string: + return LambdaExpression( + f"return {safe_exp(value)};", + args, + capture="", + return_type=output_type, + ) + return value class MockObj(Expression): diff --git a/tests/unit_tests/test_cpp_generator.py b/tests/unit_tests/test_cpp_generator.py index bdc31cdef8..aef6d056b2 100644 --- a/tests/unit_tests/test_cpp_generator.py +++ b/tests/unit_tests/test_cpp_generator.py @@ -669,11 +669,10 @@ async def test_templatable__int_with_std_string() -> None: @pytest.mark.asyncio async def test_templatable__string_with_non_string_output_type() -> None: - """Static string with non-std::string output_type returns raw string.""" + """Static string with non-std::string output_type returns stateless lambda.""" result = await cg.templatable("hello", [], ct.bool_) - assert isinstance(result, str) - assert result == "hello" + assert isinstance(result, cg.LambdaExpression) @pytest.mark.asyncio