[core] Add TemplatableFn for 4-byte function-pointer templatable storage (#15545)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
J. Nick Koston
2026-04-08 02:38:00 -10:00
committed by GitHub
parent 9bf53e0ab8
commit a8b7c7a4ac
42 changed files with 432 additions and 256 deletions

View File

@@ -19,8 +19,8 @@ class AnalogThresholdBinarySensor : public Component, public binary_sensor::Bina
protected: protected:
sensor::Sensor *sensor_{nullptr}; sensor::Sensor *sensor_{nullptr};
TemplatableValue<float> upper_threshold_{}; TemplatableFn<float> upper_threshold_{};
TemplatableValue<float> lower_threshold_{}; TemplatableFn<float> lower_threshold_{};
bool raw_state_{false}; // Pre-filter state for hysteresis logic bool raw_state_{false}; // Pre-filter state for hysteresis logic
}; };

View File

@@ -275,7 +275,7 @@ template<typename... Ts> class APIRespondAction : public Action<Ts...> {
protected: protected:
APIServer *parent_; APIServer *parent_;
TemplatableValue<bool, Ts...> success_{true}; TemplatableFn<bool, Ts...> success_{[](Ts...) -> bool { return true; }};
TemplatableValue<std::string, Ts...> error_message_{""}; TemplatableValue<std::string, Ts...> error_message_{""};
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
std::function<void(Ts..., JsonObject)> json_builder_; std::function<void(Ts..., JsonObject)> json_builder_;

View File

@@ -36,7 +36,7 @@ class TimeoutFilter : public Filter, public Component {
template<typename T> void set_timeout_value(T timeout) { this->timeout_delay_ = timeout; } template<typename T> void set_timeout_value(T timeout) { this->timeout_delay_ = timeout; }
protected: protected:
TemplatableValue<uint32_t> timeout_delay_{}; TemplatableFn<uint32_t> timeout_delay_{};
}; };
class DelayedOnOffFilter final : public Filter, public Component { class DelayedOnOffFilter final : public Filter, public Component {
@@ -49,8 +49,8 @@ class DelayedOnOffFilter final : public Filter, public Component {
template<typename T> void set_off_delay(T delay) { this->off_delay_ = delay; } template<typename T> void set_off_delay(T delay) { this->off_delay_ = delay; }
protected: protected:
TemplatableValue<uint32_t> on_delay_{}; TemplatableFn<uint32_t> on_delay_{};
TemplatableValue<uint32_t> off_delay_{}; TemplatableFn<uint32_t> off_delay_{};
}; };
class DelayedOnFilter : public Filter, public Component { class DelayedOnFilter : public Filter, public Component {
@@ -62,7 +62,7 @@ class DelayedOnFilter : public Filter, public Component {
template<typename T> void set_delay(T delay) { this->delay_ = delay; } template<typename T> void set_delay(T delay) { this->delay_ = delay; }
protected: protected:
TemplatableValue<uint32_t> delay_{}; TemplatableFn<uint32_t> delay_{};
}; };
class DelayedOffFilter : public Filter, public Component { class DelayedOffFilter : public Filter, public Component {
@@ -74,7 +74,7 @@ class DelayedOffFilter : public Filter, public Component {
template<typename T> void set_delay(T delay) { this->delay_ = delay; } template<typename T> void set_delay(T delay) { this->delay_ = delay; }
protected: protected:
TemplatableValue<uint32_t> delay_{}; TemplatableFn<uint32_t> delay_{};
}; };
class InvertFilter : public Filter { class InvertFilter : public Filter {
@@ -155,7 +155,7 @@ class SettleFilter : public Filter, public Component {
template<typename T> void set_delay(T delay) { this->delay_ = delay; } template<typename T> void set_delay(T delay) { this->delay_ = delay; }
protected: protected:
TemplatableValue<uint32_t> delay_{}; TemplatableFn<uint32_t> delay_{};
bool steady_{true}; bool steady_{true};
}; };

View File

@@ -423,11 +423,10 @@ def _register_setter_actions():
var = cg.new_Pvariable(action_id, template_arg) var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID]) await cg.register_parented(var, config[CONF_ID])
data = config[CONF_VALUE] data = config[CONF_VALUE]
if cg.is_template(data): if _map and not cg.is_template(data):
templ_ = await cg.templatable(data, args, _type) data = _map[data]
cg.add(getattr(var, _setter)(templ_)) templ_ = await cg.templatable(data, args, _type)
else: cg.add(getattr(var, _setter)(templ_))
cg.add(getattr(var, _setter)(_map[data] if _map else data))
return var return var
automation.register_action( automation.register_action(

View File

@@ -204,7 +204,8 @@ async def datetime_date_set_to_code(config, action_id, template_arg, args):
("month", date_config[CONF_MONTH]), ("month", date_config[CONF_MONTH]),
("year", date_config[CONF_YEAR]), ("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 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]), ("minute", time_config[CONF_MINUTE]),
("hour", time_config[CONF_HOUR]), ("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 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]), ("month", datetime_config[CONF_MONTH]),
("year", datetime_config[CONF_YEAR]), ("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 return action_var

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_)) cg.add(var.set_page(template_))
else: else:
paren = await cg.get_variable(config[CONF_ID]) 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 return var

View File

@@ -378,7 +378,8 @@ async def esp32_ble_tracker_start_scan_action_to_code(
): ):
paren = await cg.get_variable(config[CONF_ID]) paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren) var = cg.new_Pvariable(action_id, template_arg, paren)
cg.add(var.set_continuous(config[CONF_CONTINUOUS])) template_ = await cg.templatable(config[CONF_CONTINUOUS], args, cg.bool_)
cg.add(var.set_continuous(template_))
return var return var

View File

@@ -109,7 +109,7 @@ async def globals_set_to_code(config, action_id, template_arg, args):
template_arg = cg.TemplateArguments(full_id.type, *template_arg) template_arg = cg.TemplateArguments(full_id.type, *template_arg)
var = cg.new_Pvariable(action_id, template_arg, paren) var = cg.new_Pvariable(action_id, template_arg, paren)
templ = await cg.templatable( templ = await cg.templatable(
config[CONF_VALUE], args, None, to_exp=cg.RawExpression config[CONF_VALUE], args, None, to_exp=cg.RawExpression, wrap_constant=True
) )
cg.add(var.set_value(templ)) cg.add(var.set_value(templ))
return var return var

View File

@@ -302,11 +302,13 @@ async def http_request_action_to_code(config, action_id, template_arg, args):
template_ = await cg.templatable(config[CONF_URL], args, cg.std_string) template_ = await cg.templatable(config[CONF_URL], args, cg.std_string)
cg.add(var.set_url(template_)) cg.add(var.set_url(template_))
cg.add(var.set_method(config[CONF_METHOD])) template_ = await cg.templatable(config[CONF_METHOD], args, cg.const_char_ptr)
cg.add(var.set_method(template_))
capture_response = config[CONF_CAPTURE_RESPONSE] capture_response = config[CONF_CAPTURE_RESPONSE]
if capture_response: if capture_response:
cg.add(var.set_capture_response(capture_response)) template_ = await cg.templatable(capture_response, args, cg.bool_)
cg.add(var.set_capture_response(template_))
cg.add_define("USE_HTTP_REQUEST_RESPONSE") cg.add_define("USE_HTTP_REQUEST_RESPONSE")
cg.add(var.set_max_response_buffer_size(config[CONF_MAX_RESPONSE_BUFFER_SIZE])) cg.add(var.set_max_response_buffer_size(config[CONF_MAX_RESPONSE_BUFFER_SIZE]))

View File

@@ -457,7 +457,7 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
#endif #endif
void init_request_headers(size_t count) { this->request_headers_.init(count); } void init_request_headers(size_t count) { this->request_headers_.init(count); }
void add_request_header(const char *key, TemplatableValue<const char *, Ts...> value) { void add_request_header(const char *key, TemplatableFn<const char *, Ts...> value) {
this->request_headers_.push_back({key, value}); this->request_headers_.push_back({key, value});
} }
@@ -560,7 +560,7 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
} }
} }
HttpRequestComponent *parent_; HttpRequestComponent *parent_;
FixedVector<std::pair<const char *, TemplatableValue<const char *, Ts...>>> request_headers_{}; FixedVector<std::pair<const char *, TemplatableFn<const char *, Ts...>>> request_headers_{};
std::vector<std::string> lower_case_collect_headers_{"content-type", "content-length"}; std::vector<std::string> lower_case_collect_headers_{"content-type", "content-length"};
FixedVector<std::pair<const char *, TemplatableValue<std::string, Ts...>>> json_{}; FixedVector<std::pair<const char *, TemplatableValue<std::string, Ts...>>> json_{};
std::function<void(Ts..., JsonObject)> json_func_{nullptr}; std::function<void(Ts..., JsonObject)> json_func_{nullptr};

