diff --git a/esphome/components/cc1101/__init__.py b/esphome/components/cc1101/__init__.py index 27092908621..0feb384ac23 100644 --- a/esphome/components/cc1101/__init__.py +++ b/esphome/components/cc1101/__init__.py @@ -423,11 +423,10 @@ def _register_setter_actions(): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) data = config[CONF_VALUE] - if cg.is_template(data): - templ_ = await cg.templatable(data, args, _type) - cg.add(getattr(var, _setter)(templ_)) - else: - cg.add(getattr(var, _setter)(_map[data] if _map else data)) + if _map and not cg.is_template(data): + data = _map[data] + templ_ = await cg.templatable(data, args, _type) + cg.add(getattr(var, _setter)(templ_)) return var automation.register_action( diff --git a/esphome/components/datetime/__init__.py b/esphome/components/datetime/__init__.py index 90835624bf1..895ac4e243e 100644 --- a/esphome/components/datetime/__init__.py +++ b/esphome/components/datetime/__init__.py @@ -204,7 +204,8 @@ async def datetime_date_set_to_code(config, action_id, template_arg, args): ("month", date_config[CONF_MONTH]), ("year", date_config[CONF_YEAR]), ) - cg.add(action_var.set_date(date_struct)) + template_ = await cg.templatable(date_struct, args, cg.ESPTime) + cg.add(action_var.set_date(template_)) return action_var @@ -236,7 +237,8 @@ async def datetime_time_set_to_code(config, action_id, template_arg, args): ("minute", time_config[CONF_MINUTE]), ("hour", time_config[CONF_HOUR]), ) - cg.add(action_var.set_time(time_struct)) + template_ = await cg.templatable(time_struct, args, cg.ESPTime) + cg.add(action_var.set_time(template_)) return action_var @@ -271,5 +273,6 @@ async def datetime_datetime_set_to_code(config, action_id, template_arg, args): ("month", datetime_config[CONF_MONTH]), ("year", datetime_config[CONF_YEAR]), ) - cg.add(action_var.set_datetime(datetime_struct)) + template_ = await cg.templatable(datetime_struct, args, cg.ESPTime) + cg.add(action_var.set_datetime(template_)) return action_var diff --git a/esphome/components/display/__init__.py b/esphome/components/display/__init__.py index 67d76a59d9d..744b5d16c49 100644 --- a/esphome/components/display/__init__.py +++ b/esphome/components/display/__init__.py @@ -207,7 +207,8 @@ async def display_page_show_to_code(config, action_id, template_arg, args): cg.add(var.set_page(template_)) else: paren = await cg.get_variable(config[CONF_ID]) - cg.add(var.set_page(paren)) + template_ = await cg.templatable(paren, args, DisplayPagePtr) + cg.add(var.set_page(template_)) return var diff --git a/esphome/components/globals/__init__.py b/esphome/components/globals/__init__.py index c0694093606..fe83b1ea7c3 100644 --- a/esphome/components/globals/__init__.py +++ b/esphome/components/globals/__init__.py @@ -9,7 +9,6 @@ from esphome.const import ( CONF_VALUE, ) from esphome.core import CoroPriority, coroutine_with_priority -from esphome.cpp_generator import LambdaExpression from esphome.types import ConfigType CODEOWNERS = ["@esphome/core"] @@ -109,16 +108,8 @@ async def globals_set_to_code(config, action_id, template_arg, args): full_id, paren = await cg.get_variable_with_full_id(config[CONF_ID]) template_arg = cg.TemplateArguments(full_id.type, *template_arg) var = cg.new_Pvariable(action_id, template_arg, paren) - value = config[CONF_VALUE] - if cg.is_template(value): - templ = await cg.templatable(value, args, None, to_exp=cg.RawExpression) - else: - # Wrap raw constant in a stateless lambda for TemplatableFn storage. - # Use RawExpression for the value since T is a template parameter - # (the C++ compiler handles the type deduction). - raw_value = cg.RawExpression(value) - templ = LambdaExpression( - f"return {cg.safe_exp(raw_value)};", args, capture="", return_type=None - ) + templ = await cg.templatable( + config[CONF_VALUE], args, None, to_exp=cg.RawExpression + ) cg.add(var.set_value(templ)) return var diff --git a/esphome/components/lightwaverf/__init__.py b/esphome/components/lightwaverf/__init__.py index 46c400cb0e4..76eabc2b712 100644 --- a/esphome/components/lightwaverf/__init__.py +++ b/esphome/components/lightwaverf/__init__.py @@ -61,15 +61,13 @@ async def send_raw_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) - repeats = await cg.templatable(config[CONF_REPEAT], args, int) - inverted = await cg.templatable(config[CONF_INVERTED], args, bool) - pulse_length = await cg.templatable(config[CONF_PULSE_LENGTH], args, int) - code = config[CONF_CODE] - - cg.add(var.set_repeats(repeats)) - cg.add(var.set_inverted(inverted)) - cg.add(var.set_pulse_length(pulse_length)) - cg.add(var.set_data(code)) + template_ = await cg.templatable(config[CONF_REPEAT], args, cg.int_) + cg.add(var.set_repeat(template_)) + template_ = await cg.templatable(config[CONF_INVERTED], args, cg.int_) + cg.add(var.set_inverted(template_)) + template_ = await cg.templatable(config[CONF_PULSE_LENGTH], args, cg.int_) + cg.add(var.set_pulse_length(template_)) + cg.add(var.set_code(config[CONF_CODE])) return var diff --git a/esphome/components/lightwaverf/lightwaverf.h b/esphome/components/lightwaverf/lightwaverf.h index ee4e91e9d1d..6210e6b5d47 100644 --- a/esphome/components/lightwaverf/lightwaverf.h +++ b/esphome/components/lightwaverf/lightwaverf.h @@ -45,11 +45,7 @@ template class SendRawAction : public Action { TEMPLATABLE_VALUE(int, inverted); TEMPLATABLE_VALUE(int, pulse_length); TEMPLATABLE_VALUE(std::vector, code); - - void set_repeats(const int &data) { repeat_ = data; } - void set_inverted(const int &data) { inverted_ = data; } - void set_pulse_length(const int &data) { pulse_length_ = data; } - void set_data(const std::vector &data) { code_ = data; } + void set_code(std::initializer_list data) { this->code_ = std::vector(data); } void play(const Ts &...x) { int repeats = this->repeat_.value(x...); diff --git a/esphome/components/max7219digit/display.py b/esphome/components/max7219digit/display.py index eb751b995d1..df2423b0d0e 100644 --- a/esphome/components/max7219digit/display.py +++ b/esphome/components/max7219digit/display.py @@ -147,7 +147,8 @@ MAX7219_ON_ACTION_SCHEMA = automation.maybe_simple_id( async def max7219digit_invert_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) - cg.add(var.set_state(config[CONF_STATE])) + template_ = await cg.templatable(config[CONF_STATE], args, cg.bool_) + cg.add(var.set_state(template_)) return var @@ -166,7 +167,8 @@ async def max7219digit_invert_to_code(config, action_id, template_arg, args): async def max7219digit_visible_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) - cg.add(var.set_state(config[CONF_STATE])) + template_ = await cg.templatable(config[CONF_STATE], args, cg.bool_) + cg.add(var.set_state(template_)) return var @@ -185,7 +187,8 @@ async def max7219digit_visible_to_code(config, action_id, template_arg, args): async def max7219digit_reverse_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) - cg.add(var.set_state(config[CONF_STATE])) + template_ = await cg.templatable(config[CONF_STATE], args, cg.bool_) + cg.add(var.set_state(template_)) return var diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index 9fbaff68601..c8441002586 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -448,7 +448,11 @@ async def number_to_to_code(config, action_id, template_arg, args): template_ = await cg.templatable(cycle, args, bool) cg.add(var.set_cycle(template_)) if (mode := config.get(CONF_MODE)) is not None: - cg.add(var.set_operation(NUMBER_OPERATION_OPTIONS[mode])) + template_ = await cg.templatable( + NUMBER_OPERATION_OPTIONS[mode], args, NumberOperation + ) + cg.add(var.set_operation(template_)) if (cycle := config.get(CONF_CYCLE)) is not None: - cg.add(var.set_cycle(cycle)) + template_ = await cg.templatable(cycle, args, cg.bool_) + cg.add(var.set_cycle(template_)) return var diff --git a/esphome/components/select/__init__.py b/esphome/components/select/__init__.py index b2c17f59ac1..8c7c8f00fa1 100644 --- a/esphome/components/select/__init__.py +++ b/esphome/components/select/__init__.py @@ -282,7 +282,11 @@ async def select_operation_to_code(config, action_id, template_arg, args): template_ = await cg.templatable(cycle, args, bool) cg.add(var.set_cycle(template_)) if (mode := config.get(CONF_MODE)) is not None: - cg.add(var.set_operation(SELECT_OPERATION_OPTIONS[mode])) + template_ = await cg.templatable( + SELECT_OPERATION_OPTIONS[mode], args, SelectOperation + ) + cg.add(var.set_operation(template_)) if (cycle := config.get(CONF_CYCLE)) is not None: - cg.add(var.set_cycle(cycle)) + template_ = await cg.templatable(cycle, args, cg.bool_) + cg.add(var.set_cycle(template_)) return var diff --git a/esphome/components/speaker/media_player/__init__.py b/esphome/components/speaker/media_player/__init__.py index b16f882cbad..320e96c8979 100644 --- a/esphome/components/speaker/media_player/__init__.py +++ b/esphome/components/speaker/media_player/__init__.py @@ -516,7 +516,8 @@ async def play_on_device_media_media_action(config, action_id, template_arg, arg announcement = await cg.templatable(config[CONF_ANNOUNCEMENT], args, cg.bool_) enqueue = await cg.templatable(config[CONF_ENQUEUE], args, cg.bool_) - cg.add(var.set_audio_file(media_file)) + template_ = await cg.templatable(media_file, args, audio.AudioFile.operator("ptr")) + cg.add(var.set_audio_file(template_)) cg.add(var.set_announcement(announcement)) cg.add(var.set_enqueue(enqueue)) return var diff --git a/esphome/core/automation.h b/esphome/core/automation.h index 7d5981c3b85..eb270bfee26 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -40,6 +40,7 @@ template struct gens<0, S...> { using type = seq; }; template class TemplatableFn { public: TemplatableFn() = default; + TemplatableFn(std::nullptr_t) = delete; // Exact return type match — direct function pointer storage template TemplatableFn(F f) requires std::convertible_to : f_(f) {} @@ -80,11 +81,12 @@ template class TemplatableFn { // Forward declaration for TemplatableValue (string specialization needs it) template class TemplatableValue; -/// Selects TemplatableFn (4 bytes) for non-string types, TemplatableValue (8 bytes) for std::string. -/// std::string needs TemplatableValue for const char*, __FlashStringHelper*, and PROGMEM support. +/// Selects TemplatableFn (4 bytes) for trivially copyable types, TemplatableValue (8 bytes) otherwise. +/// Non-trivial types (std::string, std::vector, etc.) need TemplatableValue for raw value +/// storage, PROGMEM/FlashStringHelper support (strings), and proper copy/move/destruction. template using TemplatableStorage = - std::conditional_t, TemplatableValue, TemplatableFn>; + std::conditional_t, TemplatableFn, TemplatableValue>; #define TEMPLATABLE_VALUE_(type, name) \ protected: \ @@ -101,14 +103,17 @@ using TemplatableStorage = template class TemplatableValue { public: TemplatableValue() = default; + TemplatableValue(std::nullptr_t) = delete; // Accept raw constants template TemplatableValue(V value) requires(!std::invocable) : tag_(VALUE) { - new (&this->value_) T(static_cast(std::move(value))); + new (&this->storage_.value_) T(static_cast(std::move(value))); } // Accept stateless lambdas (convertible to function pointer) - template TemplatableValue(F f) requires std::convertible_to : tag_(FN) { this->f_ = f; } + template TemplatableValue(F f) requires std::convertible_to : tag_(FN) { + this->storage_.f_ = f; + } // Convertible return type (e.g., int -> uint8_t) — casting trampoline template @@ -116,7 +121,7 @@ template class TemplatableValue { "codegen")]] TemplatableValue(F) requires(!std::convertible_to) && std::invocable &&std::convertible_to, T> &&std::is_empty_v &&std::default_initializable : tag_(FN) { - this->f_ = [](X... x) -> T { return static_cast(F{}(x...)); }; + this->storage_.f_ = [](X... x) -> T { return static_cast(F{}(x...)); }; } // Reject any callable that didn't match the above @@ -128,18 +133,18 @@ template class TemplatableValue { TemplatableValue(const TemplatableValue &other) : tag_(other.tag_) { if (this->tag_ == VALUE) { - new (&this->value_) T(other.value_); + new (&this->storage_.value_) T(other.storage_.value_); } else if (this->tag_ == FN) { - this->f_ = other.f_; + this->storage_.f_ = other.storage_.f_; } } TemplatableValue(TemplatableValue &&other) noexcept : tag_(other.tag_) { if (this->tag_ == VALUE) { - new (&this->value_) T(std::move(other.value_)); + new (&this->storage_.value_) T(std::move(other.storage_.value_)); other.destroy_(); } else if (this->tag_ == FN) { - this->f_ = other.f_; + this->storage_.f_ = other.storage_.f_; } other.tag_ = NONE; } @@ -149,9 +154,9 @@ template class TemplatableValue { this->destroy_(); this->tag_ = other.tag_; if (this->tag_ == VALUE) { - new (&this->value_) T(other.value_); + new (&this->storage_.value_) T(other.storage_.value_); } else if (this->tag_ == FN) { - this->f_ = other.f_; + this->storage_.f_ = other.storage_.f_; } } return *this; @@ -162,10 +167,10 @@ template class TemplatableValue { this->destroy_(); this->tag_ = other.tag_; if (this->tag_ == VALUE) { - new (&this->value_) T(std::move(other.value_)); + new (&this->storage_.value_) T(std::move(other.storage_.value_)); other.destroy_(); } else if (this->tag_ == FN) { - this->f_ = other.f_; + this->storage_.f_ = other.storage_.f_; } other.tag_ = NONE; } @@ -178,9 +183,9 @@ template class TemplatableValue { T value(X... x) const { if (this->tag_ == FN) - return this->f_(x...); + return this->storage_.f_(x...); if (this->tag_ == VALUE) - return this->value_; + return this->storage_.value_; return T{}; } @@ -200,15 +205,20 @@ template class TemplatableValue { void destroy_() { if constexpr (!std::is_trivially_destructible_v) { if (this->tag_ == VALUE) - this->value_.~T(); + this->storage_.value_.~T(); } } enum Tag : uint8_t { NONE, VALUE, FN } tag_{NONE}; - union { + // Union with explicit ctor/dtor to support non-trivially-constructible/destructible T + // (e.g., std::vector). Lifetime of value_ is managed externally via + // placement new and destroy_(). + union Storage { + constexpr Storage() : f_(nullptr) {} + constexpr ~Storage() {} T value_; T (*f_)(X...); - }; + } storage_; }; /// Specialization for std::string: supports VALUE, STATIC_STRING, FLASH_STRING, diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index c41171257b9..814e37e02cb 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -848,9 +848,11 @@ async def templatable( # Automatically wrap static strings in ESPHOME_F() for PROGMEM storage on ESP8266. # On other platforms ESPHOME_F() is a no-op returning const char*. return FlashStringLiteral(value) - # For non-string types, wrap constants in stateless lambdas so that - # TemplatableFn (used by TEMPLATABLE_VALUE macro) stores them as function pointers. - if output_type is not None and output_type is not std_string: + # Wrap non-string constants in stateless lambdas so that TemplatableFn + # (used by TEMPLATABLE_VALUE macro) stores them as function pointers. + # When output_type is None, the lambda omits the return type annotation + # and the C++ compiler deduces it (used by globals where T is unknown). + if output_type is not std_string: return LambdaExpression( f"return {safe_exp(value)};", args, diff --git a/tests/unit_tests/test_cpp_generator.py b/tests/unit_tests/test_cpp_generator.py index 81ae586e23b..c75851df0cb 100644 --- a/tests/unit_tests/test_cpp_generator.py +++ b/tests/unit_tests/test_cpp_generator.py @@ -652,11 +652,11 @@ async def test_templatable__empty_string_with_std_string() -> None: @pytest.mark.asyncio async def test_templatable__string_with_none_output_type() -> None: - """Static string with output_type=None returns raw string (no wrapping).""" + """Static string with output_type=None returns stateless lambda (no return type).""" result = await cg.templatable("hello", [], None) - assert isinstance(result, str) - assert result == "hello" + assert isinstance(result, cg.LambdaExpression) + assert result.capture == "" @pytest.mark.asyncio @@ -678,10 +678,11 @@ async def test_templatable__string_with_non_string_output_type() -> None: @pytest.mark.asyncio async def test_templatable__with_to_exp_callable() -> None: - """When to_exp is provided, it is applied to non-template values.""" + """When to_exp is provided with output_type=None, result is lambda-wrapped.""" result = await cg.templatable(42, [], None, to_exp=lambda x: x * 2) - assert result == 84 + assert isinstance(result, cg.LambdaExpression) + assert result.capture == "" @pytest.mark.asyncio @@ -695,11 +696,12 @@ async def test_templatable__with_to_exp_callable_and_output_type() -> None: @pytest.mark.asyncio async def test_templatable__with_to_exp_dict() -> None: - """When to_exp is a dict, value is looked up.""" + """When to_exp is a dict, value is looked up and lambda-wrapped.""" mapping: dict[str, int] = {"on": 1, "off": 0} result = await cg.templatable("on", [], None, to_exp=mapping) - assert result == 1 + assert isinstance(result, cg.LambdaExpression) + assert result.capture == "" @pytest.mark.asyncio