[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:
sensor::Sensor *sensor_{nullptr};
TemplatableValue<float> upper_threshold_{};
TemplatableValue<float> lower_threshold_{};
TemplatableFn<float> upper_threshold_{};
TemplatableFn<float> lower_threshold_{};
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:
APIServer *parent_;
TemplatableValue<bool, Ts...> success_{true};
TemplatableFn<bool, Ts...> success_{[](Ts...) -> bool { return true; }};
TemplatableValue<std::string, Ts...> error_message_{""};
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
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; }
protected:
TemplatableValue<uint32_t> timeout_delay_{};
TemplatableFn<uint32_t> timeout_delay_{};
};
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; }
protected:
TemplatableValue<uint32_t> on_delay_{};
TemplatableValue<uint32_t> off_delay_{};
TemplatableFn<uint32_t> on_delay_{};
TemplatableFn<uint32_t> off_delay_{};
};
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; }
protected:
TemplatableValue<uint32_t> delay_{};
TemplatableFn<uint32_t> delay_{};
};
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; }
protected:
TemplatableValue<uint32_t> delay_{};
TemplatableFn<uint32_t> delay_{};
};
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; }
protected:
TemplatableValue<uint32_t> delay_{};
TemplatableFn<uint32_t> delay_{};
bool steady_{true};
};

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(

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

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

View File

@@ -378,7 +378,8 @@ async def esp32_ble_tracker_start_scan_action_to_code(
):
paren = await cg.get_variable(config[CONF_ID])
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

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)
var = cg.new_Pvariable(action_id, template_arg, paren)
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))
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)
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]
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(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
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});
}
@@ -560,7 +560,7 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
}
}
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"};
FixedVector<std::pair<const char *, TemplatableValue<std::string, Ts...>>> json_{};
std::function<void(Ts..., JsonObject)> json_func_{nullptr};

View File

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

View File

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

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...);

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);
}
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) {
if (!this->is_idle_ && idle_time > this->timeout_.value()) {
this->is_idle_ = true;

View File

@@ -284,10 +284,10 @@ class LvglComponent : public PollingComponent {
class IdleTrigger : public Trigger<> {
public:
explicit IdleTrigger(LvglComponent *parent, TemplatableValue<uint32_t> timeout);
explicit IdleTrigger(LvglComponent *parent, TemplatableFn<uint32_t> timeout);
protected:
TemplatableValue<uint32_t> timeout_;
TemplatableFn<uint32_t> timeout_;
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):
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

View File

@@ -13,6 +13,7 @@ from esphome.const import (
)
from esphome.core import CORE, Lambda, coroutine_with_priority
from esphome.coroutine import CoroPriority
from esphome.cpp_generator import LambdaExpression
from esphome.types import ConfigType
CODEOWNERS = ["@esphome/core"]
@@ -131,6 +132,12 @@ def mdns_service(
Returns:
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(
MDNSService,
("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();
service.service_type = MDNS_STR(SERVICE_ESPHOMELIB);
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();
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();
prom_service.service_type = MDNS_STR(SERVICE_PROMETHEUS);
prom_service.proto = MDNS_STR(SERVICE_TCP);
prom_service.port = USE_WEBSERVER_PORT;
prom_service.port = []() -> uint16_t { return USE_WEBSERVER_PORT; };
#endif
#ifdef USE_SENDSPIN
@@ -162,7 +162,7 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
auto &sendspin_service = services.emplace_next();
sendspin_service.service_type = MDNS_STR(SERVICE_SENDSPIN);
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)}};
#endif
@@ -172,7 +172,7 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
auto &web_service = services.emplace_next();
web_service.service_type = MDNS_STR(SERVICE_HTTP);
web_service.proto = MDNS_STR(SERVICE_TCP);
web_service.port = USE_WEBSERVER_PORT;
web_service.port = []() -> uint16_t { return USE_WEBSERVER_PORT; };
#endif
#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();
fallback_service.service_type = MDNS_STR(SERVICE_HTTP);
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)}};
#endif
}
@@ -199,7 +199,7 @@ void MDNSComponent::dump_config() {
ESP_LOGV(TAG, " Services:");
for (const auto &service : this->services_) {
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) {
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
// as defined in RFC6763 Section 7, like "_tcp" or "_udp"
const MDNSString *proto;
TemplatableValue<uint16_t> port;
TemplatableFn<uint16_t> port;
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].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,
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) == '_') {
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);
for (const auto &record : service.txt_records) {
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 == '_') {
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_);
for (const auto &record : service.txt_records) {
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 == '_') {
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);
for (const auto &record : service.txt_records) {
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)
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