View File

@@ -24,51 +24,60 @@ template<typename... Ts> class ToggleAction : public Action<Ts...> {
LightState *state_; 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<typename... Ts> class LightControlAction : public Action<Ts...> { template<typename... Ts> class LightControlAction : public Action<Ts...> {
public: public:
explicit LightControlAction(LightState *parent) : parent_(parent) {} explicit LightControlAction(LightState *parent) : parent_(parent) {}
#define LIGHT_CONTROL_FIELDS(X) \ TEMPLATABLE_VALUE(ColorMode, color_mode)
X(ColorMode, color_mode) \ TEMPLATABLE_VALUE(bool, state)
X(bool, state) \ TEMPLATABLE_VALUE(uint32_t, transition_length)
X(uint32_t, transition_length) \ TEMPLATABLE_VALUE(uint32_t, flash_length)
X(uint32_t, flash_length) \ TEMPLATABLE_VALUE(float, brightness)
X(float, brightness) \ TEMPLATABLE_VALUE(float, color_brightness)
X(float, color_brightness) \ TEMPLATABLE_VALUE(float, red)
X(float, red) \ TEMPLATABLE_VALUE(float, green)
X(float, green) \ TEMPLATABLE_VALUE(float, blue)
X(float, blue) \ TEMPLATABLE_VALUE(float, white)
X(float, white) \ TEMPLATABLE_VALUE(float, color_temperature)
X(float, color_temperature) \ TEMPLATABLE_VALUE(float, cold_white)
X(float, cold_white) \ TEMPLATABLE_VALUE(float, warm_white)
X(float, warm_white) \ TEMPLATABLE_VALUE(uint32_t, effect)
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_)
void play(const Ts &...x) override { void play(const Ts &...x) override {
auto call = this->parent_->make_call(); 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(); call.perform();
} }
protected: protected:
LightState *parent_; 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<typename... Ts> class DimRelativeAction : public Action<Ts...> { template<typename... Ts> class DimRelativeAction : public Action<Ts...> {

View File

@@ -1,5 +1,3 @@
from typing import Any
from esphome import automation from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
from esphome.config import path_context from esphome.config import path_context
@@ -30,7 +28,7 @@ from esphome.const import (
) )
from esphome.core import CORE, EsphomeError, Lambda from esphome.core import CORE, EsphomeError, Lambda
from esphome.cpp_generator import LambdaExpression from esphome.cpp_generator import LambdaExpression
from esphome.types import ConfigType, SafeExpType from esphome.types import ConfigType
from .types import ( from .types import (
COLOR_MODES, 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 <value>; }`` 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: def _resolve_effect_index(config: ConfigType) -> int:
"""Resolve a static effect name to its 1-based index at codegen time. """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: for conf_key, setter, type_ in FIELDS:
if conf_key in config: if conf_key in config:
cg.add( template_ = await cg.templatable(config[conf_key], args, type_)
getattr(var, setter)(await _as_lambda(config[conf_key], args, type_)) cg.add(getattr(var, setter)(template_))
)
if CONF_EFFECT in config: if CONF_EFFECT in config:
if isinstance(config[CONF_EFFECT], Lambda): 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)) cg.add(var.set_effect(wrapper))
else: else:
# Static string — resolve effect name to index at codegen time # Static string — resolve effect name to index at codegen time
cg.add( template_ = await cg.templatable(
var.set_effect( _resolve_effect_index(config), args, cg.uint32
await _as_lambda(_resolve_effect_index(config), args, cg.uint32)
)
) )
cg.add(var.set_effect(template_))
return var return var

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]) paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren) var = cg.new_Pvariable(action_id, template_arg, paren)
repeats = await cg.templatable(config[CONF_REPEAT], args, int) template_ = await cg.templatable(config[CONF_REPEAT], args, cg.int_)
inverted = await cg.templatable(config[CONF_INVERTED], args, bool) cg.add(var.set_repeat(template_))
pulse_length = await cg.templatable(config[CONF_PULSE_LENGTH], args, int) template_ = await cg.templatable(config[CONF_INVERTED], args, cg.int_)
code = config[CONF_CODE] cg.add(var.set_inverted(template_))
template_ = await cg.templatable(config[CONF_PULSE_LENGTH], args, cg.int_)
cg.add(var.set_repeats(repeats)) cg.add(var.set_pulse_length(template_))
cg.add(var.set_inverted(inverted)) cg.add(var.set_code(config[CONF_CODE]))
cg.add(var.set_pulse_length(pulse_length))
cg.add(var.set_data(code))
return var return var

View File

@@ -45,11 +45,7 @@ template<typename... Ts> class SendRawAction : public Action<Ts...> {
TEMPLATABLE_VALUE(int, inverted); TEMPLATABLE_VALUE(int, inverted);
TEMPLATABLE_VALUE(int, pulse_length); TEMPLATABLE_VALUE(int, pulse_length);
TEMPLATABLE_VALUE(std::vector<uint8_t>, code); TEMPLATABLE_VALUE(std::vector<uint8_t>, code);
void set_code(std::initializer_list<uint8_t> data) { this->code_ = std::vector<uint8_t>(data); }
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 play(const Ts &...x) { void play(const Ts &...x) {
int repeats = this->repeat_.value(x...); int repeats = this->repeat_.value(x...);

View File

@@ -412,7 +412,7 @@ void LvglComponent::flush_cb_(lv_display_t *disp_drv, const lv_area_t *area, uin
lv_display_flush_ready(disp_drv); lv_display_flush_ready(disp_drv);
} }
IdleTrigger::IdleTrigger(LvglComponent *parent, TemplatableValue<uint32_t> timeout) : timeout_(std::move(timeout)) { IdleTrigger::IdleTrigger(LvglComponent *parent, TemplatableFn<uint32_t> timeout) : timeout_(timeout) {
parent->add_on_idle_callback([this](uint32_t idle_time) { parent->add_on_idle_callback([this](uint32_t idle_time) {
if (!this->is_idle_ && idle_time > this->timeout_.value()) { if (!this->is_idle_ && idle_time > this->timeout_.value()) {
this->is_idle_ = true; this->is_idle_ = true;

View File

@@ -284,10 +284,10 @@ class LvglComponent : public PollingComponent {
class IdleTrigger : public Trigger<> { class IdleTrigger : public Trigger<> {
public: public:
explicit IdleTrigger(LvglComponent *parent, TemplatableValue<uint32_t> timeout); explicit IdleTrigger(LvglComponent *parent, TemplatableFn<uint32_t> timeout);
protected: protected:
TemplatableValue<uint32_t> timeout_; TemplatableFn<uint32_t> timeout_;
bool is_idle_{}; bool is_idle_{};
}; };

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): async def max7219digit_invert_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg) var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID]) 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 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): async def max7219digit_visible_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg) var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID]) 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 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): async def max7219digit_reverse_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg) var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID]) 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 return var

View File

