diff --git a/esphome/components/analog_threshold/analog_threshold_binary_sensor.h b/esphome/components/analog_threshold/analog_threshold_binary_sensor.h index dd70768105..55a822b9b0 100644 --- a/esphome/components/analog_threshold/analog_threshold_binary_sensor.h +++ b/esphome/components/analog_threshold/analog_threshold_binary_sensor.h @@ -19,8 +19,8 @@ class AnalogThresholdBinarySensor : public Component, public binary_sensor::Bina protected: sensor::Sensor *sensor_{nullptr}; - TemplatableValue upper_threshold_{}; - TemplatableValue lower_threshold_{}; + TemplatableFn upper_threshold_{}; + TemplatableFn lower_threshold_{}; bool raw_state_{false}; // Pre-filter state for hysteresis logic }; diff --git a/esphome/components/api/user_services.h b/esphome/components/api/user_services.h index d1b8a6ef0d..29eadda927 100644 --- a/esphome/components/api/user_services.h +++ b/esphome/components/api/user_services.h @@ -275,7 +275,7 @@ template class APIRespondAction : public Action { protected: APIServer *parent_; - TemplatableValue success_{true}; + TemplatableFn success_{[](Ts...) -> bool { return true; }}; TemplatableValue error_message_{""}; #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON std::function json_builder_; diff --git a/esphome/components/binary_sensor/filter.h b/esphome/components/binary_sensor/filter.h index 37c6bf0092..2e45554f81 100644 --- a/esphome/components/binary_sensor/filter.h +++ b/esphome/components/binary_sensor/filter.h @@ -36,7 +36,7 @@ class TimeoutFilter : public Filter, public Component { template void set_timeout_value(T timeout) { this->timeout_delay_ = timeout; } protected: - TemplatableValue timeout_delay_{}; + TemplatableFn timeout_delay_{}; }; class DelayedOnOffFilter final : public Filter, public Component { @@ -49,8 +49,8 @@ class DelayedOnOffFilter final : public Filter, public Component { template void set_off_delay(T delay) { this->off_delay_ = delay; } protected: - TemplatableValue on_delay_{}; - TemplatableValue off_delay_{}; + TemplatableFn on_delay_{}; + TemplatableFn off_delay_{}; }; class DelayedOnFilter : public Filter, public Component { @@ -62,7 +62,7 @@ class DelayedOnFilter : public Filter, public Component { template void set_delay(T delay) { this->delay_ = delay; } protected: - TemplatableValue delay_{}; + TemplatableFn delay_{}; }; class DelayedOffFilter : public Filter, public Component { @@ -74,7 +74,7 @@ class DelayedOffFilter : public Filter, public Component { template void set_delay(T delay) { this->delay_ = delay; } protected: - TemplatableValue delay_{}; + TemplatableFn delay_{}; }; class InvertFilter : public Filter { @@ -155,7 +155,7 @@ class SettleFilter : public Filter, public Component { template void set_delay(T delay) { this->delay_ = delay; } protected: - TemplatableValue delay_{}; + TemplatableFn delay_{}; bool steady_{true}; }; diff --git a/esphome/components/cc1101/__init__.py b/esphome/components/cc1101/__init__.py index 2709290862..0feb384ac2 100644 --- a/esphome/components/cc1101/__init__.py +++ b/esphome/components/cc1101/__init__.py @@ -423,11 +423,10 @@ def _register_setter_actions(): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) data = config[CONF_VALUE] - if cg.is_template(data): - templ_ = await cg.templatable(data, args, _type) - cg.add(getattr(var, _setter)(templ_)) - else: - cg.add(getattr(var, _setter)(_map[data] if _map else data)) + if _map and not cg.is_template(data): + data = _map[data] + templ_ = await cg.templatable(data, args, _type) + cg.add(getattr(var, _setter)(templ_)) return var automation.register_action( diff --git a/esphome/components/datetime/__init__.py b/esphome/components/datetime/__init__.py index 90835624bf..895ac4e243 100644 --- a/esphome/components/datetime/__init__.py +++ b/esphome/components/datetime/__init__.py @@ -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 diff --git a/esphome/components/display/__init__.py b/esphome/components/display/__init__.py index 67d76a59d9..744b5d16c4 100644 --- a/esphome/components/display/__init__.py +++ b/esphome/components/display/__init__.py @@ -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 diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index b9c4c28ccf..d758b400c4 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -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 diff --git a/esphome/components/globals/__init__.py b/esphome/components/globals/__init__.py index fe83b1ea7c..ec6730a41c 100644 --- a/esphome/components/globals/__init__.py +++ b/esphome/components/globals/__init__.py @@ -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 diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index 416432cfc4..90879c459e 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -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])) diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index 73dbda8694..ae73983bab 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -457,7 +457,7 @@ template class HttpRequestSendAction : public Action { #endif void init_request_headers(size_t count) { this->request_headers_.init(count); } - void add_request_header(const char *key, TemplatableValue value) { + void add_request_header(const char *key, TemplatableFn value) { this->request_headers_.push_back({key, value}); } @@ -560,7 +560,7 @@ template class HttpRequestSendAction : public Action { } } HttpRequestComponent *parent_; - FixedVector>> request_headers_{}; + FixedVector>> request_headers_{}; std::vector lower_case_collect_headers_{"content-type", "content-length"}; FixedVector>> json_{}; std::function json_func_{nullptr}; diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index a5c9220a23..f6a2ca52d4 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -24,51 +24,60 @@ template class ToggleAction : public Action { 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 class LightControlAction : public Action { 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 class DimRelativeAction : public Action { diff --git a/esphome/components/light/automation.py b/esphome/components/light/automation.py index 365a64584c..2400822b31 100644 --- a/esphome/components/light/automation.py +++ b/esphome/components/light/automation.py @@ -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 ; }`` 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 diff --git a/esphome/components/lightwaverf/__init__.py b/esphome/components/lightwaverf/__init__.py index 46c400cb0e..76eabc2b71 100644 --- a/esphome/components/lightwaverf/__init__.py +++ b/esphome/components/lightwaverf/__init__.py @@ -61,15 +61,13 @@ async def send_raw_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - repeats = await cg.templatable(config[CONF_REPEAT], args, int) - inverted = await cg.templatable(config[CONF_INVERTED], args, bool) - pulse_length = await cg.templatable(config[CONF_PULSE_LENGTH], args, int) - code = config[CONF_CODE] - - cg.add(var.set_repeats(repeats)) - cg.add(var.set_inverted(inverted)) - cg.add(var.set_pulse_length(pulse_length)) - cg.add(var.set_data(code)) + template_ = await cg.templatable(config[CONF_REPEAT], args, cg.int_) + cg.add(var.set_repeat(template_)) + template_ = await cg.templatable(config[CONF_INVERTED], args, cg.int_) + cg.add(var.set_inverted(template_)) + template_ = await cg.templatable(config[CONF_PULSE_LENGTH], args, cg.int_) + cg.add(var.set_pulse_length(template_)) + cg.add(var.set_code(config[CONF_CODE])) return var diff --git a/esphome/components/lightwaverf/lightwaverf.h b/esphome/components/lightwaverf/lightwaverf.h index ee4e91e9d1..6210e6b5d4 100644 --- a/esphome/components/lightwaverf/lightwaverf.h +++ b/esphome/components/lightwaverf/lightwaverf.h @@ -45,11 +45,7 @@ template class SendRawAction : public Action { TEMPLATABLE_VALUE(int, inverted); TEMPLATABLE_VALUE(int, pulse_length); TEMPLATABLE_VALUE(std::vector, code); - - void set_repeats(const int &data) { repeat_ = data; } - void set_inverted(const int &data) { inverted_ = data; } - void set_pulse_length(const int &data) { pulse_length_ = data; } - void set_data(const std::vector &data) { code_ = data; } + void set_code(std::initializer_list data) { this->code_ = std::vector(data); } void play(const Ts &...x) { int repeats = this->repeat_.value(x...); diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index 0c4e7a3425..ce9b013dcf 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -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 timeout) : timeout_(std::move(timeout)) { +IdleTrigger::IdleTrigger(LvglComponent *parent, TemplatableFn 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; diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 3433aaa527..3ba258b1a2 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -284,10 +284,10 @@ class LvglComponent : public PollingComponent { class IdleTrigger : public Trigger<> { public: - explicit IdleTrigger(LvglComponent *parent, TemplatableValue timeout); + explicit IdleTrigger(LvglComponent *parent, TemplatableFn timeout); protected: - TemplatableValue timeout_; + TemplatableFn timeout_; bool is_idle_{}; }; diff --git a/esphome/components/max7219digit/display.py b/esphome/components/max7219digit/display.py index eb751b995d..df2423b0d0 100644 --- a/esphome/components/max7219digit/display.py +++ b/esphome/components/max7219digit/display.py @@ -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 diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index 0d535d6970..79d355e8ae 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -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)})")), diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index 342a6e6c64..e05373ac5d 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -57,7 +57,7 @@ void MDNSComponent::compile_records_(StaticVectorget_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 uint16_t { return USE_WEBSERVER_PORT; }; #endif #ifdef USE_SENDSPIN @@ -162,7 +162,7 @@ void MDNSComponent::compile_records_(StaticVector 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 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 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 &>(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)); } diff --git a/esphome/components/mdns/mdns_component.h b/esphome/components/mdns/mdns_component.h index 47cad4bf71..adf88a9cf1 100644 --- a/esphome/components/mdns/mdns_component.h +++ b/esphome/components/mdns/mdns_component.h @@ -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 port; + TemplatableFn port; FixedVector txt_records; }; diff --git a/esphome/components/mdns/mdns_esp32.cpp b/esphome/components/mdns/mdns_esp32.cpp index 3e997402bc..17000a2bd7 100644 --- a/esphome/components/mdns/mdns_esp32.cpp +++ b/esphome/components/mdns/mdns_esp32.cpp @@ -37,7 +37,7 @@ static void register_esp32(MDNSComponent *comp, StaticVector &>(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()); diff --git a/esphome/components/mdns/mdns_esp8266.cpp b/esphome/components/mdns/mdns_esp8266.cpp index 295a408cbd..70c614f8d3 100644 --- a/esphome/components/mdns/mdns_esp8266.cpp +++ b/esphome/components/mdns/mdns_esp8266.cpp @@ -27,7 +27,7 @@ static void register_esp8266(MDNSComponent *, StaticVector &>(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)), diff --git a/esphome/components/mdns/mdns_libretiny.cpp b/esphome/components/mdns/mdns_libretiny.cpp index 986099fa1f..a543a3809a 100644 --- a/esphome/components/mdns/mdns_libretiny.cpp +++ b/esphome/components/mdns/mdns_libretiny.cpp @@ -27,7 +27,7 @@ static void register_libretiny(MDNSComponent *, StaticVector &>(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)); diff --git a/esphome/components/mdns/mdns_rp2040.cpp b/esphome/components/mdns/mdns_rp2040.cpp index 88f707afd3..64b603030c 100644 --- a/esphome/components/mdns/mdns_rp2040.cpp +++ b/esphome/components/mdns/mdns_rp2040.cpp @@ -32,7 +32,7 @@ static void register_rp2040(MDNSComponent *, StaticVector &>(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)); diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index 9fbaff6860..c844100258 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -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 diff --git a/esphome/components/number/automation.h b/esphome/components/number/automation.h index a7cd04f083..2843aa6bf5 100644 --- a/esphome/components/number/automation.h +++ b/esphome/components/number/automation.h @@ -63,8 +63,8 @@ class ValueRangeTrigger : public Trigger, public Component { Number *parent_; ESPPreferenceObject rtc_; bool previous_in_range_{false}; - TemplatableValue min_{NAN}; - TemplatableValue max_{NAN}; + TemplatableFn min_{[](float) -> float { return NAN; }}; + TemplatableFn max_{[](float) -> float { return NAN; }}; }; template class NumberInRangeCondition : public Condition { diff --git a/esphome/components/openthread/openthread.cpp b/esphome/components/openthread/openthread.cpp index 7c9a308303..21dad4f867 100644 --- a/esphome/components/openthread/openthread.cpp +++ b/esphome/components/openthread/openthread.cpp @@ -181,7 +181,7 @@ void OpenThreadSrpComponent::setup() { memcpy(string, host_name.c_str(), host_name_len); // Set port - entry->mService.mPort = const_cast &>(service.port).value(); + entry->mService.mPort = service.port.value(); otDnsTxtEntry *txt_entries = reinterpret_cast(this->pool_alloc_(sizeof(otDnsTxtEntry) * service.txt_records.size())); diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 99eda76f81..042ac9d46a 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -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_)) diff --git a/esphome/components/remote_base/toto_protocol.h b/esphome/components/remote_base/toto_protocol.h index 6a635b0f7c..53d453f7e3 100644 --- a/esphome/components/remote_base/toto_protocol.h +++ b/esphome/components/remote_base/toto_protocol.h @@ -35,8 +35,6 @@ template class TotoAction : public RemoteTransmitterActionBaserc_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); } }; diff --git a/esphome/components/script/script.h b/esphome/components/script/script.h index cd1a084f16..a0dffe26bf 100644 --- a/esphome/components/script/script.h +++ b/esphome/components/script/script.h @@ -211,7 +211,7 @@ template class ScriptExecuteAction, T public: ScriptExecuteAction(Script *script) : script_(script) {} - using Args = std::tuple...>; + using Args = std::tuple...>; template void set_args(F... x) { args_ = Args{x...}; } diff --git a/esphome/components/select/__init__.py b/esphome/components/select/__init__.py index b2c17f59ac..8c7c8f00fa 100644 --- a/esphome/components/select/__init__.py +++ b/esphome/components/select/__init__.py @@ -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 diff --git a/esphome/components/sensor/automation.h b/esphome/components/sensor/automation.h index b4de712727..37578f5320 100644 --- a/esphome/components/sensor/automation.h +++ b/esphome/components/sensor/automation.h @@ -79,8 +79,8 @@ class ValueRangeTrigger : public Trigger, public Component { Sensor *parent_; ESPPreferenceObject rtc_; bool previous_in_range_{false}; - TemplatableValue min_{NAN}; - TemplatableValue max_{NAN}; + TemplatableFn min_{[](float) -> float { return NAN; }}; + TemplatableFn max_{[](float) -> float { return NAN; }}; }; template class SensorInRangeCondition : public Condition { diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index 6a90a5af66..fbac7d3535 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -213,17 +213,17 @@ optional LambdaFilter::new_value(float value) { } // OffsetFilter -OffsetFilter::OffsetFilter(TemplatableValue offset) : offset_(std::move(offset)) {} +OffsetFilter::OffsetFilter(TemplatableFn offset) : offset_(offset) {} optional OffsetFilter::new_value(float value) { return value + this->offset_.value(); } // MultiplyFilter -MultiplyFilter::MultiplyFilter(TemplatableValue multiplier) : multiplier_(std::move(multiplier)) {} +MultiplyFilter::MultiplyFilter(TemplatableFn multiplier) : multiplier_(multiplier) {} optional MultiplyFilter::new_value(float value) { return value * this->multiplier_.value(); } // ValueListFilter helper (non-template, shared by all ValueListFilter instantiations) -bool value_list_matches_any(Sensor *parent, float sensor_value, const TemplatableValue *values, size_t count) { +bool value_list_matches_any(Sensor *parent, float sensor_value, const TemplatableFn *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 ThrottleFilter::new_value(float value) { } // ThrottleWithPriorityFilter helper (non-template, keeps App access in .cpp) -optional throttle_with_priority_new_value(Sensor *parent, float value, const TemplatableValue *values, +optional throttle_with_priority_new_value(Sensor *parent, float value, const TemplatableFn *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 || diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index cb4abd154a..0dbbc33ab3 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -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 offset); + explicit OffsetFilter(TemplatableFn offset); optional new_value(float value) override; protected: - TemplatableValue offset_; + TemplatableFn offset_; }; /// A simple filter that multiplies to each value it receives by `multiplier`. class MultiplyFilter : public Filter { public: - explicit MultiplyFilter(TemplatableValue multiplier); + explicit MultiplyFilter(TemplatableFn multiplier); optional new_value(float value) override; protected: - TemplatableValue multiplier_; + TemplatableFn multiplier_; }; /// Non-template helper for value matching (implementation in filter.cpp) -bool value_list_matches_any(Sensor *parent, float sensor_value, const TemplatableValue *values, size_t count); +bool value_list_matches_any(Sensor *parent, float sensor_value, const TemplatableFn *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 class ValueListFilter : public Filter { protected: - explicit ValueListFilter(std::initializer_list> values) { + explicit ValueListFilter(std::initializer_list> values) { init_array_from(this->values_, values); } @@ -351,13 +351,13 @@ template class ValueListFilter : public Filter { return value_list_matches_any(this->parent_, sensor_value, this->values_.data(), N); } - std::array, N> values_{}; + std::array, N> values_{}; }; /// A simple filter that only forwards the filter chain if it doesn't receive `value_to_filter_out`. template class FilterOutValueFilter : public ValueListFilter { public: - explicit FilterOutValueFilter(std::initializer_list> values_to_filter_out) + explicit FilterOutValueFilter(std::initializer_list> values_to_filter_out) : ValueListFilter(values_to_filter_out) {} optional new_value(float value) override { @@ -379,14 +379,14 @@ class ThrottleFilter : public Filter { }; /// Non-template helper for ThrottleWithPriorityFilter (implementation in filter.cpp) -optional throttle_with_priority_new_value(Sensor *parent, float value, const TemplatableValue *values, +optional throttle_with_priority_new_value(Sensor *parent, float value, const TemplatableFn *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 class ThrottleWithPriorityFilter : public ValueListFilter { public: explicit ThrottleWithPriorityFilter(uint32_t min_time_between_inputs, - std::initializer_list> prioritized_values) + std::initializer_list> prioritized_values) : ValueListFilter(prioritized_values), min_time_between_inputs_(min_time_between_inputs) {} optional 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 &new_value) + explicit TimeoutFilterConfigured(uint32_t time_period, const TemplatableFn &new_value) : TimeoutFilterBase(time_period), value_(new_value) {} optional new_value(float value) override; protected: float get_output_value() override { return this->value_.value(); } - TemplatableValue value_; // 16 bytes (configured output value, can be lambda) - // Total: 8 (base) + 16 = 24 bytes + vtable ptr + Component overhead + TemplatableFn 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 { diff --git a/esphome/components/speaker/media_player/__init__.py b/esphome/components/speaker/media_player/__init__.py index b16f882cba..320e96c897 100644 --- a/esphome/components/speaker/media_player/__init__.py +++ b/esphome/components/speaker/media_player/__init__.py @@ -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 diff --git a/esphome/components/speaker_source/media_player.py b/esphome/components/speaker_source/media_player.py index 7f0f776ee5..70feeac318 100644 --- a/esphome/components/speaker_source/media_player.py +++ b/esphome/components/speaker_source/media_player.py @@ -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_)) diff --git a/esphome/components/sprinkler/__init__.py b/esphome/components/sprinkler/__init__.py index fb2beb5b16..efa5b0bf15 100644 --- a/esphome/components/sprinkler/__init__.py +++ b/esphome/components/sprinkler/__init__.py @@ -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) diff --git a/esphome/components/sprinkler/automation.h b/esphome/components/sprinkler/automation.h index b3f030805d..c6fe2e4e02 100644 --- a/esphome/components/sprinkler/automation.h +++ b/esphome/components/sprinkler/automation.h @@ -108,7 +108,8 @@ template class StartSingleValveAction : public Action { 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 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 class StartSingleValveAction : public Action { protected: Sprinkler *sprinkler_; + TemplatableValue valve_to_start_{}; }; template class ShutdownAction : public Action { diff --git a/esphome/core/automation.h b/esphome/core/automation.h index 05c7f19588..eb270bfee2 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -34,70 +34,236 @@ template struct gens<0, S...> { using type = seq; }; #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 class TemplatableFn { + public: + TemplatableFn() = default; + TemplatableFn(std::nullptr_t) = delete; + + // Exact return type match — direct function pointer storage + template TemplatableFn(F f) requires std::convertible_to : 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 + [[deprecated("Lambda return type does not match TemplatableFn — use the correct type in " + "codegen")]] TemplatableFn(F) requires(!std::convertible_to) && + std::invocable &&std::convertible_to, T> &&std::is_empty_v + &&std::default_initializable : f_([](X... x) -> T { return static_cast(F{}(x...)); }) {} + + // Reject any callable that didn't match the above (stateful lambdas or inconvertible return types) + template + TemplatableFn(F) requires std::invocable && + (!std::convertible_to) &&(!std::is_empty_v || + !std::convertible_to, T> || + !std::default_initializable) = delete; + + bool has_value() const { return this->f_ != nullptr; } + + T value(X... x) const { return this->f_ ? this->f_(x...) : T{}; } + + optional 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 class TemplatableValue; + +/// Selects TemplatableFn (4 bytes) for trivially copyable types, TemplatableValue (8 bytes) otherwise. +/// Non-trivial types (std::string, std::vector, etc.) need TemplatableValue for raw value +/// storage, PROGMEM/FlashStringHelper support (strings), and proper copy/move/destruction. +template +using TemplatableStorage = + std::conditional_t, TemplatableFn, TemplatableValue>; + #define TEMPLATABLE_VALUE_(type, name) \ protected: \ - TemplatableValue name##_{}; \ + TemplatableStorage name##_{}; \ \ public: \ template 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 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; + public: + TemplatableValue() = default; + TemplatableValue(std::nullptr_t) = delete; + // Accept raw constants + template TemplatableValue(V value) requires(!std::invocable) : tag_(VALUE) { + new (&this->storage_.value_) T(static_cast(std::move(value))); + } + + // Accept stateless lambdas (convertible to function pointer) + template TemplatableValue(F f) requires std::convertible_to : tag_(FN) { + this->storage_.f_ = f; + } + + // Convertible return type (e.g., int -> uint8_t) — casting trampoline + template + [[deprecated("Lambda return type does not match TemplatableValue — use the correct type in " + "codegen")]] TemplatableValue(F) requires(!std::convertible_to) && + std::invocable &&std::convertible_to, T> &&std::is_empty_v + &&std::default_initializable : tag_(FN) { + this->storage_.f_ = [](X... x) -> T { return static_cast(F{}(x...)); }; + } + + // Reject any callable that didn't match the above + template + TemplatableValue(F) requires std::invocable && + (!std::convertible_to) &&(!std::is_empty_v || + !std::convertible_to, T> || + !std::default_initializable) = 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 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) { + 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). 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 class TemplatableValue { 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 : 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 : 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(str); } #endif template TemplatableValue(F value) requires(!std::invocable) : 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 - TemplatableValue(F f) requires std::invocable && std::convertible_to + TemplatableValue(F f) requires std::invocable && std::convertible_to : type_(STATELESS_LAMBDA) { this->stateless_f_ = f; // Implicit conversion to function pointer } // For stateful lambdas (not convertible to function pointer): use std::function template - TemplatableValue(F f) requires std::invocable &&(!std::convertible_to) : type_(LAMBDA) { - this->f_ = new std::function(std::move(f)); + TemplatableValue(F f) requires std::invocable &&(!std::convertible_to) + : type_(LAMBDA) { + this->f_ = new std::function(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(*other.f_); + this->f_ = new std::function(*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 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 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 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) { - 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) { - 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 optional_value(X... x) { - if (!this->has_value()) { + optional 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 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 { + bool is_empty() const { switch (this->type_) { case NONE: return true; @@ -245,7 +390,7 @@ template 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 { + 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 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; + 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 *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 *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 }; }; diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index a8efe96cce..cf90b878e1 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -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): diff --git a/tests/benchmarks/components/sensor/bench_sensor_filter.cpp b/tests/benchmarks/components/sensor/bench_sensor_filter.cpp index e4aa397690..e6dc783567 100644 --- a/tests/benchmarks/components/sensor/bench_sensor_filter.cpp +++ b/tests/benchmarks/components/sensor/bench_sensor_filter.cpp @@ -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), }); diff --git a/tests/unit_tests/test_cpp_generator.py b/tests/unit_tests/test_cpp_generator.py index bdc31cdef8..81ae586e23 100644 --- a/tests/unit_tests/test_cpp_generator.py +++ b/tests/unit_tests/test_cpp_generator.py @@ -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."""