mirror of
https://github.com/esphome/esphome.git
synced 2026-10-15 10:42:52 +00:00
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:
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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_{};
|
||||
|
||||
@@ -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}")(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user