Move flash copy and compare into ProgmemStringRef

ProgmemStringRef owns write_to() and equals() with the platform split, so the
EntityBase accessors are inline forwarders. Adds ESPHOME_ENTITY_NAME_MAX_LEN
mirroring NAME_MAX_LENGTH and tidies the codegen and tests.
This commit is contained in:
J. Nick Koston
2026-10-08 17:44:53 -10:00
parent b5c06efb2f
commit 4c6e6b5447
8 changed files with 61 additions and 64 deletions
+1
View File
@@ -2445,6 +2445,7 @@ def _validate_no_slash(value):
# This ensures web server URL IDs fit in a 280-byte buffer:
# domain(20) + "/" + device(120) + "/" + name(120) + null = 263 bytes
# Note: Must be < 255 because web_server UrlMatch uses uint8_t for length fields
# Keep in sync with ESPHOME_ENTITY_NAME_MAX_LEN in esphome/core/entity_base.h
NAME_MAX_LENGTH = 120
+4 -22
View File
@@ -1,6 +1,5 @@
#include "esphome/core/entity_base.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
@@ -11,11 +10,11 @@ namespace esphome {
ESPHOME_LOG_TAG(TAG, "entity_base");
void EntityBase::configure_entity_(const char *name, uint32_t object_id_hash, uint32_t entity_fields) {
this->name_ = StringRef(name, ESPHOME_strlen_P(name));
this->name_ = ProgmemStringRef(name, ESPHOME_strlen_P(name));
if (this->name_.empty()) {
#ifdef USE_DEVICES
if (this->device_ != nullptr) {
this->name_ = StringRef(this->device_->get_name());
this->name_ = ProgmemStringRef(StringRef(this->device_->get_name()));
} else
#endif
{
@@ -25,10 +24,10 @@ void EntityBase::configure_entity_(const char *name, uint32_t object_id_hash, ui
const auto &friendly = App.get_friendly_name();
if (App.is_name_add_mac_suffix_enabled()) {
// MAC suffix enabled - use friendly_name directly (even if empty) for compatibility
this->name_ = friendly;
this->name_ = ProgmemStringRef(friendly);
} else {
// No MAC suffix - fallback to device name if friendly_name is empty
this->name_ = !friendly.empty() ? friendly : App.get_name();
this->name_ = ProgmemStringRef(!friendly.empty() ? friendly : App.get_name());
}
}
this->flags_.has_own_name = false;
@@ -125,23 +124,6 @@ void EntityBase::calc_object_id_() {
this->object_id_hash_ = fnv1_hash_object_id(name.c_str(), name.size());
}
size_t EntityBase::write_name_to(char *buf, size_t buf_size) const {
size_t len = std::min(this->name_.size(), buf_size - 1);
progmem_memcpy(buf, this->name_.progmem_ptr(), len);
buf[len] = '\0';
return len;
}
#ifdef USE_ESP8266
StringRef EntityBase::get_name_to(std::span<char, ENTITY_NAME_BUF_SIZE> buffer) const {
return StringRef(buffer.data(), this->write_name_to(buffer.data(), buffer.size()));
}
bool EntityBase::name_equals(const StringRef &other) const {
return other.size() == this->name_.size() && memcmp_P(other.c_str(), this->name_.progmem_ptr(), other.size()) == 0;
}
#endif
size_t EntityBase::write_object_id_to(char *buf, size_t buf_size) const {
size_t len = this->write_name_to(buf, buf_size);
for (size_t i = 0; i < len; i++) {
+14 -12
View File
@@ -31,8 +31,11 @@ static constexpr size_t ESPHOME_DEVICE_NAME_MAX_LEN = 31;
// esphome/core/config.py
static constexpr size_t ESPHOME_FRIENDLY_NAME_MAX_LEN = 120;
// Buffer size for EntityBase::get_name_to(); entity names are capped by NAME_MAX_LENGTH in config_validation.py
static constexpr size_t ENTITY_NAME_BUF_SIZE = ESPHOME_FRIENDLY_NAME_MAX_LEN + 1;
// Maximum entity name length - keep in sync with NAME_MAX_LENGTH in esphome/config_validation.py
static constexpr size_t ESPHOME_ENTITY_NAME_MAX_LEN = 120;
// Buffer size for EntityBase::get_name_to()
static constexpr size_t ENTITY_NAME_BUF_SIZE = ESPHOME_ENTITY_NAME_MAX_LEN + 1;
// Maximum domain length (longest: "alarm_control_panel" = 19)
static constexpr size_t ESPHOME_DOMAIN_MAX_LEN = 20;
@@ -75,20 +78,22 @@ class EntityBase {
template<typename T = int> StringRef get_name() const {
static_assert(sizeof(T) == 0, "get_name() unavailable on ESP8266 (name is in flash). "
"Use get_name_to() with a stack buffer, or get_log_name() for logging.");
return StringRef(this->name_.progmem_ptr(), this->name_.size());
return {};
}
/// Get the name of this Entity, copied out of flash into buffer.
StringRef get_name_to(std::span<char, ENTITY_NAME_BUF_SIZE> buffer) const;
bool name_equals(const StringRef &other) const;
StringRef get_name_to(std::span<char, ENTITY_NAME_BUF_SIZE> buffer) const {
return StringRef(buffer.data(), this->name_.write_to(buffer.data(), buffer.size()));
}
#else
ESPDEPRECATED("Use get_name_to() or get_log_name() instead. Will be removed in ESPHome 2027.5.0", "2026.11.0")
StringRef get_name() const { return this->name_ref_(); }
StringRef get_name_to(std::span<char, ENTITY_NAME_BUF_SIZE> /*buffer*/) const { return this->name_ref_(); }
bool name_equals(const StringRef &other) const { return this->name_ref_() == other; }
StringRef get_name() const { return this->name_.ram_ref(); }
StringRef get_name_to(std::span<char, ENTITY_NAME_BUF_SIZE> /*buffer*/) const { return this->name_.ram_ref(); }
#endif
bool name_equals(const StringRef &other) const { return this->name_.equals(other); }
/// Copy the name into buf (null terminated), returns its length.
size_t write_name_to(char *buf, size_t buf_size) const;
size_t write_name_to(char *buf, size_t buf_size) const { return this->name_.write_to(buf, buf_size); }
/// Get the name for a "%s" log argument (LOG_STR_ARG).
const LogString *get_log_name() const { return reinterpret_cast<const LogString *>(this->name_.progmem_ptr()); }
@@ -234,9 +239,6 @@ class EntityBase {
ESPPreferenceObject make_entity_preference_(size_t size, uint32_t version);
void calc_object_id_();
#ifndef USE_ESP8266
StringRef name_ref_() const { return StringRef(this->name_.progmem_ptr(), this->name_.size()); }
#endif
ProgmemStringRef name_;
uint32_t object_id_hash_{};
+3 -2
View File
@@ -312,7 +312,9 @@ def finalize_entity_strings(var: MockObj, config: ConfigType) -> None:
``App.register_<method>(var, name, hash, packed)``. Otherwise falls back to a
standalone ``var->configure_entity_(name, hash, packed)``.
"""
entity_name = config[_KEY_ENTITY_NAME]
entity_name = RawExpression(
f"ESPHOME_PSTR({cpp_string_escape(config[_KEY_ENTITY_NAME])})"
)
object_id_hash = config[_KEY_OBJECT_ID_HASH]
dc_idx = config.get(_KEY_DC_IDX, 0)
uom_idx = config.get(_KEY_UOM_IDX, 0)
@@ -330,7 +332,6 @@ def finalize_entity_strings(var: MockObj, config: ConfigType) -> None:
)
# Build inline comment describing the packed flags for readability
comment = _describe_packed_flags(config, entity_category)
entity_name = RawExpression(f"ESPHOME_PSTR({cpp_string_escape(entity_name)})")
register_method = config.get(_KEY_REGISTER_METHOD)
if register_method is not None:
expr = getattr(App, f"register_{register_method}")(
+31 -5
View File
@@ -1,5 +1,6 @@
#pragma once
#include <algorithm>
#include <cstdint>
#include <cstring>
#include <iterator>
@@ -222,20 +223,43 @@ inline std::string operator+(const std::string &lhs, const StringRef &rhs) {
str.append(rhs.c_str(), rhs.size());
return str;
}
// String conversion functions for ADL compatibility (allows stoi(x) where x is StringRef)
// Must be in esphome namespace for ADL to find them. Uses strtol/strtod directly to avoid heap allocation.
/// A string that may live in flash (on ESP8266): pointer and length, with no byte-reading operations.
/// Read the contents with the progmem helpers; progmem_ptr() is also safe as a "%s" log argument.
/// progmem_ptr() is safe as a "%s" log argument; read the contents with write_to() or equals().
class ProgmemStringRef {
public:
constexpr ProgmemStringRef() = default;
constexpr ProgmemStringRef(const char *s, size_t len) : base_(s), len_(len) {}
constexpr ProgmemStringRef(const StringRef &s) : base_(s.c_str()), len_(s.size()) {} // NOLINT
constexpr explicit ProgmemStringRef(const StringRef &s) : base_(s.c_str()), len_(s.size()) {}
constexpr const char *progmem_ptr() const { return this->base_; }
constexpr size_t size() const { return this->len_; }
constexpr bool empty() const { return this->len_ == 0; }
/// Copy into buf (null terminated, truncated to fit), returns the length copied.
size_t write_to(char *buf, size_t buf_size) const {
size_t len = std::min(this->len_, buf_size - 1);
#ifdef USE_ESP8266
memcpy_P(buf, this->base_, len);
#else
std::memcpy(buf, this->base_, len);
#endif
buf[len] = '\0';
return len;
}
bool equals(const StringRef &other) const {
#ifdef USE_ESP8266
return other.size() == this->len_ && memcmp_P(other.c_str(), this->base_, this->len_) == 0;
#else
return other == StringRef(this->base_, this->len_);
#endif
}
#ifndef USE_ESP8266
/// The string is in RAM here, so it can be viewed directly.
constexpr StringRef ram_ref() const { return StringRef(this->base_, this->len_); }
#endif
// Remove before 2027.5.0 (helpers.h's ESPDEPRECATED is not reachable from here)
[[deprecated(
"May point to flash; use progmem_ptr() or the owner's accessors. Removed in 2027.5.0")]] constexpr const char *
@@ -243,11 +267,13 @@ class ProgmemStringRef {
return this->base_;
}
protected:
private:
const char *base_{""};
size_t len_{0};
};
// String conversion functions for ADL compatibility (allows stoi(x) where x is StringRef)
// Must be in esphome namespace for ADL to find them. Uses strtol/strtod directly to avoid heap allocation.
namespace internal {
// NOLINTBEGIN(google-runtime-int)
template<typename R, typename F> inline R parse_number(const StringRef &str, size_t *pos, F conv) {
+3 -6
View File
@@ -14,15 +14,12 @@ def extract_packed_value(main_cpp: str, var_name: str) -> int:
combined form ``App.register_<entity>(var, name, hash, packed)``.
"""
escaped_var = re.escape(var_name)
name_re = r'ESPHOME_PSTR\("(?:\\.|[^"\\])*"\)'
legacy_pattern = (
rf"{escaped_var}->configure_entity_\("
r'ESPHOME_PSTR\("(?:\\.|[^"\\])*"\)'
r",\s*\w+,\s*(\d+)\)"
rf"{escaped_var}->configure_entity_\(" + name_re + r",\s*\w+,\s*(\d+)\)"
)
combined_pattern = (
rf"App\.register_\w+\(\s*{escaped_var}\s*,\s*"
r'ESPHOME_PSTR\("(?:\\.|[^"\\])*"\)'
r",\s*\w+,\s*(\d+)\)"
rf"App\.register_\w+\(\s*{escaped_var}\s*,\s*" + name_re + r",\s*\w+,\s*(\d+)\)"
)
match = re.search(combined_pattern, main_cpp) or re.search(legacy_pattern, main_cpp)
assert match, f"configure call not found for {var_name}"
@@ -28,18 +28,9 @@ def test_text_sensor_sets_mandatory_fields(generate_main):
main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml")
# Then
assert (
'App.register_text_sensor(ts_1, ESPHOME_PSTR("Template Text Sensor 1"),'
in main_cpp
)
assert (
'App.register_text_sensor(ts_2, ESPHOME_PSTR("Template Text Sensor 2"),'
in main_cpp
)
assert (
'App.register_text_sensor(ts_3, ESPHOME_PSTR("Template Text Sensor 3"),'
in main_cpp
)
for i in (1, 2, 3):
name = f'ESPHOME_PSTR("Template Text Sensor {i}")'
assert f"App.register_text_sensor(ts_{i}, {name}," in main_cpp
def test_text_sensor_config_value_internal_set(generate_main):
+2 -5
View File
@@ -38,11 +38,8 @@ from esphome.helpers import fnv1_hash, sanitize, snake_case
from .common import load_config_from_fixture
# Pre-compiled regex pattern for extracting names from configure_entity_/set_name calls
# Matches: .configure_entity_(ESPHOME_PSTR("name"), ...) or .set_name("name", ...)
ENTITY_NAME_PATTERN = re.compile(
r'\.(?:configure_entity_|set_name)\(ESPHOME_PSTR\(["\']([^"\']*)["\']'
)
# Extracts the name from .configure_entity_(ESPHOME_PSTR("name"), ...)
ENTITY_NAME_PATTERN = re.compile(r'\.configure_entity_\(ESPHOME_PSTR\("([^"]*)"')
FIXTURES_DIR = Path(__file__).parent.parent / "fixtures" / "core" / "entity_helpers"