diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h
index b50be5d0d4..268d3f4b05 100644
--- a/esphome/components/api/api_connection.h
+++ b/esphome/components/api/api_connection.h
@@ -310,17 +310,10 @@ class APIConnection final : public APIServerConnection {
APIConnection *conn, uint32_t remaining_size, bool is_single) {
// Set common fields that are shared by all entity types
msg.key = entity->get_object_id_hash();
- // Try to use static reference first to avoid allocation
- StringRef static_ref = entity->get_object_id_ref_for_api_();
- // Store dynamic string outside the if-else to maintain lifetime
- std::string object_id;
- if (!static_ref.empty()) {
- msg.set_object_id(static_ref);
- } else {
- // Dynamic case - need to allocate
- object_id = entity->get_object_id();
- msg.set_object_id(StringRef(object_id));
- }
+ // Get object_id with zero heap allocation
+ // Static case returns direct reference, dynamic case uses buffer
+ char object_id_buf[OBJECT_ID_MAX_LEN];
+ msg.set_object_id(entity->get_object_id_to(object_id_buf));
if (entity->has_own_name()) {
msg.set_name(entity->get_name());
diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp
index 0c22c2f08d..e6a7b48fbd 100644
--- a/esphome/components/web_server/web_server.cpp
+++ b/esphome/components/web_server/web_server.cpp
@@ -404,8 +404,9 @@ void WebServer::handle_js_request(AsyncWebServerRequest *request) {
// Helper functions to reduce code size by avoiding macro expansion
static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, JsonDetail start_config) {
+ char object_id_buf[OBJECT_ID_MAX_LEN];
+ StringRef object_id = obj->get_object_id_to(object_id_buf);
char id_buf[160]; // object_id can be up to 128 chars + prefix + dash + null
- const auto &object_id = obj->get_object_id();
snprintf(id_buf, sizeof(id_buf), "%s-%s", prefix, object_id.c_str());
root[ESPHOME_F("id")] = id_buf;
if (start_config == DETAIL_ALL) {
diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h
index bb69d57872..98234ec1ae 100644
--- a/esphome/components/web_server/web_server.h
+++ b/esphome/components/web_server/web_server.h
@@ -52,14 +52,10 @@ struct UrlMatch {
}
bool id_equals_entity(EntityBase *entity) const {
- // Zero-copy comparison using StringRef
- StringRef static_ref = entity->get_object_id_ref_for_api_();
- if (!static_ref.empty()) {
- return id && id_len == static_ref.size() && memcmp(id, static_ref.c_str(), id_len) == 0;
- }
- // Fallback to allocation (rare)
- const auto &obj_id = entity->get_object_id();
- return id && id_len == obj_id.length() && memcmp(id, obj_id.c_str(), id_len) == 0;
+ // Get object_id with zero heap allocation
+ char object_id_buf[OBJECT_ID_MAX_LEN];
+ StringRef object_id = entity->get_object_id_to(object_id_buf);
+ return id && id_len == object_id.size() && memcmp(id, object_id.c_str(), id_len) == 0;
}
bool method_equals(const char *str) const {
diff --git a/esphome/components/web_server/web_server_v1.cpp b/esphome/components/web_server/web_server_v1.cpp
index 4f0d0cd1a9..cbc25b9dec 100644
--- a/esphome/components/web_server/web_server_v1.cpp
+++ b/esphome/components/web_server/web_server_v1.cpp
@@ -15,7 +15,8 @@ void write_row(AsyncResponseStream *stream, EntityBase *obj, const std::string &
stream->print("\" id=\"");
stream->print(klass.c_str());
stream->print("-");
- stream->print(obj->get_object_id().c_str());
+ char object_id_buf[OBJECT_ID_MAX_LEN];
+ stream->print(obj->get_object_id_to(object_id_buf).c_str());
stream->print("\">
");
stream->print(obj->get_name().c_str());
stream->print(" | | ");
diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp
index 046f99d8cc..98fb957971 100644
--- a/esphome/core/entity_base.cpp
+++ b/esphome/core/entity_base.cpp
@@ -60,15 +60,6 @@ std::string EntityBase::get_object_id() const {
// `App.get_friendly_name()` is constant.
return this->object_id_c_str_ == nullptr ? "" : this->object_id_c_str_;
}
-StringRef EntityBase::get_object_id_ref_for_api_() const {
- static constexpr auto EMPTY_STRING = StringRef::from_lit("");
- // Return empty for dynamic case (MAC suffix)
- if (this->is_object_id_dynamic_()) {
- return EMPTY_STRING;
- }
- // For static case, return the string or empty if null
- return this->object_id_c_str_ == nullptr ? EMPTY_STRING : StringRef(this->object_id_c_str_);
-}
void EntityBase::set_object_id(const char *object_id) {
this->object_id_c_str_ = object_id;
this->calc_object_id_();
@@ -82,8 +73,24 @@ void EntityBase::set_name_and_object_id(const char *name, const char *object_id)
// Calculate Object ID Hash from Entity Name
void EntityBase::calc_object_id_() {
- this->object_id_hash_ =
- fnv1_hash(this->is_object_id_dynamic_() ? this->get_object_id().c_str() : this->object_id_c_str_);
+ char buf[OBJECT_ID_MAX_LEN];
+ StringRef object_id = this->get_object_id_to(buf);
+ this->object_id_hash_ = fnv1_hash(object_id.c_str());
+}
+
+StringRef EntityBase::get_object_id_to(std::span buf) const {
+ if (!this->is_object_id_dynamic_()) {
+ // Static case: return direct reference, buffer unused
+ return this->object_id_c_str_ == nullptr ? StringRef() : StringRef(this->object_id_c_str_);
+ }
+ // Dynamic case: format into buffer
+ const std::string &name = App.get_friendly_name();
+ size_t len = std::min(name.size(), buf.size() - 1);
+ for (size_t i = 0; i < len; i++) {
+ buf[i] = to_sanitized_char(to_snake_case_char(name[i]));
+ }
+ buf[len] = '\0';
+ return StringRef(buf.data(), len);
}
uint32_t EntityBase::get_object_id_hash() { return this->object_id_hash_; }
diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h
index fdf3f6300a..1714194802 100644
--- a/esphome/core/entity_base.h
+++ b/esphome/core/entity_base.h
@@ -1,7 +1,8 @@
#pragma once
-#include
#include
+#include
+#include
#include "string_ref.h"
#include "helpers.h"
#include "log.h"
@@ -12,14 +13,8 @@
namespace esphome {
-// Forward declaration for friend access
-namespace api {
-class APIConnection;
-} // namespace api
-
-namespace web_server {
-struct UrlMatch;
-} // namespace web_server
+// Maximum size for object_id buffer (friendly_name max ~120 + margin)
+static constexpr size_t OBJECT_ID_MAX_LEN = 128;
enum EntityCategory : uint8_t {
ENTITY_CATEGORY_NONE = 0,
@@ -47,6 +42,11 @@ class EntityBase {
// Get the unique Object ID of this Entity
uint32_t get_object_id_hash();
+ /// Get object_id with zero heap allocation
+ /// For static case: returns StringRef to internal storage (buffer unused)
+ /// For dynamic case: formats into buffer and returns StringRef to buffer
+ StringRef get_object_id_to(std::span buf) const;
+
// Get/set whether this Entity should be hidden outside ESPHome
bool is_internal() const { return this->flags_.internal; }
void set_internal(bool internal) { this->flags_.internal = internal; }
@@ -125,13 +125,6 @@ class EntityBase {
}
protected:
- friend class api::APIConnection;
- friend struct web_server::UrlMatch;
-
- // Get object_id as StringRef when it's static (for API usage)
- // Returns empty StringRef if object_id is dynamic (needs allocation)
- StringRef get_object_id_ref_for_api_() const;
-
void calc_object_id_();
/// Check if the object_id is dynamic (changes with MAC suffix)
diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp
index bbe59e53f1..4b905553ba 100644
--- a/esphome/core/helpers.cpp
+++ b/esphome/core/helpers.cpp
@@ -189,14 +189,6 @@ template std::string str_ctype_transform(const std::string &str)
}
std::string str_lower_case(const std::string &str) { return str_ctype_transform(str); }
std::string str_upper_case(const std::string &str) { return str_ctype_transform(str); }
-// Convert char to snake_case: lowercase and spaces to underscores
-static constexpr char to_snake_case_char(char c) {
- return (c == ' ') ? '_' : (c >= 'A' && c <= 'Z') ? c + ('a' - 'A') : c;
-}
-// Sanitize char: keep alphanumerics, dashes, underscores; replace others with underscore
-static constexpr char to_sanitized_char(char c) {
- return (c == '-' || c == '_' || (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) ? c : '_';
-}
std::string str_snake_case(const std::string &str) {
std::string result = str;
for (char &c : result) {
diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h
index f9dcfccb45..21e910916e 100644
--- a/esphome/core/helpers.h
+++ b/esphome/core/helpers.h
@@ -516,9 +516,16 @@ std::string str_until(const std::string &str, char ch);
std::string str_lower_case(const std::string &str);
/// Convert the string to upper case.
std::string str_upper_case(const std::string &str);
+
+/// Convert a single char to snake_case: lowercase and space to underscore.
+constexpr char to_snake_case_char(char c) { return (c == ' ') ? '_' : (c >= 'A' && c <= 'Z') ? c + ('a' - 'A') : c; }
/// Convert the string to snake case (lowercase with underscores).
std::string str_snake_case(const std::string &str);
+/// Sanitize a single char: keep alphanumerics, dashes, underscores; replace others with underscore.
+constexpr char to_sanitized_char(char c) {
+ return (c == '-' || c == '_' || (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) ? c : '_';
+}
/// Sanitizes the input string by removing all characters but alphanumerics, dashes and underscores.
std::string str_sanitize(const std::string &str);
|