@@ -13,6 +13,7 @@ from esphome.const import (
) )
from esphome.core import CORE, Lambda, coroutine_with_priority from esphome.core import CORE, Lambda, coroutine_with_priority
from esphome.coroutine import CoroPriority from esphome.coroutine import CoroPriority
from esphome.cpp_generator import LambdaExpression
from esphome.types import ConfigType from esphome.types import ConfigType
CODEOWNERS = ["@esphome/core"] CODEOWNERS = ["@esphome/core"]
@@ -131,6 +132,12 @@ def mdns_service(
Returns: Returns:
A StructInitializer representing a MDNSService struct A StructInitializer representing a MDNSService struct
""" """
# Wrap port in a stateless lambda for TemplatableFn storage.
# Can't use cg.templatable() here because this is a sync function.
if not isinstance(port, LambdaExpression):
port = LambdaExpression(
f"return {cg.safe_exp(port)};", [], capture="", return_type=cg.uint16
)
return cg.StructInitializer( return cg.StructInitializer(
MDNSService, MDNSService,
("service_type", cg.RawExpression(f"MDNS_STR({cg.safe_exp(service)})")), ("service_type", cg.RawExpression(f"MDNS_STR({cg.safe_exp(service)})")),

View File

@@ -57,7 +57,7 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
auto &service = services.emplace_next(); auto &service = services.emplace_next();
service.service_type = MDNS_STR(SERVICE_ESPHOMELIB); service.service_type = MDNS_STR(SERVICE_ESPHOMELIB);
service.proto = MDNS_STR(SERVICE_TCP); service.proto = MDNS_STR(SERVICE_TCP);
service.port = api::global_api_server->get_port(); service.port = []() -> uint16_t { return api::global_api_server->get_port(); };
const auto &friendly_name = App.get_friendly_name(); const auto &friendly_name = App.get_friendly_name();
bool friendly_name_empty = friendly_name.empty(); bool friendly_name_empty = friendly_name.empty();
@@ -151,7 +151,7 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
auto &prom_service = services.emplace_next(); auto &prom_service = services.emplace_next();
prom_service.service_type = MDNS_STR(SERVICE_PROMETHEUS); prom_service.service_type = MDNS_STR(SERVICE_PROMETHEUS);
prom_service.proto = MDNS_STR(SERVICE_TCP); prom_service.proto = MDNS_STR(SERVICE_TCP);
prom_service.port = USE_WEBSERVER_PORT; prom_service.port = []() -> uint16_t { return USE_WEBSERVER_PORT; };
#endif #endif
#ifdef USE_SENDSPIN #ifdef USE_SENDSPIN
@@ -162,7 +162,7 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
auto &sendspin_service = services.emplace_next(); auto &sendspin_service = services.emplace_next();
sendspin_service.service_type = MDNS_STR(SERVICE_SENDSPIN); sendspin_service.service_type = MDNS_STR(SERVICE_SENDSPIN);
sendspin_service.proto = MDNS_STR(SERVICE_TCP); sendspin_service.proto = MDNS_STR(SERVICE_TCP);
sendspin_service.port = USE_SENDSPIN_PORT; sendspin_service.port = []() -> uint16_t { return USE_SENDSPIN_PORT; };
sendspin_service.txt_records = {{MDNS_STR(TXT_SENDSPIN_PATH), MDNS_STR(VALUE_SENDSPIN_PATH)}}; sendspin_service.txt_records = {{MDNS_STR(TXT_SENDSPIN_PATH), MDNS_STR(VALUE_SENDSPIN_PATH)}};
#endif #endif
@@ -172,7 +172,7 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
auto &web_service = services.emplace_next(); auto &web_service = services.emplace_next();
web_service.service_type = MDNS_STR(SERVICE_HTTP); web_service.service_type = MDNS_STR(SERVICE_HTTP);
web_service.proto = MDNS_STR(SERVICE_TCP); web_service.proto = MDNS_STR(SERVICE_TCP);
web_service.port = USE_WEBSERVER_PORT; web_service.port = []() -> uint16_t { return USE_WEBSERVER_PORT; };
#endif #endif
#if !defined(USE_API) && !defined(USE_PROMETHEUS) && !defined(USE_SENDSPIN) && !defined(USE_WEBSERVER) && \ #if !defined(USE_API) && !defined(USE_PROMETHEUS) && !defined(USE_SENDSPIN) && !defined(USE_WEBSERVER) && \
@@ -185,7 +185,7 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
auto &fallback_service = services.emplace_next(); auto &fallback_service = services.emplace_next();
fallback_service.service_type = MDNS_STR(SERVICE_HTTP); fallback_service.service_type = MDNS_STR(SERVICE_HTTP);
fallback_service.proto = MDNS_STR(SERVICE_TCP); fallback_service.proto = MDNS_STR(SERVICE_TCP);
fallback_service.port = USE_WEBSERVER_PORT; fallback_service.port = []() -> uint16_t { return USE_WEBSERVER_PORT; };
fallback_service.txt_records = {{MDNS_STR(TXT_VERSION), MDNS_STR(VALUE_VERSION)}}; fallback_service.txt_records = {{MDNS_STR(TXT_VERSION), MDNS_STR(VALUE_VERSION)}};
#endif #endif
} }
@@ -199,7 +199,7 @@ void MDNSComponent::dump_config() {
ESP_LOGV(TAG, " Services:"); ESP_LOGV(TAG, " Services:");
for (const auto &service : this->services_) { for (const auto &service : this->services_) {
ESP_LOGV(TAG, " - %s, %s, %d", MDNS_STR_ARG(service.service_type), MDNS_STR_ARG(service.proto), ESP_LOGV(TAG, " - %s, %s, %d", MDNS_STR_ARG(service.service_type), MDNS_STR_ARG(service.proto),
const_cast<TemplatableValue<uint16_t> &>(service.port).value()); service.port.value());
for (const auto &record : service.txt_records) { for (const auto &record : service.txt_records) {
ESP_LOGV(TAG, " TXT: %s = %s", MDNS_STR_ARG(record.key), MDNS_STR_ARG(record.value)); ESP_LOGV(TAG, " TXT: %s = %s", MDNS_STR_ARG(record.key), MDNS_STR_ARG(record.value));
} }

View File

@@ -36,7 +36,7 @@ struct MDNSService {
// second label indicating protocol _including_ underscore character prefix // second label indicating protocol _including_ underscore character prefix
// as defined in RFC6763 Section 7, like "_tcp" or "_udp" // as defined in RFC6763 Section 7, like "_tcp" or "_udp"
const MDNSString *proto; const MDNSString *proto;
TemplatableValue<uint16_t> port; TemplatableFn<uint16_t> port;
FixedVector<MDNSTXTRecord> txt_records; FixedVector<MDNSTXTRecord> txt_records;
}; };

View File

@@ -37,7 +37,7 @@ static void register_esp32(MDNSComponent *comp, StaticVector<MDNSService, MDNS_S
txt_records.get()[i].key = MDNS_STR_ARG(record.key); txt_records.get()[i].key = MDNS_STR_ARG(record.key);
txt_records.get()[i].value = MDNS_STR_ARG(record.value); txt_records.get()[i].value = MDNS_STR_ARG(record.value);
} }
uint16_t port = const_cast<TemplatableValue<uint16_t> &>(service.port).value(); uint16_t port = service.port.value();
err = mdns_service_add(nullptr, MDNS_STR_ARG(service.service_type), MDNS_STR_ARG(service.proto), port, err = mdns_service_add(nullptr, MDNS_STR_ARG(service.service_type), MDNS_STR_ARG(service.proto), port,
txt_records.get(), service.txt_records.size()); txt_records.get(), service.txt_records.size());

View File

@@ -27,7 +27,7 @@ static void register_esp8266(MDNSComponent *, StaticVector<MDNSService, MDNS_SER
while (progmem_read_byte((const uint8_t *) service_type) == '_') { while (progmem_read_byte((const uint8_t *) service_type) == '_') {
service_type++; service_type++;
} }
uint16_t port = const_cast<TemplatableValue<uint16_t> &>(service.port).value(); uint16_t port = service.port.value();
MDNS.addService(FPSTR(service_type), FPSTR(proto), port); MDNS.addService(FPSTR(service_type), FPSTR(proto), port);
for (const auto &record : service.txt_records) { for (const auto &record : service.txt_records) {
MDNS.addServiceTxt(FPSTR(service_type), FPSTR(proto), FPSTR(MDNS_STR_ARG(record.key)), MDNS.addServiceTxt(FPSTR(service_type), FPSTR(proto), FPSTR(MDNS_STR_ARG(record.key)),

View File

@@ -27,7 +27,7 @@ static void register_libretiny(MDNSComponent *, StaticVector<MDNSService, MDNS_S
while (*service_type == '_') { while (*service_type == '_') {
service_type++; service_type++;
} }
uint16_t port_ = const_cast<TemplatableValue<uint16_t> &>(service.port).value(); uint16_t port_ = service.port.value();
MDNS.addService(service_type, proto, port_); MDNS.addService(service_type, proto, port_);
for (const auto &record : service.txt_records) { for (const auto &record : service.txt_records) {
MDNS.addServiceTxt(service_type, proto, MDNS_STR_ARG(record.key), MDNS_STR_ARG(record.value)); MDNS.addServiceTxt(service_type, proto, MDNS_STR_ARG(record.key), MDNS_STR_ARG(record.value));

View File

@@ -32,7 +32,7 @@ static void register_rp2040(MDNSComponent *, StaticVector<MDNSService, MDNS_SERV
while (*service_type == '_') { while (*service_type == '_') {
service_type++; service_type++;
} }
uint16_t port = const_cast<TemplatableValue<uint16_t> &>(service.port).value(); uint16_t port = service.port.value();
MDNS.addService(service_type, proto, port); MDNS.addService(service_type, proto, port);
for (const auto &record : service.txt_records) { for (const auto &record : service.txt_records) {
MDNS.addServiceTxt(service_type, proto, MDNS_STR_ARG(record.key), MDNS_STR_ARG(record.value)); MDNS.addServiceTxt(service_type, proto, MDNS_STR_ARG(record.key), MDNS_STR_ARG(record.value));

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) template_ = await cg.templatable(cycle, args, bool)
cg.add(var.set_cycle(template_)) cg.add(var.set_cycle(template_))
if (mode := config.get(CONF_MODE)) is not None: 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: 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 return var

View File

@@ -63,8 +63,8 @@ class ValueRangeTrigger : public Trigger<float>, public Component {
Number *parent_; Number *parent_;
ESPPreferenceObject rtc_; ESPPreferenceObject rtc_;
bool previous_in_range_{false}; bool previous_in_range_{false};
TemplatableValue<float, float> min_{NAN}; TemplatableFn<float, float> min_{[](float) -> float { return NAN; }};
TemplatableValue<float, float> max_{NAN}; TemplatableFn<float, float> max_{[](float) -> float { return NAN; }};
}; };
template<typename... Ts> class NumberInRangeCondition : public Condition<Ts...> { template<typename... Ts> class NumberInRangeCondition : public Condition<Ts...> {

View File

@@ -181,7 +181,7 @@ void OpenThreadSrpComponent::setup() {
memcpy(string, host_name.c_str(), host_name_len); memcpy(string, host_name.c_str(), host_name_len);
// Set port // Set port
entry->mService.mPort = const_cast<TemplatableValue<uint16_t> &>(service.port).value(); entry->mService.mPort = service.port.value();
otDnsTxtEntry *txt_entries = otDnsTxtEntry *txt_entries =
reinterpret_cast<otDnsTxtEntry *>(this->pool_alloc_(sizeof(otDnsTxtEntry) * service.txt_records.size())); reinterpret_cast<otDnsTxtEntry *>(this->pool_alloc_(sizeof(otDnsTxtEntry) * service.txt_records.size()));

View File

@@ -2123,7 +2123,8 @@ async def abbwelcome_action(var, config, args):
await cg.templatable(config[CONF_MESSAGE_TYPE], args, cg.uint8) await cg.templatable(config[CONF_MESSAGE_TYPE], args, cg.uint8)
) )
) )
cg.add(var.set_auto_message_id(CONF_MESSAGE_ID not in config)) template_ = await cg.templatable(CONF_MESSAGE_ID not in config, args, cg.bool_)
cg.add(var.set_auto_message_id(template_))
if CONF_MESSAGE_ID in config: if CONF_MESSAGE_ID in config:
cg.add( cg.add(
var.set_message_id( var.set_message_id(
@@ -2231,3 +2232,9 @@ async def Toto_action(var, config, args):
cg.add(var.set_rc_code_2(template_)) cg.add(var.set_rc_code_2(template_))
template_ = await cg.templatable(config[CONF_COMMAND], args, cg.uint8) template_ = await cg.templatable(config[CONF_COMMAND], args, cg.uint8)
cg.add(var.set_command(template_)) cg.add(var.set_command(template_))
# Set toto-specific defaults (only if user didn't configure repeat)
if CONF_REPEAT not in config:
template_ = await cg.templatable(3, args, cg.uint32)
cg.add(var.set_send_times(template_))
template_ = await cg.templatable(36000, args, cg.uint32)
cg.add(var.set_send_wait(template_))

View File

@@ -35,8 +35,6 @@ template<typename... Ts> class TotoAction : public RemoteTransmitterActionBase<T
data.rc_code_1 = this->rc_code_1_.value(x...); data.rc_code_1 = this->rc_code_1_.value(x...);
data.rc_code_2 = this->rc_code_2_.value(x...); data.rc_code_2 = this->rc_code_2_.value(x...);
data.command = this->command_.value(x...); data.command = this->command_.value(x...);
this->set_send_times(this->send_times_.value_or(x..., 3));
this->set_send_wait(this->send_wait_.value_or(x..., 36000));
TotoProtocol().encode(dst, data); TotoProtocol().encode(dst, data);
} }
}; };

View File

@@ -211,7 +211,7 @@ template<class... As, typename... Ts> class ScriptExecuteAction<Script<As...>, T
public: public:
ScriptExecuteAction(Script<As...> *script) : script_(script) {} ScriptExecuteAction(Script<As...> *script) : script_(script) {}
using Args = std::tuple<TemplatableValue<As, Ts...>...>; using Args = std::tuple<TemplatableFn<As, Ts...>...>;
template<typename... F> void set_args(F... x) { args_ = Args{x...}; } template<typename... F> void set_args(F... x) { args_ = Args{x...}; }

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) template_ = await cg.templatable(cycle, args, bool)
cg.add(var.set_cycle(template_)) cg.add(var.set_cycle(template_))
if (mode := config.get(CONF_MODE)) is not None: 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: 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 return var

View File

@@ -79,8 +79,8 @@ class ValueRangeTrigger : public Trigger<float>, public Component {
Sensor *parent_; Sensor *parent_;
ESPPreferenceObject rtc_; ESPPreferenceObject rtc_;
bool previous_in_range_{false}; bool previous_in_range_{false};
TemplatableValue<float, float> min_{NAN}; TemplatableFn<float, float> min_{[](float) -> float { return NAN; }};
TemplatableValue<float, float> max_{NAN}; TemplatableFn<float, float> max_{[](float) -> float { return NAN; }};
}; };
template<typename... Ts> class SensorInRangeCondition : public Condition<Ts...> { template<typename... Ts> class SensorInRangeCondition : public Condition<Ts...> {

View File

@@ -213,17 +213,17 @@ optional<float> LambdaFilter::new_value(float value) {
} }
// OffsetFilter // OffsetFilter
OffsetFilter::OffsetFilter(TemplatableValue<float> offset) : offset_(std::move(offset)) {} OffsetFilter::OffsetFilter(TemplatableFn<float> offset) : offset_(offset) {}
optional<float> OffsetFilter::new_value(float value) { return value + this->offset_.value(); } optional<float> OffsetFilter::new_value(float value) { return value + this->offset_.value(); }
// MultiplyFilter // MultiplyFilter
MultiplyFilter::MultiplyFilter(TemplatableValue<float> multiplier) : multiplier_(std::move(multiplier)) {} MultiplyFilter::MultiplyFilter(TemplatableFn<float> multiplier) : multiplier_(multiplier) {}
optional<float> MultiplyFilter::new_value(float value) { return value * this->multiplier_.value(); } optional<float> MultiplyFilter::new_value(float value) { return value * this->multiplier_.value(); }
// ValueListFilter helper (non-template, shared by all ValueListFilter<N> instantiations) // ValueListFilter helper (non-template, shared by all ValueListFilter<N> instantiations)
bool value_list_matches_any(Sensor *parent, float sensor_value, const TemplatableValue<float> *values, size_t count) { bool value_list_matches_any(Sensor *parent, float sensor_value, const TemplatableFn<float> *values, size_t count) {
int8_t accuracy = parent->get_accuracy_decimals(); int8_t accuracy = parent->get_accuracy_decimals();
float accuracy_mult = pow10_int(accuracy); float accuracy_mult = pow10_int(accuracy);
float rounded_sensor = roundf(accuracy_mult * sensor_value); float rounded_sensor = roundf(accuracy_mult * sensor_value);
@@ -258,7 +258,7 @@ optional<float> ThrottleFilter::new_value(float value) {
} }
// ThrottleWithPriorityFilter helper (non-template, keeps App access in .cpp) // ThrottleWithPriorityFilter helper (non-template, keeps App access in .cpp)
optional<float> throttle_with_priority_new_value(Sensor *parent, float value, const TemplatableValue<float> *values, optional<float> throttle_with_priority_new_value(Sensor *parent, float value, const TemplatableFn<float> *values,
size_t count, uint32_t &last_input, uint32_t min_time_between_inputs) { size_t count, uint32_t &last_input, uint32_t min_time_between_inputs) {
const uint32_t now = App.get_loop_component_start_time(); const uint32_t now = App.get_loop_component_start_time();
if (last_input == 0 || now - last_input >= min_time_between_inputs || if (last_input == 0 || now - last_input >= min_time_between_inputs ||

View File

@@ -311,26 +311,26 @@ class StatelessLambdaFilter : public Filter {
/// A simple filter that adds `offset` to each value it receives. /// A simple filter that adds `offset` to each value it receives.
class OffsetFilter : public Filter { class OffsetFilter : public Filter {
public: public:
explicit OffsetFilter(TemplatableValue<float> offset); explicit OffsetFilter(TemplatableFn<float> offset);
optional<float> new_value(float value) override; optional<float> new_value(float value) override;
protected: protected:
TemplatableValue<float> offset_; TemplatableFn<float> offset_;
}; };
/// A simple filter that multiplies to each value it receives by `multiplier`. /// A simple filter that multiplies to each value it receives by `multiplier`.
class MultiplyFilter : public Filter { class MultiplyFilter : public Filter {
public: public:
explicit MultiplyFilter(TemplatableValue<float> multiplier); explicit MultiplyFilter(TemplatableFn<float> multiplier);
optional<float> new_value(float value) override; optional<float> new_value(float value) override;
protected: protected:
TemplatableValue<float> multiplier_; TemplatableFn<float> multiplier_;
}; };
/// Non-template helper for value matching (implementation in filter.cpp) /// Non-template helper for value matching (implementation in filter.cpp)
bool value_list_matches_any(Sensor *parent, float sensor_value, const TemplatableValue<float> *values, size_t count); bool value_list_matches_any(Sensor *parent, float sensor_value, const TemplatableFn<float> *values, size_t count);
/** Base class for filters that compare sensor values against a fixed list of configured values. /** Base class for filters that compare sensor values against a fixed list of configured values.
* *
@@ -342,7 +342,7 @@ bool value_list_matches_any(Sensor *parent, float sensor_value, const Templatabl
*/ */
template<size_t N> class ValueListFilter : public Filter { template<size_t N> class ValueListFilter : public Filter {
protected: protected:
explicit ValueListFilter(std::initializer_list<TemplatableValue<float>> values) { explicit ValueListFilter(std::initializer_list<TemplatableFn<float>> values) {
init_array_from(this->values_, values); init_array_from(this->values_, values);
} }
@@ -351,13 +351,13 @@ template<size_t N> class ValueListFilter : public Filter {
return value_list_matches_any(this->parent_, sensor_value, this->values_.data(), N); return value_list_matches_any(this->parent_, sensor_value, this->values_.data(), N);
} }
std::array<TemplatableValue<float>, N> values_{}; std::array<TemplatableFn<float>, N> values_{};
}; };
/// A simple filter that only forwards the filter chain if it doesn't receive `value_to_filter_out`. /// A simple filter that only forwards the filter chain if it doesn't receive `value_to_filter_out`.
template<size_t N> class FilterOutValueFilter : public ValueListFilter<N> { template<size_t N> class FilterOutValueFilter : public ValueListFilter<N> {
public: public:
explicit FilterOutValueFilter(std::initializer_list<TemplatableValue<float>> values_to_filter_out) explicit FilterOutValueFilter(std::initializer_list<TemplatableFn<float>> values_to_filter_out)
: ValueListFilter<N>(values_to_filter_out) {} : ValueListFilter<N>(values_to_filter_out) {}
optional<float> new_value(float value) override { optional<float> new_value(float value) override {
@@ -379,14 +379,14 @@ class ThrottleFilter : public Filter {
}; };
/// Non-template helper for ThrottleWithPriorityFilter (implementation in filter.cpp) /// Non-template helper for ThrottleWithPriorityFilter (implementation in filter.cpp)
optional<float> throttle_with_priority_new_value(Sensor *parent, float value, const TemplatableValue<float> *values, optional<float> throttle_with_priority_new_value(Sensor *parent, float value, const TemplatableFn<float> *values,
size_t count, uint32_t &last_input, uint32_t min_time_between_inputs); size_t count, uint32_t &last_input, uint32_t min_time_between_inputs);
/// Same as 'throttle' but will immediately publish values contained in `value_to_prioritize`. /// Same as 'throttle' but will immediately publish values contained in `value_to_prioritize`.
template<size_t N> class ThrottleWithPriorityFilter : public ValueListFilter<N> { template<size_t N> class ThrottleWithPriorityFilter : public ValueListFilter<N> {
public: public:
explicit ThrottleWithPriorityFilter(uint32_t min_time_between_inputs, explicit ThrottleWithPriorityFilter(uint32_t min_time_between_inputs,
std::initializer_list<TemplatableValue<float>> prioritized_values) std::initializer_list<TemplatableFn<float>> prioritized_values)
: ValueListFilter<N>(prioritized_values), min_time_between_inputs_(min_time_between_inputs) {} : ValueListFilter<N>(prioritized_values), min_time_between_inputs_(min_time_between_inputs) {}
optional<float> new_value(float value) override { optional<float> new_value(float value) override {
@@ -430,15 +430,15 @@ class TimeoutFilterLast : public TimeoutFilterBase {
// Timeout filter with configured value - evaluates TemplatableValue after timeout // Timeout filter with configured value - evaluates TemplatableValue after timeout
class TimeoutFilterConfigured : public TimeoutFilterBase { class TimeoutFilterConfigured : public TimeoutFilterBase {
public: public:
explicit TimeoutFilterConfigured(uint32_t time_period, const TemplatableValue<float> &new_value) explicit TimeoutFilterConfigured(uint32_t time_period, const TemplatableFn<float> &new_value)
: TimeoutFilterBase(time_period), value_(new_value) {} : TimeoutFilterBase(time_period), value_(new_value) {}
optional<float> new_value(float value) override; optional<float> new_value(float value) override;
protected: protected:
float get_output_value() override { return this->value_.value(); } float get_output_value() override { return this->value_.value(); }
TemplatableValue<float> value_; // 16 bytes (configured output value, can be lambda) TemplatableFn<float> value_; // 4 bytes (configured output value, can be lambda)
// Total: 8 (base) + 16 = 24 bytes + vtable ptr + Component overhead // Total: 8 (base) + 4 = 12 bytes + vtable ptr + Component overhead
}; };
class DebounceFilter : public Filter, public Component { class DebounceFilter : public Filter, public Component {

View File

@@ -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_) announcement = await cg.templatable(config[CONF_ANNOUNCEMENT], args, cg.bool_)
enqueue = await cg.templatable(config[CONF_ENQUEUE], 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_announcement(announcement))
cg.add(var.set_enqueue(enqueue)) cg.add(var.set_enqueue(enqueue))
return var return var

View File

@@ -312,7 +312,8 @@ async def set_playlist_delay_action_to_code(
parent = await cg.get_variable(config[CONF_ID]) parent = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, parent) var = cg.new_Pvariable(action_id, template_arg, parent)
cg.add(var.set_pipeline(config[CONF_PIPELINE])) template_ = await cg.templatable(config[CONF_PIPELINE], args, cg.uint8)
cg.add(var.set_pipeline(template_))
template_ = await cg.templatable(config[CONF_DELAY], args, cg.uint32) template_ = await cg.templatable(config[CONF_DELAY], args, cg.uint32)
cg.add(var.set_delay(template_)) cg.add(var.set_delay(template_))

View File

@@ -455,7 +455,7 @@ async def sprinkler_set_multiplier_to_code(config, action_id, template_arg, args
async def sprinkler_set_queued_valve_to_code(config, action_id, template_arg, args): async def sprinkler_set_queued_valve_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID]) paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren) var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_VALVE_NUMBER], args, cg.uint8) template_ = await cg.templatable(config[CONF_VALVE_NUMBER], args, cg.size_t)
cg.add(var.set_valve_number(template_)) cg.add(var.set_valve_number(template_))
template_ = await cg.templatable(config[CONF_RUN_DURATION], args, cg.uint32) template_ = await cg.templatable(config[CONF_RUN_DURATION], args, cg.uint32)
cg.add(var.set_valve_run_duration(template_)) cg.add(var.set_valve_run_duration(template_))
@@ -487,7 +487,7 @@ async def sprinkler_set_valve_run_duration_to_code(
): ):
paren = await cg.get_variable(config[CONF_ID]) paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren) var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_VALVE_NUMBER], args, cg.uint8) template_ = await cg.templatable(config[CONF_VALVE_NUMBER], args, cg.size_t)
cg.add(var.set_valve_number(template_)) cg.add(var.set_valve_number(template_))
template_ = await cg.templatable(config[CONF_RUN_DURATION], args, cg.uint32) template_ = await cg.templatable(config[CONF_RUN_DURATION], args, cg.uint32)
cg.add(var.set_valve_run_duration(template_)) cg.add(var.set_valve_run_duration(template_))
@@ -525,7 +525,7 @@ async def sprinkler_start_full_cycle_to_code(config, action_id, template_arg, ar
async def sprinkler_start_single_valve_to_code(config, action_id, template_arg, args): async def sprinkler_start_single_valve_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID]) paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren) var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_VALVE_NUMBER], args, cg.uint8) template_ = await cg.templatable(config[CONF_VALVE_NUMBER], args, cg.size_t)
cg.add(var.set_valve_to_start(template_)) cg.add(var.set_valve_to_start(template_))
if CONF_RUN_DURATION in config: if CONF_RUN_DURATION in config:
template_ = await cg.templatable(config[CONF_RUN_DURATION], args, cg.uint32) template_ = await cg.templatable(config[CONF_RUN_DURATION], args, cg.uint32)

View File

@@ -108,7 +108,8 @@ template<typename... Ts> class StartSingleValveAction : public Action<Ts...> {
public: public:
explicit StartSingleValveAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} explicit StartSingleValveAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {}
TEMPLATABLE_VALUE(size_t, valve_to_start) // TemplatableValue (not TemplatableFn) — also set from C++ with raw values in sprinkler.cpp
template<typename V> void set_valve_to_start(V valve_to_start) { this->valve_to_start_ = valve_to_start; }
TEMPLATABLE_VALUE(uint32_t, valve_run_duration) TEMPLATABLE_VALUE(uint32_t, valve_run_duration)
void play(const Ts &...x) override { void play(const Ts &...x) override {
@@ -118,6 +119,7 @@ template<typename... Ts> class StartSingleValveAction : public Action<Ts...> {
protected: protected:
Sprinkler *sprinkler_; Sprinkler *sprinkler_;
TemplatableValue<size_t, Ts...> valve_to_start_{};
}; };
template<typename... Ts> class ShutdownAction : public Action<Ts...> { template<typename... Ts> class ShutdownAction : public Action<Ts...> {

View File

@@ -34,70 +34,236 @@ template<int... S> struct gens<0, S...> { using type = seq<S...>; };
#endif #endif
// NOLINTEND(readability-identifier-naming) // NOLINTEND(readability-identifier-naming)
/// Function-pointer-only templatable storage (4 bytes on 32-bit).
/// Used by the TEMPLATABLE_VALUE macro for codegen-managed fields.
/// Codegen wraps constants in stateless lambdas so only a function pointer is needed.
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) {}
// Convertible return type (e.g., int -> uint8_t) — casting trampoline.
// Stateless lambdas are default-constructible in C++20, so F{} recreates the lambda inside
// the trampoline without capturing. This compiles to the same code as a direct call + cast.
// Deprecated: codegen should use the correct output type to avoid the trampoline.
template<typename F>
[[deprecated("Lambda return type does not match TemplatableFn<T> — use the correct type in "
"codegen")]] TemplatableFn(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> : f_([](X... x) -> T { return static_cast<T>(F{}(x...)); }) {}
// Reject any callable that didn't match the above (stateful lambdas or inconvertible return types)
template<typename F>
TemplatableFn(F) requires std::invocable<F, X...> &&
(!std::convertible_to<F, T (*)(X...)>) &&(!std::is_empty_v<F> ||
!std::convertible_to<std::invoke_result_t<F, X...>, T> ||
!std::default_initializable<F>) = delete;
bool has_value() const { return this->f_ != nullptr; }
T value(X... x) const { return this->f_ ? this->f_(x...) : T{}; }
optional<T> 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};
};
// Forward declaration for TemplatableValue (string specialization needs it)
template<typename T, typename... X> class TemplatableValue;
/// 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::is_trivially_copyable_v<T>, TemplatableFn<T, X...>, TemplatableValue<T, X...>>;
#define TEMPLATABLE_VALUE_(type, name) \ #define TEMPLATABLE_VALUE_(type, name) \
protected: \ protected: \
TemplatableValue<type, Ts...> name##_{}; \ TemplatableStorage<type, Ts...> name##_{}; \
\ \
public: \ public: \
template<typename V> void set_##name(V name) { this->name##_ = name; } template<typename V> void set_##name(V name) { this->name##_ = name; }
#define TEMPLATABLE_VALUE(type, name) TEMPLATABLE_VALUE_(type, name) #define TEMPLATABLE_VALUE(type, name) TEMPLATABLE_VALUE_(type, name)
/// Primary TemplatableValue: stores either a constant value or a function pointer.
/// No std::function, no string-specific paths. 8 bytes on 32-bit.
/// Accepts raw constants for backward compatibility with direct C++ usage.
template<typename T, typename... X> class TemplatableValue { template<typename T, typename... X> class TemplatableValue {
// For std::string, store pointer to heap-allocated string to keep union pointer-sized. public:
// For other types, store value inline. TemplatableValue() = default;
static constexpr bool USE_HEAP_STORAGE = std::same_as<T, std::string>; TemplatableValue(std::nullptr_t) = delete;
// Accept raw constants
template<typename V> TemplatableValue(V value) requires(!std::invocable<V, X...>) : tag_(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->storage_.f_ = f;
}
// Convertible return type (e.g., int -> uint8_t) — casting trampoline
template<typename F>
[[deprecated("Lambda return type does not match TemplatableValue<T> — use the correct type in "
"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->storage_.f_ = [](X... x) -> T { return static_cast<T>(F{}(x...)); };
}
// Reject any callable that didn't match the above
template<typename F>
TemplatableValue(F) requires std::invocable<F, X...> &&
(!std::convertible_to<F, T (*)(X...)>) &&(!std::is_empty_v<F> ||
!std::convertible_to<std::invoke_result_t<F, X...>, T> ||
!std::default_initializable<F>) = delete;
TemplatableValue(const TemplatableValue &other) : tag_(other.tag_) {
if (this->tag_ == VALUE) {
new (&this->storage_.value_) T(other.storage_.value_);
} else if (this->tag_ == FN) {
this->storage_.f_ = other.storage_.f_;
}
}
TemplatableValue(TemplatableValue &&other) noexcept : tag_(other.tag_) {
if (this->tag_ == VALUE) {
new (&this->storage_.value_) T(std::move(other.storage_.value_));
other.destroy_();
} else if (this->tag_ == FN) {
this->storage_.f_ = other.storage_.f_;
}
other.tag_ = NONE;
}
TemplatableValue &operator=(const TemplatableValue &other) {
if (this != &other) {
this->destroy_();
this->tag_ = other.tag_;
if (this->tag_ == VALUE) {
new (&this->storage_.value_) T(other.storage_.value_);
} else if (this->tag_ == FN) {
this->storage_.f_ = other.storage_.f_;
}
}
return *this;
}
TemplatableValue &operator=(TemplatableValue &&other) noexcept {
if (this != &other) {
this->destroy_();
this->tag_ = other.tag_;
if (this->tag_ == VALUE) {
new (&this->storage_.value_) T(std::move(other.storage_.value_));
other.destroy_();
} else if (this->tag_ == FN) {
this->storage_.f_ = other.storage_.f_;
}
other.tag_ = NONE;
}
return *this;
}
~TemplatableValue() { this->destroy_(); }
bool has_value() const { return this->tag_ != NONE; }
T value(X... x) const {
if (this->tag_ == FN)
return this->storage_.f_(x...);
if (this->tag_ == VALUE)
return this->storage_.value_;
return T{};
}
optional<T> optional_value(X... x) const {
if (this->tag_ == NONE)
return {};
return this->value(x...);
}
T value_or(X... x, T default_value) const {
if (this->tag_ == NONE)
return default_value;
return this->value(x...);
}
protected:
void destroy_() {
if constexpr (!std::is_trivially_destructible_v<T>) {
if (this->tag_ == VALUE)
this->storage_.value_.~T();
}
}
enum Tag : uint8_t { NONE, VALUE, FN } tag_{NONE};
// 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,
/// stateless lambdas, and stateful lambdas (std::function).
template<typename... X> class TemplatableValue<std::string, X...> {
public: public:
TemplatableValue() : type_(NONE) {} TemplatableValue() : type_(NONE) {}
// For const char* when T is std::string: store pointer directly, no heap allocation // For const char*: store pointer directly, no heap allocation.
// String remains in flash and is only converted to std::string when value() is called // String remains in flash and is only converted to std::string when value() is called.
TemplatableValue(const char *str) requires std::same_as<T, std::string> : type_(STATIC_STRING) { TemplatableValue(const char *str) : type_(STATIC_STRING) { this->static_str_ = str; }
this->static_str_ = str;
}
#ifdef USE_ESP8266 #ifdef USE_ESP8266
// On ESP8266, __FlashStringHelper* is a distinct type from const char*. // On ESP8266, __FlashStringHelper* is a distinct type from const char*.
// ESPHOME_F(s) expands to F(s) which returns __FlashStringHelper* pointing to PROGMEM. // 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 // Store as FLASH_STRING — value()/is_empty()/ref_or_copy_to() use _P functions.
// to access the PROGMEM pointer safely. TemplatableValue(const __FlashStringHelper *str) : type_(FLASH_STRING) {
TemplatableValue(const __FlashStringHelper *str) requires std::same_as<T, std::string> : type_(FLASH_STRING) {
this->static_str_ = reinterpret_cast<const char *>(str); this->static_str_ = reinterpret_cast<const char *>(str);
} }
#endif #endif
template<typename F> TemplatableValue(F value) requires(!std::invocable<F, X...>) : type_(VALUE) { template<typename F> TemplatableValue(F value) requires(!std::invocable<F, X...>) : type_(VALUE) {
if constexpr (USE_HEAP_STORAGE) { this->value_ = new std::string(std::move(value));
this->value_ = new T(std::move(value));
} else {
new (&this->value_) T(std::move(value));
}
} }
// For stateless lambdas (convertible to function pointer): use function pointer // For stateless lambdas (convertible to function pointer): use function pointer
template<typename F> template<typename F>
TemplatableValue(F f) requires std::invocable<F, X...> && std::convertible_to<F, T (*)(X...)> TemplatableValue(F f) requires std::invocable<F, X...> && std::convertible_to<F, std::string (*)(X...)>
: type_(STATELESS_LAMBDA) { : type_(STATELESS_LAMBDA) {
this->stateless_f_ = f; // Implicit conversion to function pointer this->stateless_f_ = f; // Implicit conversion to function pointer
} }
// For stateful lambdas (not convertible to function pointer): use std::function // For stateful lambdas (not convertible to function pointer): use std::function
template<typename F> template<typename F>
TemplatableValue(F f) requires std::invocable<F, X...> &&(!std::convertible_to<F, T (*)(X...)>) : type_(LAMBDA) { TemplatableValue(F f) requires std::invocable<F, X...> &&(!std::convertible_to<F, std::string (*)(X...)>)
this->f_ = new std::function<T(X...)>(std::move(f)); : type_(LAMBDA) {
this->f_ = new std::function<std::string(X...)>(std::move(f));
} }
// Copy constructor // Copy constructor
TemplatableValue(const TemplatableValue &other) : type_(other.type_) { TemplatableValue(const TemplatableValue &other) : type_(other.type_) {
if (this->type_ == VALUE) { if (this->type_ == VALUE) {
if constexpr (USE_HEAP_STORAGE) { this->value_ = new std::string(*other.value_);
this->value_ = new T(*other.value_);
} else {
new (&this->value_) T(other.value_);
}
} else if (this->type_ == LAMBDA) { } else if (this->type_ == LAMBDA) {
this->f_ = new std::function<T(X...)>(*other.f_); this->f_ = new std::function<std::string(X...)>(*other.f_);
} else if (this->type_ == STATELESS_LAMBDA) { } else if (this->type_ == STATELESS_LAMBDA) {
this->stateless_f_ = other.stateless_f_; this->stateless_f_ = other.stateless_f_;
} else if (this->type_ == STATIC_STRING || this->type_ == FLASH_STRING) { } else if (this->type_ == STATIC_STRING || this->type_ == FLASH_STRING) {
@@ -108,12 +274,8 @@ template<typename T, typename... X> class TemplatableValue {
// Move constructor // Move constructor
TemplatableValue(TemplatableValue &&other) noexcept : type_(other.type_) { TemplatableValue(TemplatableValue &&other) noexcept : type_(other.type_) {
if (this->type_ == VALUE) { if (this->type_ == VALUE) {
if constexpr (USE_HEAP_STORAGE) { this->value_ = other.value_;
this->value_ = other.value_; other.value_ = nullptr;
other.value_ = nullptr;
} else {
new (&this->value_) T(std::move(other.value_));
}
} else if (this->type_ == LAMBDA) { } else if (this->type_ == LAMBDA) {
this->f_ = other.f_; this->f_ = other.f_;
other.f_ = nullptr; other.f_ = nullptr;
@@ -144,11 +306,7 @@ template<typename T, typename... X> class TemplatableValue {
~TemplatableValue() { ~TemplatableValue() {
if (this->type_ == VALUE) { if (this->type_ == VALUE) {
if constexpr (USE_HEAP_STORAGE) { delete this->value_;
delete this->value_;
} else {
this->value_.~T();
}
} else if (this->type_ == LAMBDA) { } else if (this->type_ == LAMBDA) {
delete this->f_; delete this->f_;
} }
@@ -157,53 +315,40 @@ template<typename T, typename... X> class TemplatableValue {
bool has_value() const { return this->type_ != NONE; } bool has_value() const { return this->type_ != NONE; }
T value(X... x) const { std::string value(X... x) const {
switch (this->type_) { switch (this->type_) {
case STATELESS_LAMBDA: case STATELESS_LAMBDA:
return this->stateless_f_(x...); // Direct function pointer call return this->stateless_f_(x...); // Direct function pointer call
case LAMBDA: case LAMBDA:
return (*this->f_)(x...); // std::function call return (*this->f_)(x...); // std::function call
case VALUE: case VALUE:
if constexpr (USE_HEAP_STORAGE) { return *this->value_;
return *this->value_;
} else {
return this->value_;
}
case STATIC_STRING: case STATIC_STRING:
// if constexpr required: code must compile for all T, but STATIC_STRING return std::string(this->static_str_);
// can only be set when T is std::string (enforced by constructor constraint)
if constexpr (std::same_as<T, std::string>) {
return std::string(this->static_str_);
}
__builtin_unreachable();
#ifdef USE_ESP8266 #ifdef USE_ESP8266
case FLASH_STRING: case FLASH_STRING: {
// PROGMEM pointer — must use _P functions to access on ESP8266 // PROGMEM pointer — must use _P functions to access on ESP8266
if constexpr (std::same_as<T, std::string>) { size_t len = strlen_P(this->static_str_);
size_t len = strlen_P(this->static_str_); std::string result(len, '\0');
std::string result(len, '\0'); memcpy_P(result.data(), this->static_str_, len);
memcpy_P(result.data(), this->static_str_, len); return result;
return result; }
}
__builtin_unreachable();
#endif #endif
case NONE: case NONE:
default: default:
return T{}; return {};
} }
} }
optional<T> optional_value(X... x) { optional<std::string> optional_value(X... x) const {
if (!this->has_value()) { if (!this->has_value())
return {}; return {};
}
return this->value(x...); return this->value(x...);
} }
T value_or(X... x, T default_value) { std::string value_or(X... x, std::string default_value) const {
if (!this->has_value()) { if (!this->has_value())
return default_value; return default_value;
}
return this->value(x...); return this->value(x...);
} }
@@ -216,10 +361,10 @@ template<typename T, typename... X> class TemplatableValue {
/// The pointer is always directly readable — FLASH_STRING uses a separate type. /// The pointer is always directly readable — FLASH_STRING uses a separate type.
const char *get_static_string() const { return this->static_str_; } const char *get_static_string() const { return this->static_str_; }
/// Check if the string value is empty without allocating (for std::string specialization). /// Check if the string value is empty without allocating.
/// For NONE, returns true. For STATIC_STRING/VALUE, checks without allocation. /// For NONE, returns true. For STATIC_STRING/VALUE, checks without allocation.
/// For LAMBDA/STATELESS_LAMBDA, must call value() which may allocate. /// For LAMBDA/STATELESS_LAMBDA, must call value() which may allocate.
bool is_empty() const requires std::same_as<T, std::string> { bool is_empty() const {
switch (this->type_) { switch (this->type_) {
case NONE: case NONE:
return true; return true;
@@ -245,7 +390,7 @@ template<typename T, typename... X> class TemplatableValue {
/// @param lambda_buf Buffer used only for copy cases (must remain valid while StringRef is used). /// @param lambda_buf Buffer used only for copy cases (must remain valid while StringRef is used).
/// @param lambda_buf_size Size of the buffer. /// @param lambda_buf_size Size of the buffer.
/// @return StringRef pointing to the string data. /// @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<T, std::string> { StringRef ref_or_copy_to(char *lambda_buf, size_t lambda_buf_size) const {
switch (this->type_) { switch (this->type_) {
case NONE: case NONE:
return StringRef(); return StringRef();
@@ -278,22 +423,20 @@ template<typename T, typename... X> class TemplatableValue {
} }
} }
protected : enum : uint8_t { protected:
NONE, enum : uint8_t {
VALUE, NONE,
LAMBDA, VALUE,
STATELESS_LAMBDA, LAMBDA,
STATIC_STRING, // For const char* when T is std::string - avoids heap allocation STATELESS_LAMBDA,
FLASH_STRING, // PROGMEM pointer on ESP8266; never set on other platforms STATIC_STRING, // For const char* — avoids heap allocation
} type_; FLASH_STRING, // PROGMEM pointer on ESP8266; never set on other platforms
// For std::string, use heap pointer to minimize union size (4 bytes vs 12+). } type_;
// For other types, store value inline as before.
using ValueStorage = std::conditional_t<USE_HEAP_STORAGE, T *, T>;
union { union {
ValueStorage value_; // T for inline storage, T* for heap storage std::string *value_; // Heap-allocated string (VALUE)
std::function<T(X...)> *f_; std::function<std::string(X...)> *f_; // Heap-allocated std::function (LAMBDA)
T (*stateless_f_)(X...); std::string (*stateless_f_)(X...); // Function pointer (STATELESS_LAMBDA)
const char *static_str_; // For STATIC_STRING and FLASH_STRING types const char *static_str_; // For STATIC_STRING and FLASH_STRING types
}; };
}; };

View File

@@ -819,11 +819,17 @@ async def templatable(
args: list[tuple[SafeExpType, str]], args: list[tuple[SafeExpType, str]],
output_type: SafeExpType | None, output_type: SafeExpType | None,
to_exp: Callable | dict = None, to_exp: Callable | dict = None,
*,
wrap_constant: bool = False,
): ):
"""Generate code for a templatable config option. """Generate code for a templatable config option.
If `value` is a templated value, the lambda expression is returned. 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),
using the std::string-specific TemplatableValue specialization.
For all other output types, constants are wrapped in stateless lambdas
so that TemplatableFn-backed macro-generated fields can store them as
function pointers.
:param value: The value to process. :param value: The value to process.
:param args: The arguments for the lambda expression. :param args: The arguments for the lambda expression.
@@ -833,20 +839,28 @@ async def templatable(
""" """
if is_template(value): if is_template(value):
return await process_lambda(value, args, return_type=output_type) return await process_lambda(value, args, return_type=output_type)
if to_exp is None: # Late import to avoid circular dependency (cpp_generator <-> cpp_types).
from esphome.cpp_types import std_string
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 and output_type is std_string
):
# Automatically wrap static strings in ESPHOME_F() for PROGMEM storage on ESP8266. # Automatically wrap static strings in ESPHOME_F() for PROGMEM storage on ESP8266.
# On other platforms ESPHOME_F() is a no-op returning const char*. # On other platforms ESPHOME_F() is a no-op returning const char*.
# Lazy import to avoid circular dependency (cpp_generator <-> cpp_types). return FlashStringLiteral(value)
# Identity check (is) avoids brittle string comparison. # Wrap non-string constants in stateless lambdas so that TemplatableFn
if isinstance(value, str) and output_type is not None: # (used by TEMPLATABLE_VALUE macro) stores them as function pointers.
from esphome.cpp_types import std_string # wrap_constant=True forces wrapping even with output_type=None (compiler deduces type).
if (output_type is not None or wrap_constant) and output_type is not std_string:
if output_type is std_string: return LambdaExpression(
return FlashStringLiteral(value) f"return {safe_exp(value)};",
return value args,
if isinstance(to_exp, dict): capture="",
return to_exp[value] return_type=output_type,
return to_exp(value) )
return value
class MockObj(Expression): class MockObj(Expression):

View File

@@ -56,8 +56,8 @@ static void SensorFilter_Chain3(benchmark::State &state) {
Sensor sensor; Sensor sensor;
sensor.add_filters({ sensor.add_filters({
new OffsetFilter(1.0f), new OffsetFilter([]() -> float { return 1.0f; }),
new MultiplyFilter(2.0f), new MultiplyFilter([]() -> float { return 2.0f; }),
new SlidingWindowMovingAverageFilter(5, 1, 1), new SlidingWindowMovingAverageFilter(5, 1, 1),
}); });

View File

@@ -669,11 +669,11 @@ async def test_templatable__int_with_std_string() -> None:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_templatable__string_with_non_string_output_type() -> None: 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_) result = await cg.templatable("hello", [], ct.bool_)
assert isinstance(result, str) assert isinstance(result, cg.LambdaExpression)
assert result == "hello" assert result.capture == ""
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -684,6 +684,15 @@ async def test_templatable__with_to_exp_callable() -> None:
assert result == 84 assert result == 84
@pytest.mark.asyncio
async def test_templatable__with_to_exp_callable_and_output_type() -> None:
"""When to_exp is provided with non-string output_type, result is lambda-wrapped."""
result = await cg.templatable(42, [], ct.int_, to_exp=lambda x: x * 2)
assert isinstance(result, cg.LambdaExpression)
assert result.capture == ""
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_templatable__with_to_exp_dict() -> None: 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."""