Share the object id hash loop with the flash name path

This commit is contained in:
J. Nick Koston
2026-10-15 15:30:50 -10:00
parent 984dbd616f
commit aff866913c
3 changed files with 23 additions and 18 deletions
+5 -16
View File
@@ -14,17 +14,6 @@ ESPHOME_LOG_TAG(TAG, "entity_base");
#ifdef USE_ESP8266
namespace {
// fnv1_hash_object_id() for a name in flash
uint32_t fnv1_hash_object_id_progmem(const char *str, size_t len) {
uint32_t hash = FNV1_OFFSET_BASIS;
for (size_t i = 0; i < len; i++) {
hash *= FNV1_PRIME;
char c = static_cast<char>(progmem_read_byte(reinterpret_cast<const uint8_t *>(str + i)));
hash ^= static_cast<uint8_t>(to_sanitized_char(to_snake_case_char(c)));
}
return hash;
}
// RAM copies made by get_name(), only for entities whose name is asked for
struct NameCopy {
NameCopy *next;
@@ -82,11 +71,11 @@ void EntityBase::configure_entity_(const char *name, uint32_t object_id_hash, ui
this->flags_.has_own_name = true;
// Static name - codegen precomputes the hash; external register_* callers may pass 0
if (object_id_hash == 0) {
#ifdef USE_ESP8266
object_id_hash = fnv1_hash_object_id_progmem(this->name_.progmem_ptr(), this->name_.size());
#else
object_id_hash = fnv1_hash_object_id(this->name_.progmem_ptr(), this->name_.size());
#endif
// The name is in flash on ESP8266, so read it byte by byte through progmem_read_byte()
const char *name_p = this->name_.progmem_ptr();
object_id_hash = fnv1_hash_object_id(this->name_.size(), [name_p](size_t i) {
return static_cast<char>(progmem_read_byte(reinterpret_cast<const uint8_t *>(name_p + i)));
});
}
this->object_id_hash_ = object_id_hash;
}
+6 -2
View File
@@ -1133,17 +1133,21 @@ template<size_t N> inline char *str_sanitize_to(char (&buffer)[N], const char *s
/// Calculate FNV-1 hash of a string while applying snake_case + sanitize transformations.
/// This computes object_id hashes directly from names without creating an intermediate buffer.
/// read_byte(i) returns character i, so a name in flash can be hashed through progmem_read_byte().
/// IMPORTANT: Must match Python fnv1_hash_object_id() in esphome/helpers.py.
/// If you modify this function, update the Python version and tests in both places.
inline uint32_t fnv1_hash_object_id(const char *str, size_t len) {
template<typename ReadByte> inline uint32_t fnv1_hash_object_id(size_t len, ReadByte read_byte) {
uint32_t hash = FNV1_OFFSET_BASIS;
for (size_t i = 0; i < len; i++) {
hash *= FNV1_PRIME;
// Apply snake_case (space->underscore, uppercase->lowercase) then sanitize
hash ^= static_cast<uint8_t>(to_sanitized_char(to_snake_case_char(str[i])));
hash ^= static_cast<uint8_t>(to_sanitized_char(to_snake_case_char(read_byte(i))));
}
return hash;
}
inline uint32_t fnv1_hash_object_id(const char *str, size_t len) {
return fnv1_hash_object_id(len, [str](size_t i) { return str[i]; });
}
#ifdef USE_ESP8266
// ESP8266: Use vsnprintf_P to keep format strings in flash (PROGMEM)
@@ -2,6 +2,7 @@
#include "esphome/core/entity_base.h"
#include <cstring>
#include <type_traits>
#include <utility>
@@ -24,6 +25,17 @@ TEST(EntityBaseName, ZeroHashIsComputedFromTheName) {
EXPECT_EQ(entity.get_object_id_hash(), fnv1_hash_object_id("Living Room Temp", 16));
}
// The values come from the Python fnv1_hash_object_id() test table. Reading through the byte
// reader is the path a name in flash takes on ESP8266.
TEST(EntityBaseName, HashThroughByteReaderMatchesPlainHash) {
const char *name = "My Sensor Name";
const size_t len = strlen(name);
const uint32_t expected = 0x2760962A;
EXPECT_EQ(fnv1_hash_object_id(name, len), expected);
EXPECT_EQ(fnv1_hash_object_id(len, [name](size_t i) { return name[i]; }), expected);
EXPECT_EQ(fnv1_hash_object_id(0, [](size_t) { return 'x'; }), 0x811C9DC5u);
}
TEST(EntityBaseName, PrecomputedHashIsKept) {
TestEntity entity;
entity.configure("Living Room Temp", 0x1234);