From 8c0e6198036606119ee4836e5f5e97ff9969041e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 7 Apr 2026 21:20:26 -1000 Subject: [PATCH 1/3] fix missing templatable in lightwaverf and match name to code --- esphome/components/lightwaverf/__init__.py | 16 +++++++--------- esphome/components/lightwaverf/lightwaverf.h | 6 +----- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/esphome/components/lightwaverf/__init__.py b/esphome/components/lightwaverf/__init__.py index 46c400cb0e..76eabc2b71 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 ee4e91e9d1..6210e6b5d4 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...); From b18c911429fc25a139815e865d9bab8aacaf3dae Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 7 Apr 2026 21:23:27 -1000 Subject: [PATCH 2/3] another one --- esphome/components/cc1101/__init__.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/esphome/components/cc1101/__init__.py b/esphome/components/cc1101/__init__.py index 2709290862..0feb384ac2 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( From fe17c86f58b1bee4aa023d53437836610ece2259 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 7 Apr 2026 21:32:20 -1000 Subject: [PATCH 3/3] now fix is always the same for unwrapped --- esphome/components/globals/__init__.py | 15 +++------------ esphome/cpp_generator.py | 8 +++++--- tests/unit_tests/test_cpp_generator.py | 16 +++++++++------- 3 files changed, 17 insertions(+), 22 deletions(-) diff --git a/esphome/components/globals/__init__.py b/esphome/components/globals/__init__.py index c069409360..fe83b1ea7c 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/cpp_generator.py b/esphome/cpp_generator.py index c41171257b..814e37e02c 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 81ae586e23..c75851df0c 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