mirror of
https://github.com/esphome/esphome.git
synced 2026-02-10 19:47:35 -07:00
Compare commits
9 Commits
api-dedup-
...
api-flash-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2590636e48 | ||
|
|
cf7da8e86d | ||
|
|
dbef2e24b3 | ||
|
|
9440138ac7 | ||
|
|
5cf27a8927 | ||
|
|
5d5344cf91 | ||
|
|
b0cf94c409 | ||
|
|
c990da265a | ||
|
|
91487e7f14 |
@@ -11,6 +11,7 @@
|
|||||||
from esphome.cpp_generator import ( # noqa: F401
|
from esphome.cpp_generator import ( # noqa: F401
|
||||||
ArrayInitializer,
|
ArrayInitializer,
|
||||||
Expression,
|
Expression,
|
||||||
|
FlashStringLiteral,
|
||||||
LineComment,
|
LineComment,
|
||||||
LogStringLiteral,
|
LogStringLiteral,
|
||||||
MockObj,
|
MockObj,
|
||||||
|
|||||||
@@ -524,24 +524,31 @@ async def homeassistant_service_to_code(
|
|||||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||||
serv = await cg.get_variable(config[CONF_ID])
|
serv = await cg.get_variable(config[CONF_ID])
|
||||||
var = cg.new_Pvariable(action_id, template_arg, serv, False)
|
var = cg.new_Pvariable(action_id, template_arg, serv, False)
|
||||||
templ = await cg.templatable(config[CONF_ACTION], args, None)
|
templ = await cg.templatable(config[CONF_ACTION], args, cg.std_string)
|
||||||
cg.add(var.set_service(templ))
|
cg.add(var.set_service(templ))
|
||||||
|
|
||||||
# Initialize FixedVectors with exact sizes from config
|
# Initialize FixedVectors with exact sizes from config
|
||||||
cg.add(var.init_data(len(config[CONF_DATA])))
|
cg.add(var.init_data(len(config[CONF_DATA])))
|
||||||
for key, value in config[CONF_DATA].items():
|
for key, value in config[CONF_DATA].items():
|
||||||
|
# output_type=None because lambdas can return non-string types (int,
|
||||||
|
# float, char*) that TemplatableStringValue converts via to_string.
|
||||||
|
# Static strings are manually wrapped for PROGMEM on ESP8266.
|
||||||
templ = await cg.templatable(value, args, None)
|
templ = await cg.templatable(value, args, None)
|
||||||
cg.add(var.add_data(key, templ))
|
if isinstance(templ, str):
|
||||||
|
templ = cg.FlashStringLiteral(templ)
|
||||||
|
cg.add(var.add_data(cg.FlashStringLiteral(key), templ))
|
||||||
|
|
||||||
cg.add(var.init_data_template(len(config[CONF_DATA_TEMPLATE])))
|
cg.add(var.init_data_template(len(config[CONF_DATA_TEMPLATE])))
|
||||||
for key, value in config[CONF_DATA_TEMPLATE].items():
|
for key, value in config[CONF_DATA_TEMPLATE].items():
|
||||||
templ = await cg.templatable(value, args, None)
|
templ = await cg.templatable(value, args, None)
|
||||||
cg.add(var.add_data_template(key, templ))
|
if isinstance(templ, str):
|
||||||
|
templ = cg.FlashStringLiteral(templ)
|
||||||
|
cg.add(var.add_data_template(cg.FlashStringLiteral(key), templ))
|
||||||
|
|
||||||
cg.add(var.init_variables(len(config[CONF_VARIABLES])))
|
cg.add(var.init_variables(len(config[CONF_VARIABLES])))
|
||||||
for key, value in config[CONF_VARIABLES].items():
|
for key, value in config[CONF_VARIABLES].items():
|
||||||
templ = await cg.templatable(value, args, None)
|
templ = await cg.templatable(value, args, None)
|
||||||
cg.add(var.add_variable(key, templ))
|
cg.add(var.add_variable(cg.FlashStringLiteral(key), templ))
|
||||||
|
|
||||||
if on_error := config.get(CONF_ON_ERROR):
|
if on_error := config.get(CONF_ON_ERROR):
|
||||||
cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES")
|
cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES")
|
||||||
@@ -609,24 +616,31 @@ async def homeassistant_event_to_code(config, action_id, template_arg, args):
|
|||||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||||
serv = await cg.get_variable(config[CONF_ID])
|
serv = await cg.get_variable(config[CONF_ID])
|
||||||
var = cg.new_Pvariable(action_id, template_arg, serv, True)
|
var = cg.new_Pvariable(action_id, template_arg, serv, True)
|
||||||
templ = await cg.templatable(config[CONF_EVENT], args, None)
|
templ = await cg.templatable(config[CONF_EVENT], args, cg.std_string)
|
||||||
cg.add(var.set_service(templ))
|
cg.add(var.set_service(templ))
|
||||||
|
|
||||||
# Initialize FixedVectors with exact sizes from config
|
# Initialize FixedVectors with exact sizes from config
|
||||||
cg.add(var.init_data(len(config[CONF_DATA])))
|
cg.add(var.init_data(len(config[CONF_DATA])))
|
||||||
for key, value in config[CONF_DATA].items():
|
for key, value in config[CONF_DATA].items():
|
||||||
|
# output_type=None because lambdas can return non-string types (int,
|
||||||
|
# float, char*) that TemplatableStringValue converts via to_string.
|
||||||
|
# Static strings are manually wrapped for PROGMEM on ESP8266.
|
||||||
templ = await cg.templatable(value, args, None)
|
templ = await cg.templatable(value, args, None)
|
||||||
cg.add(var.add_data(key, templ))
|
if isinstance(templ, str):
|
||||||
|
templ = cg.FlashStringLiteral(templ)
|
||||||
|
cg.add(var.add_data(cg.FlashStringLiteral(key), templ))
|
||||||
|
|
||||||
cg.add(var.init_data_template(len(config[CONF_DATA_TEMPLATE])))
|
cg.add(var.init_data_template(len(config[CONF_DATA_TEMPLATE])))
|
||||||
for key, value in config[CONF_DATA_TEMPLATE].items():
|
for key, value in config[CONF_DATA_TEMPLATE].items():
|
||||||
templ = await cg.templatable(value, args, None)
|
templ = await cg.templatable(value, args, None)
|
||||||
cg.add(var.add_data_template(key, templ))
|
if isinstance(templ, str):
|
||||||
|
templ = cg.FlashStringLiteral(templ)
|
||||||
|
cg.add(var.add_data_template(cg.FlashStringLiteral(key), templ))
|
||||||
|
|
||||||
cg.add(var.init_variables(len(config[CONF_VARIABLES])))
|
cg.add(var.init_variables(len(config[CONF_VARIABLES])))
|
||||||
for key, value in config[CONF_VARIABLES].items():
|
for key, value in config[CONF_VARIABLES].items():
|
||||||
templ = await cg.templatable(value, args, None)
|
templ = await cg.templatable(value, args, None)
|
||||||
cg.add(var.add_variable(key, templ))
|
cg.add(var.add_variable(cg.FlashStringLiteral(key), templ))
|
||||||
|
|
||||||
return var
|
return var
|
||||||
|
|
||||||
@@ -649,11 +663,11 @@ async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, arg
|
|||||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||||
serv = await cg.get_variable(config[CONF_ID])
|
serv = await cg.get_variable(config[CONF_ID])
|
||||||
var = cg.new_Pvariable(action_id, template_arg, serv, True)
|
var = cg.new_Pvariable(action_id, template_arg, serv, True)
|
||||||
cg.add(var.set_service("esphome.tag_scanned"))
|
cg.add(var.set_service(cg.FlashStringLiteral("esphome.tag_scanned")))
|
||||||
# Initialize FixedVector with exact size (1 data field)
|
# Initialize FixedVector with exact size (1 data field)
|
||||||
cg.add(var.init_data(1))
|
cg.add(var.init_data(1))
|
||||||
templ = await cg.templatable(config[CONF_TAG], args, cg.std_string)
|
templ = await cg.templatable(config[CONF_TAG], args, cg.std_string)
|
||||||
cg.add(var.add_data("tag_id", templ))
|
cg.add(var.add_data(cg.FlashStringLiteral("tag_id"), templ))
|
||||||
return var
|
return var
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -128,6 +128,20 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
|||||||
this->add_kv_(this->variables_, key, std::forward<V>(value));
|
this->add_kv_(this->variables_, key, std::forward<V>(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef USE_ESP8266
|
||||||
|
// On ESP8266, ESPHOME_F() returns __FlashStringHelper* (PROGMEM pointer).
|
||||||
|
// Store as const char* — populate_service_map copies from PROGMEM at play() time.
|
||||||
|
template<typename V> void add_data(const __FlashStringHelper *key, V &&value) {
|
||||||
|
this->add_kv_(this->data_, reinterpret_cast<const char *>(key), std::forward<V>(value));
|
||||||
|
}
|
||||||
|
template<typename V> void add_data_template(const __FlashStringHelper *key, V &&value) {
|
||||||
|
this->add_kv_(this->data_template_, reinterpret_cast<const char *>(key), std::forward<V>(value));
|
||||||
|
}
|
||||||
|
template<typename V> void add_variable(const __FlashStringHelper *key, V &&value) {
|
||||||
|
this->add_kv_(this->variables_, reinterpret_cast<const char *>(key), std::forward<V>(value));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
template<typename T> void set_response_template(T response_template) {
|
template<typename T> void set_response_template(T response_template) {
|
||||||
this->response_template_ = response_template;
|
this->response_template_ = response_template;
|
||||||
@@ -219,7 +233,32 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
|||||||
Ts... x) {
|
Ts... x) {
|
||||||
dest.init(source.size());
|
dest.init(source.size());
|
||||||
|
|
||||||
// Count non-static strings to allocate exact storage needed
|
#ifdef USE_ESP8266
|
||||||
|
// On ESP8266, all static strings from codegen are FLASH_STRING (PROGMEM),
|
||||||
|
// so is_static_string() is always false — the zero-copy STATIC_STRING fast
|
||||||
|
// path from the non-ESP8266 branch cannot trigger. We copy all keys and
|
||||||
|
// values unconditionally: keys via _P functions (may be in PROGMEM), values
|
||||||
|
// via value() which handles FLASH_STRING internally.
|
||||||
|
value_storage.init(source.size() * 2);
|
||||||
|
|
||||||
|
for (auto &it : source) {
|
||||||
|
auto &kv = dest.emplace_back();
|
||||||
|
|
||||||
|
// Key: copy from possible PROGMEM
|
||||||
|
{
|
||||||
|
size_t key_len = strlen_P(it.key);
|
||||||
|
value_storage.push_back(std::string(key_len, '\0'));
|
||||||
|
memcpy_P(value_storage.back().data(), it.key, key_len);
|
||||||
|
kv.key = StringRef(value_storage.back());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value: value() handles FLASH_STRING via _P functions internally
|
||||||
|
value_storage.push_back(it.value.value(x...));
|
||||||
|
kv.value = StringRef(value_storage.back());
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
// On non-ESP8266, strings are directly readable from flash-mapped memory.
|
||||||
|
// Count non-static strings to allocate exact storage needed.
|
||||||
size_t lambda_count = 0;
|
size_t lambda_count = 0;
|
||||||
for (const auto &it : source) {
|
for (const auto &it : source) {
|
||||||
if (!it.value.is_static_string()) {
|
if (!it.value.is_static_string()) {
|
||||||
@@ -233,14 +272,15 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
|||||||
kv.key = StringRef(it.key);
|
kv.key = StringRef(it.key);
|
||||||
|
|
||||||
if (it.value.is_static_string()) {
|
if (it.value.is_static_string()) {
|
||||||
// Static string from YAML - zero allocation
|
// Static string — pointer directly readable, zero allocation
|
||||||
kv.value = StringRef(it.value.get_static_string());
|
kv.value = StringRef(it.value.get_static_string());
|
||||||
} else {
|
} else {
|
||||||
// Lambda evaluation - store result, reference it
|
// Lambda — evaluate and store result
|
||||||
value_storage.push_back(it.value.value(x...));
|
value_storage.push_back(it.value.value(x...));
|
||||||
kv.value = StringRef(value_storage.back());
|
kv.value = StringRef(value_storage.back());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
APIServer *parent_;
|
APIServer *parent_;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "esphome/core/defines.h"
|
#include "esphome/core/defines.h"
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/core/preferences.h"
|
#include "esphome/core/preferences.h"
|
||||||
|
#include "esphome/core/progmem.h"
|
||||||
#include "esphome/core/string_ref.h"
|
#include "esphome/core/string_ref.h"
|
||||||
#include <concepts>
|
#include <concepts>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
@@ -56,6 +57,16 @@ template<typename T, typename... X> class TemplatableValue {
|
|||||||
this->static_str_ = str;
|
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) {
|
||||||
|
this->static_str_ = reinterpret_cast<const char *>(str);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
template<typename F> TemplatableValue(F value) requires(!std::invocable<F, X...>) : type_(VALUE) {
|
template<typename F> TemplatableValue(F value) requires(!std::invocable<F, X...>) : type_(VALUE) {
|
||||||
if constexpr (USE_HEAP_STORAGE) {
|
if constexpr (USE_HEAP_STORAGE) {
|
||||||
this->value_ = new T(std::move(value));
|
this->value_ = new T(std::move(value));
|
||||||
@@ -89,7 +100,7 @@ template<typename T, typename... X> class TemplatableValue {
|
|||||||
this->f_ = new std::function<T(X...)>(*other.f_);
|
this->f_ = new std::function<T(X...)>(*other.f_);
|
||||||
} else if (this->type_ == STATELESS_LAMBDA) {
|
} else if (this->type_ == STATELESS_LAMBDA) {
|
||||||
this->stateless_f_ = other.stateless_f_;
|
this->stateless_f_ = other.stateless_f_;
|
||||||
} else if (this->type_ == STATIC_STRING) {
|
} else if (this->type_ == STATIC_STRING || this->type_ == FLASH_STRING) {
|
||||||
this->static_str_ = other.static_str_;
|
this->static_str_ = other.static_str_;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,7 +119,7 @@ template<typename T, typename... X> class TemplatableValue {
|
|||||||
other.f_ = nullptr;
|
other.f_ = nullptr;
|
||||||
} else if (this->type_ == STATELESS_LAMBDA) {
|
} else if (this->type_ == STATELESS_LAMBDA) {
|
||||||
this->stateless_f_ = other.stateless_f_;
|
this->stateless_f_ = other.stateless_f_;
|
||||||
} else if (this->type_ == STATIC_STRING) {
|
} else if (this->type_ == STATIC_STRING || this->type_ == FLASH_STRING) {
|
||||||
this->static_str_ = other.static_str_;
|
this->static_str_ = other.static_str_;
|
||||||
}
|
}
|
||||||
other.type_ = NONE;
|
other.type_ = NONE;
|
||||||
@@ -141,7 +152,7 @@ template<typename T, typename... X> class TemplatableValue {
|
|||||||
} else if (this->type_ == LAMBDA) {
|
} else if (this->type_ == LAMBDA) {
|
||||||
delete this->f_;
|
delete this->f_;
|
||||||
}
|
}
|
||||||
// STATELESS_LAMBDA/STATIC_STRING/NONE: no cleanup needed (pointers, not heap-allocated)
|
// STATELESS_LAMBDA/STATIC_STRING/FLASH_STRING/NONE: no cleanup needed (pointers, not heap-allocated)
|
||||||
}
|
}
|
||||||
|
|
||||||
bool has_value() const { return this->type_ != NONE; }
|
bool has_value() const { return this->type_ != NONE; }
|
||||||
@@ -165,6 +176,17 @@ template<typename T, typename... X> class TemplatableValue {
|
|||||||
return std::string(this->static_str_);
|
return std::string(this->static_str_);
|
||||||
}
|
}
|
||||||
__builtin_unreachable();
|
__builtin_unreachable();
|
||||||
|
#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();
|
||||||
|
#endif
|
||||||
case NONE:
|
case NONE:
|
||||||
default:
|
default:
|
||||||
return T{};
|
return T{};
|
||||||
@@ -186,9 +208,12 @@ template<typename T, typename... X> class TemplatableValue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Check if this holds a static string (const char* stored without allocation)
|
/// 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).
|
||||||
bool is_static_string() const { return this->type_ == STATIC_STRING; }
|
bool is_static_string() const { return this->type_ == STATIC_STRING; }
|
||||||
|
|
||||||
/// Get the static string pointer (only valid if is_static_string() returns true)
|
/// 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.
|
||||||
const char *get_static_string() const { return this->static_str_; }
|
const char *get_static_string() const { return this->static_str_; }
|
||||||
|
|
||||||
/// Check if the string value is empty without allocating (for std::string specialization).
|
/// Check if the string value is empty without allocating (for std::string specialization).
|
||||||
@@ -200,6 +225,12 @@ template<typename T, typename... X> class TemplatableValue {
|
|||||||
return true;
|
return true;
|
||||||
case STATIC_STRING:
|
case STATIC_STRING:
|
||||||
return this->static_str_ == nullptr || this->static_str_[0] == '\0';
|
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:
|
case VALUE:
|
||||||
return this->value_->empty();
|
return this->value_->empty();
|
||||||
default: // LAMBDA/STATELESS_LAMBDA - must call value()
|
default: // LAMBDA/STATELESS_LAMBDA - must call value()
|
||||||
@@ -209,8 +240,9 @@ template<typename T, typename... X> class TemplatableValue {
|
|||||||
|
|
||||||
/// Get a StringRef to the string value without heap allocation when possible.
|
/// Get a StringRef to the string value without heap allocation when possible.
|
||||||
/// For STATIC_STRING/VALUE, returns reference to existing data (no allocation).
|
/// 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.
|
/// For LAMBDA/STATELESS_LAMBDA, calls value(), copies to provided buffer, returns ref to buffer.
|
||||||
/// @param lambda_buf Buffer used only for lambda case (must remain valid while StringRef is used).
|
/// @param lambda_buf Buffer used only for copy cases (must remain valid while StringRef is used).
|
||||||
/// @param lambda_buf_size Size of the buffer.
|
/// @param lambda_buf_size Size of the buffer.
|
||||||
/// @return StringRef pointing to the string data.
|
/// @return StringRef pointing to the string data.
|
||||||
StringRef ref_or_copy_to(char *lambda_buf, size_t lambda_buf_size) const requires std::same_as<T, std::string> {
|
StringRef ref_or_copy_to(char *lambda_buf, size_t lambda_buf_size) const requires std::same_as<T, std::string> {
|
||||||
@@ -221,6 +253,19 @@ template<typename T, typename... X> class TemplatableValue {
|
|||||||
if (this->static_str_ == nullptr)
|
if (this->static_str_ == nullptr)
|
||||||
return StringRef();
|
return StringRef();
|
||||||
return StringRef(this->static_str_, strlen(this->static_str_));
|
return StringRef(this->static_str_, strlen(this->static_str_));
|
||||||
|
#ifdef USE_ESP8266
|
||||||
|
case FLASH_STRING:
|
||||||
|
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);
|
||||||
|
lambda_buf[copy_len] = '\0';
|
||||||
|
return StringRef(lambda_buf, copy_len);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
case VALUE:
|
case VALUE:
|
||||||
return StringRef(this->value_->data(), this->value_->size());
|
return StringRef(this->value_->data(), this->value_->size());
|
||||||
default: { // LAMBDA/STATELESS_LAMBDA - must call value() and copy
|
default: { // LAMBDA/STATELESS_LAMBDA - must call value() and copy
|
||||||
@@ -239,6 +284,7 @@ template<typename T, typename... X> class TemplatableValue {
|
|||||||
LAMBDA,
|
LAMBDA,
|
||||||
STATELESS_LAMBDA,
|
STATELESS_LAMBDA,
|
||||||
STATIC_STRING, // For const char* when T is std::string - avoids heap allocation
|
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_;
|
} type_;
|
||||||
// For std::string, use heap pointer to minimize union size (4 bytes vs 12+).
|
// For std::string, use heap pointer to minimize union size (4 bytes vs 12+).
|
||||||
// For other types, store value inline as before.
|
// For other types, store value inline as before.
|
||||||
@@ -247,7 +293,7 @@ template<typename T, typename... X> class TemplatableValue {
|
|||||||
ValueStorage value_; // T for inline storage, T* for heap storage
|
ValueStorage value_; // T for inline storage, T* for heap storage
|
||||||
std::function<T(X...)> *f_;
|
std::function<T(X...)> *f_;
|
||||||
T (*stateless_f_)(X...);
|
T (*stateless_f_)(X...);
|
||||||
const char *static_str_; // For STATIC_STRING type
|
const char *static_str_; // For STATIC_STRING and FLASH_STRING types
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -247,6 +247,23 @@ class LogStringLiteral(Literal):
|
|||||||
return f"LOG_STR({cpp_string_escape(self.string)})"
|
return f"LOG_STR({cpp_string_escape(self.string)})"
|
||||||
|
|
||||||
|
|
||||||
|
class FlashStringLiteral(Literal):
|
||||||
|
"""A string literal wrapped in ESPHOME_F() for PROGMEM storage on ESP8266.
|
||||||
|
|
||||||
|
On ESP8266, ESPHOME_F(s) expands to F(s) which stores the string in flash (PROGMEM).
|
||||||
|
On other platforms, ESPHOME_F(s) expands to plain s (no-op).
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("string",)
|
||||||
|
|
||||||
|
def __init__(self, string: str) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.string = string
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"ESPHOME_F({cpp_string_escape(self.string)})"
|
||||||
|
|
||||||
|
|
||||||
class IntLiteral(Literal):
|
class IntLiteral(Literal):
|
||||||
__slots__ = ("i",)
|
__slots__ = ("i",)
|
||||||
|
|
||||||
@@ -761,6 +778,15 @@ async def templatable(
|
|||||||
if is_template(value):
|
if is_template(value):
|
||||||
return await process_lambda(value, args, return_type=output_type)
|
return await process_lambda(value, args, return_type=output_type)
|
||||||
if to_exp is None:
|
if to_exp is 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
|
||||||
|
|
||||||
|
if output_type is std_string:
|
||||||
|
return FlashStringLiteral(value)
|
||||||
return value
|
return value
|
||||||
if isinstance(to_exp, dict):
|
if isinstance(to_exp, dict):
|
||||||
return to_exp[value]
|
return to_exp[value]
|
||||||
|
|||||||
@@ -248,6 +248,12 @@ class TestLiterals:
|
|||||||
(cg.FloatLiteral(4.2), "4.2f"),
|
(cg.FloatLiteral(4.2), "4.2f"),
|
||||||
(cg.FloatLiteral(1.23456789), "1.23456789f"),
|
(cg.FloatLiteral(1.23456789), "1.23456789f"),
|
||||||
(cg.FloatLiteral(math.nan), "NAN"),
|
(cg.FloatLiteral(math.nan), "NAN"),
|
||||||
|
(cg.FlashStringLiteral("hello"), 'ESPHOME_F("hello")'),
|
||||||
|
(cg.FlashStringLiteral(""), 'ESPHOME_F("")'),
|
||||||
|
(
|
||||||
|
cg.FlashStringLiteral('quote"here'),
|
||||||
|
'ESPHOME_F("quote\\042here")',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_str__simple(self, target: cg.Literal, expected: str):
|
def test_str__simple(self, target: cg.Literal, expected: str):
|
||||||
@@ -624,3 +630,75 @@ class TestProcessLambda:
|
|||||||
# Test invalid tuple format (single element)
|
# Test invalid tuple format (single element)
|
||||||
with pytest.raises(AssertionError):
|
with pytest.raises(AssertionError):
|
||||||
await cg.process_lambda(lambda_obj, [(int,)])
|
await cg.process_lambda(lambda_obj, [(int,)])
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_templatable__string_with_std_string_returns_flash_literal() -> None:
|
||||||
|
"""Static string with std::string output_type returns FlashStringLiteral."""
|
||||||
|
result = await cg.templatable("hello", [], ct.std_string)
|
||||||
|
|
||||||
|
assert isinstance(result, cg.FlashStringLiteral)
|
||||||
|
assert str(result) == 'ESPHOME_F("hello")'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_templatable__empty_string_with_std_string() -> None:
|
||||||
|
"""Empty static string with std::string output_type returns FlashStringLiteral."""
|
||||||
|
result = await cg.templatable("", [], ct.std_string)
|
||||||
|
|
||||||
|
assert isinstance(result, cg.FlashStringLiteral)
|
||||||
|
assert str(result) == 'ESPHOME_F("")'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_templatable__string_with_none_output_type() -> None:
|
||||||
|
"""Static string with output_type=None returns raw string (no wrapping)."""
|
||||||
|
result = await cg.templatable("hello", [], None)
|
||||||
|
|
||||||
|
assert isinstance(result, str)
|
||||||
|
assert result == "hello"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_templatable__int_with_std_string() -> None:
|
||||||
|
"""Non-string value with std::string output_type returns raw value."""
|
||||||
|
result = await cg.templatable(42, [], ct.std_string)
|
||||||
|
|
||||||
|
assert result == 42
|
||||||
|
|
||||||
|
|
||||||
|
@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."""
|
||||||
|
result = await cg.templatable("hello", [], ct.bool_)
|
||||||
|
|
||||||
|
assert isinstance(result, str)
|
||||||
|
assert result == "hello"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_templatable__with_to_exp_callable() -> None:
|
||||||
|
"""When to_exp is provided, it is applied to non-template values."""
|
||||||
|
result = await cg.templatable(42, [], None, to_exp=lambda x: x * 2)
|
||||||
|
|
||||||
|
assert result == 84
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_templatable__with_to_exp_dict() -> None:
|
||||||
|
"""When to_exp is a dict, value is looked up."""
|
||||||
|
mapping: dict[str, int] = {"on": 1, "off": 0}
|
||||||
|
result = await cg.templatable("on", [], None, to_exp=mapping)
|
||||||
|
|
||||||
|
assert result == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_templatable__lambda_with_std_string() -> None:
|
||||||
|
"""Lambda value returns LambdaExpression, not FlashStringLiteral."""
|
||||||
|
from esphome.core import Lambda
|
||||||
|
|
||||||
|
lambda_obj = Lambda('return "hello";')
|
||||||
|
result = await cg.templatable(lambda_obj, [], ct.std_string)
|
||||||
|
|
||||||
|
assert isinstance(result, cg.LambdaExpression)
|
||||||
|
|||||||
Reference in New Issue
Block a user