Merge remote-tracking branch 'upstream/templatable-value-specialize' into integration

This commit is contained in:
J. Nick Koston
2026-04-07 21:51:20 -10:00
13 changed files with 86 additions and 72 deletions
+4 -5
View File
@@ -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(
+6 -3
View File
@@ -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
+2 -1
View File
@@ -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
+3 -12
View File
@@ -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
+7 -9
View File
@@ -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
+1 -5
View File
@@ -45,11 +45,7 @@ template<typename... Ts> class SendRawAction : public Action<Ts...> {
TEMPLATABLE_VALUE(int, inverted);
TEMPLATABLE_VALUE(int, pulse_length);
TEMPLATABLE_VALUE(std::vector<uint8_t>, 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<uint8_t> &data) { code_ = data; }
void set_code(std::initializer_list<uint8_t> data) { this->code_ = std::vector<uint8_t>(data); }
void play(const Ts &...x) {
int repeats = this->repeat_.value(x...);
+6 -3
View File
@@ -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
+6 -2
View File
@@ -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
+6 -2
View File
@@ -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
@@ -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
+29 -19
View File
@@ -40,6 +40,7 @@ template<int... S> struct gens<0, S...> { using type = seq<S...>; };
template<typename T, typename... X> class TemplatableFn {
public:
TemplatableFn() = default;
TemplatableFn(std::nullptr_t) = delete;
// Exact return type match — direct function pointer storage
template<typename F> TemplatableFn(F f) requires std::convertible_to<F, T (*)(X...)> : f_(f) {}
@@ -80,11 +81,12 @@ template<typename T, typename... X> class TemplatableFn {
// Forward declaration for TemplatableValue (string specialization needs it)
template<typename T, typename... X> 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<uint8_t>, etc.) need TemplatableValue for raw value
/// storage, PROGMEM/FlashStringHelper support (strings), and proper copy/move/destruction.
template<typename T, typename... X>
using TemplatableStorage =
std::conditional_t<std::same_as<T, std::string>, TemplatableValue<T, X...>, TemplatableFn<T, X...>>;
std::conditional_t<std::is_trivially_copyable_v<T>, TemplatableFn<T, X...>, TemplatableValue<T, X...>>;
#define TEMPLATABLE_VALUE_(type, name) \
protected: \
@@ -101,14 +103,17 @@ using TemplatableStorage =
template<typename T, typename... X> class TemplatableValue {
public:
TemplatableValue() = default;
TemplatableValue(std::nullptr_t) = delete;
// Accept raw constants
template<typename V> TemplatableValue(V value) requires(!std::invocable<V, X...>) : tag_(VALUE) {
new (&this->value_) T(static_cast<T>(std::move(value)));
new (&this->storage_.value_) T(static_cast<T>(std::move(value)));
}
// Accept stateless lambdas (convertible to function pointer)
template<typename F> TemplatableValue(F f) requires std::convertible_to<F, T (*)(X...)> : tag_(FN) { this->f_ = f; }
template<typename F> TemplatableValue(F f) requires std::convertible_to<F, T (*)(X...)> : tag_(FN) {
this->storage_.f_ = f;
}
// Convertible return type (e.g., int -> uint8_t) — casting trampoline
template<typename F>
@@ -116,7 +121,7 @@ template<typename T, typename... X> class TemplatableValue {
"codegen")]] TemplatableValue(F) requires(!std::convertible_to<F, T (*)(X...)>) &&
std::invocable<F, X...> &&std::convertible_to<std::invoke_result_t<F, X...>, T> &&std::is_empty_v<F>
&&std::default_initializable<F> : tag_(FN) {
this->f_ = [](X... x) -> T { return static_cast<T>(F{}(x...)); };
this->storage_.f_ = [](X... x) -> T { return static_cast<T>(F{}(x...)); };
}
// Reject any callable that didn't match the above
@@ -128,18 +133,18 @@ template<typename T, typename... X> 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<typename T, typename... X> 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<typename T, typename... X> 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<typename T, typename... X> 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<typename T, typename... X> class TemplatableValue {
void destroy_() {
if constexpr (!std::is_trivially_destructible_v<T>) {
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<uint8_t>). 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,
+5 -3
View File
@@ -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,
+9 -7
View File
@@ -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