mirror of
https://github.com/esphome/esphome.git
synced 2026-09-17 10:08:40 +00:00
Merge remote-tracking branch 'upstream/templatable-value-specialize' into integration
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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...);
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user