View File

@@ -63,8 +63,8 @@ class ValueRangeTrigger : public Trigger<float>, public Component {
Number *parent_;
ESPPreferenceObject rtc_;
bool previous_in_range_{false};
TemplatableValue<float, float> min_{NAN};
TemplatableValue<float, float> max_{NAN};
TemplatableFn<float, float> min_{[](float) -> float { return NAN; }};
TemplatableFn<float, float> max_{[](float) -> float { return NAN; }};
};
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);
// Set port
entry->mService.mPort = const_cast<TemplatableValue<uint16_t> &>(service.port).value();
entry->mService.mPort = service.port.value();
otDnsTxtEntry *txt_entries =
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)
)
)
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:
cg.add(
var.set_message_id(
@@ -2231,3 +2232,9 @@ async def Toto_action(var, config, args):
cg.add(var.set_rc_code_2(template_))
template_ = await cg.templatable(config[CONF_COMMAND], args, cg.uint8)
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_2 = this->rc_code_2_.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);
}
};

View File

@@ -211,7 +211,7 @@ template<class... As, typename... Ts> class ScriptExecuteAction<Script<As...>, T
public:
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...}; }

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

View File

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

View File

@@ -213,17 +213,17 @@ optional<float> LambdaFilter::new_value(float value) {
}
// 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(); }
// 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(); }
// 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();
float accuracy_mult = pow10_int(accuracy);
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)
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) {
const uint32_t now = App.get_loop_component_start_time();
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.
class OffsetFilter : public Filter {
public:
explicit OffsetFilter(TemplatableValue<float> offset);
explicit OffsetFilter(TemplatableFn<float> offset);
optional<float> new_value(float value) override;
protected:
TemplatableValue<float> offset_;
TemplatableFn<float> offset_;
};
/// A simple filter that multiplies to each value it receives by `multiplier`.
class MultiplyFilter : public Filter {
public:
explicit MultiplyFilter(TemplatableValue<float> multiplier);
explicit MultiplyFilter(TemplatableFn<float> multiplier);
optional<float> new_value(float value) override;
protected:
TemplatableValue<float> multiplier_;
TemplatableFn<float> multiplier_;
};
/// 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.
*
@@ -342,7 +342,7 @@ bool value_list_matches_any(Sensor *parent, float sensor_value, const Templatabl
*/
template<size_t N> class ValueListFilter : public Filter {
protected:
explicit ValueListFilter(std::initializer_list<TemplatableValue<float>> values) {
explicit ValueListFilter(std::initializer_list<TemplatableFn<float>> 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);
}
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`.
template<size_t N> class FilterOutValueFilter : public ValueListFilter<N> {
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) {}
optional<float> new_value(float value) override {
@@ -379,14 +379,14 @@ class ThrottleFilter : public Filter {
};
/// 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);
/// Same as 'throttle' but will immediately publish values contained in `value_to_prioritize`.
template<size_t N> class ThrottleWithPriorityFilter : public ValueListFilter<N> {
public:
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) {}
optional<float> new_value(float value) override {
@@ -430,15 +430,15 @@ class TimeoutFilterLast : public TimeoutFilterBase {
// Timeout filter with configured value - evaluates TemplatableValue after timeout
class TimeoutFilterConfigured : public TimeoutFilterBase {
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) {}
optional<float> new_value(float value) override;
protected:
float get_output_value() override { return this->value_.value(); }
TemplatableValue<float> value_; // 16 bytes (configured output value, can be lambda)
// Total: 8 (base) + 16 = 24 bytes + vtable ptr + Component overhead
TemplatableFn<float> value_; // 4 bytes (configured output value, can be lambda)
// Total: 8 (base) + 4 = 12 bytes + vtable ptr + Component overhead
};
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_)
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

View File

@@ -312,7 +312,8 @@ async def set_playlist_delay_action_to_code(
parent = await cg.get_variable(config[CONF_ID])
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)
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):
paren = await cg.get_variable(config[CONF_ID])
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_))
template_ = await cg.templatable(config[CONF_RUN_DURATION], args, cg.uint32)
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])
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_))
template_ = await cg.templatable(config[CONF_RUN_DURATION], args, cg.uint32)
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):
paren = await cg.get_variable(config[CONF_ID])
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_))
if CONF_RUN_DURATION in config:
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:
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)
void play(const Ts &...x) override {
@@ -118,6 +119,7 @@ template<typename... Ts> class StartSingleValveAction : public Action<Ts...> {
protected:
Sprinkler *sprinkler_;
TemplatableValue<size_t, Ts...> valve_to_start_{};
};
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
// 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) \
protected: \
TemplatableValue<type, Ts...> name##_{}; \
TemplatableStorage<type, Ts...> name##_{}; \
\
public: \
template<typename V> void set_##name(V name) { this->name##_ = 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 {
// For std::string, store pointer to heap-allocated string to keep union pointer-sized.
// For other types, store value inline.
static constexpr bool USE_HEAP_STORAGE = std::same_as<T, std::string>;
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->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:
TemplatableValue() : type_(NONE) {}
// For const char* when T is std::string: store pointer directly, no heap allocation
// 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) {
this->static_str_ = str;
}
// For const char*: store pointer directly, no heap allocation.
// String remains in flash and is only converted to std::string when value() is called.
TemplatableValue(const char *str) : type_(STATIC_STRING) { this->static_str_ = str; }
#ifdef USE_ESP8266
// On ESP8266, __FlashStringHelper* is a distinct type from const char*.
// 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
// to access the PROGMEM pointer safely.
TemplatableValue(const __FlashStringHelper *str) requires std::same_as<T, std::string> : type_(FLASH_STRING) {
// Store as FLASH_STRING — value()/is_empty()/ref_or_copy_to() use _P functions.
TemplatableValue(const __FlashStringHelper *str) : type_(FLASH_STRING) {
this->static_str_ = reinterpret_cast<const char *>(str);
}
#endif
template<typename F> TemplatableValue(F value) requires(!std::invocable<F, X...>) : type_(VALUE) {
if constexpr (USE_HEAP_STORAGE) {
this->value_ = new T(std::move(value));
} else {
new (&this->value_) T(std::move(value));
}
this->value_ = new std::string(std::move(value));
}
// For stateless lambdas (convertible to function pointer): use function pointer
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) {
this->stateless_f_ = f; // Implicit conversion to function pointer
}
// For stateful lambdas (not convertible to function pointer): use std::function
template<typename F>
TemplatableValue(F f) requires std::invocable<F, X...> &&(!std::convertible_to<F, T (*)(X...)>) : type_(LAMBDA) {
this->f_ = new std::function<T(X...)>(std::move(f));
TemplatableValue(F f) requires std::invocable<F, X...> &&(!std::convertible_to<F, std::string (*)(X...)>)
: type_(LAMBDA) {
this->f_ = new std::function<std::string(X...)>(std::move(f));
}
// Copy constructor
TemplatableValue(const TemplatableValue &other) : type_(other.type_) {
if (this->type_ == VALUE) {
if constexpr (USE_HEAP_STORAGE) {
this->value_ = new T(*other.value_);
} else {
new (&this->value_) T(other.value_);
}
this->value_ = new std::string(*other.value_);
} 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) {
this->stateless_f_ = other.stateless_f_;
} else if (this->type_ == STATIC_STRING || this->type_ == FLASH_STRING) {
@@ -108,12 +274,8 @@ template<typename T, typename... X> class TemplatableValue {
// Move constructor
TemplatableValue(TemplatableValue &&other) noexcept : type_(other.type_) {
if (this->type_ == VALUE) {
if constexpr (USE_HEAP_STORAGE) {
this->value_ = other.value_;
other.value_ = nullptr;
} else {
new (&this->value_) T(std::move(other.value_));
}
this->value_ = other.value_;
other.value_ = nullptr;
} else if (this->type_ == LAMBDA) {
this->f_ = other.f_;
other.f_ = nullptr;
@@ -144,11 +306,7 @@ template<typename T, typename... X> class TemplatableValue {
~TemplatableValue() {
if (this->type_ == VALUE) {
if constexpr (USE_HEAP_STORAGE) {
delete this->value_;
} else {
this->value_.~T();
}
delete this->value_;
} else if (this->type_ == LAMBDA) {
delete this->f_;
}
@@ -157,53 +315,40 @@ template<typename T, typename... X> class TemplatableValue {
bool has_value() const { return this->type_ != NONE; }
T value(X... x) const {
std::string value(X... x) const {
switch (this->type_) {
case STATELESS_LAMBDA:
return this->stateless_f_(x...); // Direct function pointer call
case LAMBDA:
return (*this->f_)(x...); // std::function call
case VALUE:
if constexpr (USE_HEAP_STORAGE) {
return *this->value_;
} else {
return this->value_;
}
return *this->value_;
case STATIC_STRING:
// if constexpr required: code must compile for all T, but STATIC_STRING
// 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();
return std::string(this->static_str_);
#ifdef USE_ESP8266
case FLASH_STRING:
case FLASH_STRING: {
// 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_);
std::string result(len, '\0');
memcpy_P(result.data(), this->static_str_, len);
return result;
}
__builtin_unreachable();
size_t len = strlen_P(this->static_str_);
std::string result(len, '\0');
memcpy_P(result.data(), this->static_str_, len);
return result;
}
#endif
case NONE:
default:
return T{};
return {};
}
}
optional<T> optional_value(X... x) {
if (!this->has_value()) {
optional<std::string> optional_value(X... x) const {
if (!this->has_value())
return {};
}
return this->value(x...);
}
T value_or(X... x, T default_value) {
if (!this->has_value()) {
std::string value_or(X... x, std::string default_value) const {
if (!this->has_value())
return default_value;
}
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.
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 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_) {
case NONE:
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_size Size of the buffer.
/// @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_) {
case NONE:
return StringRef();
@@ -278,22 +423,20 @@ template<typename T, typename... X> class TemplatableValue {
}
}
protected : enum : uint8_t {
NONE,
VALUE,
LAMBDA,
STATELESS_LAMBDA,
STATIC_STRING, // For const char* when T is std::string - avoids heap allocation
FLASH_STRING, // PROGMEM pointer on ESP8266; never set on other platforms
} type_;
// For std::string, use heap pointer to minimize union size (4 bytes vs 12+).
// For other types, store value inline as before.
using ValueStorage = std::conditional_t<USE_HEAP_STORAGE, T *, T>;
protected:
enum : uint8_t {
NONE,
VALUE,
LAMBDA,
STATELESS_LAMBDA,
STATIC_STRING, // For const char* — avoids heap allocation
FLASH_STRING, // PROGMEM pointer on ESP8266; never set on other platforms
} type_;
union {
ValueStorage value_; // T for inline storage, T* for heap storage
std::function<T(X...)> *f_;
T (*stateless_f_)(X...);
const char *static_str_; // For STATIC_STRING and FLASH_STRING types
std::string *value_; // Heap-allocated string (VALUE)
std::function<std::string(X...)> *f_; // Heap-allocated std::function (LAMBDA)
std::string (*stateless_f_)(X...); // Function pointer (STATELESS_LAMBDA)
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]],
output_type: SafeExpType | None,
to_exp: Callable | dict = None,
*,
wrap_constant: bool = False,
):
"""Generate code for a templatable config option.
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 args: The arguments for the lambda expression.
@@ -833,20 +839,28 @@ async def templatable(
"""
if is_template(value):
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.
# On other platforms ESPHOME_F() is a no-op returning const char*.
# Lazy import to avoid circular dependency (cpp_generator <-> cpp_types).
# Identity check (is) avoids brittle string comparison.
if isinstance(value, str) and output_type is not None:
from esphome.cpp_types import std_string
if output_type is std_string:
return FlashStringLiteral(value)
return value
if isinstance(to_exp, dict):
return to_exp[value]
return to_exp(value)
return FlashStringLiteral(value)
# Wrap non-string constants in stateless lambdas so that TemplatableFn
# (used by TEMPLATABLE_VALUE macro) stores them as function pointers.
# 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:
return LambdaExpression(
f"return {safe_exp(value)};",
args,
capture="",
return_type=output_type,
)
return value
class MockObj(Expression):

View File

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

View File

@@ -669,11 +669,11 @@ async def test_templatable__int_with_std_string() -> None:
@pytest.mark.asyncio
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_)
assert isinstance(result, str)
assert result == "hello"
assert isinstance(result, cg.LambdaExpression)
assert result.capture == ""
@pytest.mark.asyncio
@@ -684,6 +684,15 @@ async def test_templatable__with_to_exp_callable() -> None:
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
async def test_templatable__with_to_exp_dict() -> None:
"""When to_exp is a dict, value is looked up."""