mirror of
https://github.com/esphome/esphome.git
synced 2026-07-10 08:55:36 +00:00
[core] Specialize TemplatableValue for non-string types as function pointer
Specialize TemplatableValue so non-string types use function-pointer-only storage (4 bytes on 32-bit) instead of the tagged union with std::function support (8 bytes). The std::string specialization retains full support for VALUE, STATIC_STRING, FLASH_STRING, and stateful lambdas. Codegen now wraps non-string constants in stateless lambdas automatically, so the generated C++ always assigns a function pointer.
This commit is contained in:
@@ -275,7 +275,7 @@ template<typename... Ts> class APIRespondAction : public Action<Ts...> {
|
||||
|
||||
protected:
|
||||
APIServer *parent_;
|
||||
TemplatableValue<bool, Ts...> success_{true};
|
||||
TemplatableValue<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_;
|
||||
|
||||
@@ -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...> {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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};
|
||||
TemplatableValue<float, float> min_{[](float) -> float { return NAN; }}; // NAN = no bound
|
||||
TemplatableValue<float, float> max_{[](float) -> float { return NAN; }}; // NAN = no bound
|
||||
};
|
||||
|
||||
template<typename... Ts> class NumberInRangeCondition : public Condition<Ts...> {
|
||||
|
||||
@@ -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};
|
||||
TemplatableValue<float, float> min_{[](float) -> float { return NAN; }};
|
||||
TemplatableValue<float, float> max_{[](float) -> float { return NAN; }};
|
||||
};
|
||||
|
||||
template<typename... Ts> class SensorInRangeCondition : public Condition<Ts...> {
|
||||
|
||||
+84
-107
@@ -43,61 +43,78 @@ template<int... S> struct gens<0, S...> { using type = seq<S...>; };
|
||||
|
||||
#define TEMPLATABLE_VALUE(type, name) TEMPLATABLE_VALUE_(type, name)
|
||||
|
||||
/// Primary template: function-pointer-only storage (4 bytes on 32-bit).
|
||||
/// Codegen wraps constants in stateless lambdas so only a function pointer is needed.
|
||||
/// Stateful lambdas (std::function) are rejected at compile time.
|
||||
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;
|
||||
|
||||
// Accept stateless lambdas (convertible to function pointer)
|
||||
template<typename F> TemplatableValue(F f) requires std::convertible_to<F, T (*)(X...)> : f_(f) {}
|
||||
|
||||
// Reject stateful lambdas at compile time
|
||||
template<typename F>
|
||||
TemplatableValue(F) requires std::invocable<F, X...> &&(!std::convertible_to<F, T (*)(X...)>) = 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};
|
||||
};
|
||||
|
||||
/// 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
|
||||
this->stateless_f_ = f;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
@@ -105,15 +122,10 @@ 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;
|
||||
@@ -125,7 +137,6 @@ template<typename T, typename... X> class TemplatableValue {
|
||||
other.type_ = NONE;
|
||||
}
|
||||
|
||||
// Assignment operators
|
||||
TemplatableValue &operator=(const TemplatableValue &other) {
|
||||
if (this != &other) {
|
||||
this->~TemplatableValue();
|
||||
@@ -144,82 +155,58 @@ 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_;
|
||||
}
|
||||
// STATELESS_LAMBDA/STATIC_STRING/FLASH_STRING/NONE: no cleanup needed (pointers, not heap-allocated)
|
||||
}
|
||||
|
||||
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
|
||||
return this->stateless_f_(x...);
|
||||
case LAMBDA:
|
||||
return (*this->f_)(x...); // std::function call
|
||||
return (*this->f_)(x...);
|
||||
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:
|
||||
// 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();
|
||||
case FLASH_STRING: {
|
||||
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) {
|
||||
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) {
|
||||
if (!this->has_value())
|
||||
return default_value;
|
||||
}
|
||||
return this->value(x...);
|
||||
}
|
||||
|
||||
/// Check if this holds a static string (const char* stored without allocation)
|
||||
/// The pointer is always directly readable (RAM or flash-mapped).
|
||||
/// Returns false for FLASH_STRING (PROGMEM on ESP8266, requires _P functions).
|
||||
/// Check if this holds a static string (const char* stored without allocation).
|
||||
bool is_static_string() const { return this->type_ == STATIC_STRING; }
|
||||
|
||||
/// Get the static string pointer (only valid if is_static_string() returns true)
|
||||
/// The pointer is always directly readable — FLASH_STRING uses a separate type.
|
||||
/// Get the static string pointer (only valid if is_static_string() returns true).
|
||||
const char *get_static_string() const { return this->static_str_; }
|
||||
|
||||
/// Check if the string value is empty without allocating (for std::string specialization).
|
||||
/// 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> {
|
||||
/// Check if the string value is empty without allocating.
|
||||
bool is_empty() const {
|
||||
switch (this->type_) {
|
||||
case NONE:
|
||||
return true;
|
||||
@@ -227,25 +214,18 @@ template<typename T, typename... X> class TemplatableValue {
|
||||
return this->static_str_ == nullptr || this->static_str_[0] == '\0';
|
||||
#ifdef USE_ESP8266
|
||||
case FLASH_STRING:
|
||||
// PROGMEM pointer — must use progmem_read_byte on ESP8266
|
||||
return this->static_str_ == nullptr ||
|
||||
progmem_read_byte(reinterpret_cast<const uint8_t *>(this->static_str_)) == '\0';
|
||||
#endif
|
||||
case VALUE:
|
||||
return this->value_->empty();
|
||||
default: // LAMBDA/STATELESS_LAMBDA - must call value()
|
||||
default:
|
||||
return this->value().empty();
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a StringRef to the string value without heap allocation when possible.
|
||||
/// For STATIC_STRING/VALUE, returns reference to existing data (no allocation).
|
||||
/// For FLASH_STRING (ESP8266 PROGMEM), copies to provided buffer via _P functions.
|
||||
/// For LAMBDA/STATELESS_LAMBDA, calls value(), copies to provided buffer, returns ref to buffer.
|
||||
/// @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> {
|
||||
/// Get a StringRef without heap allocation when possible.
|
||||
StringRef ref_or_copy_to(char *lambda_buf, size_t lambda_buf_size) const {
|
||||
switch (this->type_) {
|
||||
case NONE:
|
||||
return StringRef();
|
||||
@@ -258,7 +238,6 @@ template<typename T, typename... X> class TemplatableValue {
|
||||
if (this->static_str_ == nullptr)
|
||||
return StringRef();
|
||||
{
|
||||
// PROGMEM pointer — copy to buffer via _P functions
|
||||
size_t len = strlen_P(this->static_str_);
|
||||
size_t copy_len = std::min(len, lambda_buf_size - 1);
|
||||
memcpy_P(lambda_buf, this->static_str_, copy_len);
|
||||
@@ -268,7 +247,7 @@ template<typename T, typename... X> class TemplatableValue {
|
||||
#endif
|
||||
case VALUE:
|
||||
return StringRef(this->value_->data(), this->value_->size());
|
||||
default: { // LAMBDA/STATELESS_LAMBDA - must call value() and copy
|
||||
default: {
|
||||
std::string result = this->value();
|
||||
size_t copy_len = std::min(result.size(), lambda_buf_size - 1);
|
||||
memcpy(lambda_buf, result.data(), copy_len);
|
||||
@@ -278,22 +257,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,
|
||||
FLASH_STRING,
|
||||
} 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_;
|
||||
std::function<std::string(X...)> *f_;
|
||||
std::string (*stateless_f_)(X...);
|
||||
const char *static_str_;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
+22
-12
@@ -823,7 +823,9 @@ async def templatable(
|
||||
"""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).
|
||||
For all other output types, constants are wrapped in stateless lambdas
|
||||
so that TemplatableValue can store them as function pointers.
|
||||
|
||||
:param value: The value to process.
|
||||
:param args: The arguments for the lambda expression.
|
||||
@@ -833,20 +835,28 @@ async def templatable(
|
||||
"""
|
||||
if is_template(value):
|
||||
return await process_lambda(value, args, return_type=output_type)
|
||||
if to_exp is None:
|
||||
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:
|
||||
# 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
|
||||
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)
|
||||
if output_type is std_string:
|
||||
return FlashStringLiteral(value)
|
||||
# For non-string types, wrap constants in stateless lambdas so that
|
||||
# TemplatableValue stores them as function pointers (4 bytes vs 8).
|
||||
if output_type is not None:
|
||||
from esphome.cpp_types import std_string
|
||||
|
||||
if output_type is not std_string:
|
||||
return LambdaExpression(
|
||||
f"return {safe_exp(value)};",
|
||||
args,
|
||||
capture="",
|
||||
return_type=output_type,
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
class MockObj(Expression):
|
||||
|
||||
@@ -669,11 +669,10 @@ 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)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
Reference in New Issue
Block a user