[core] Partially revert "Hash entity keys from the raw name to fix collisions" (#18361)

This commit is contained in:
J. Nick Koston
2026-08-13 20:06:48 -05:00
committed by GitHub
parent 45a056e337
commit dd51624fbb
32 changed files with 489 additions and 1132 deletions
+4 -4
View File
@@ -120,8 +120,8 @@ class Application {
// NOLINTBEGIN(bugprone-macro-parentheses)
#define ENTITY_TYPE_(type, singular, plural, count, upper) \
void register_##singular(type *obj) { this->plural##_.push_back(obj); } \
void register_##singular(type *obj, const char *name, uint32_t entity_key, uint32_t entity_fields) { \
obj->configure_entity_(name, entity_key, entity_fields); \
void register_##singular(type *obj, const char *name, uint32_t object_id_hash, uint32_t entity_fields) { \
obj->configure_entity_(name, object_id_hash, entity_fields); \
this->plural##_.push_back(obj); \
}
#define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) \
@@ -329,7 +329,7 @@ class Application {
#define GET_ENTITY_METHOD(entity_type, entity_name, entities_member) \
entity_type *get_##entity_name##_by_key(uint32_t key, uint32_t device_id, bool include_internal = false) { \
for (auto *obj : this->entities_member##_) { \
if (obj->get_entity_key() == key && obj->get_device_id() == device_id && \
if (obj->get_object_id_hash() == key && obj->get_device_id() == device_id && \
(include_internal || !obj->is_internal())) \
return obj; \
} \
@@ -340,7 +340,7 @@ class Application {
#define GET_ENTITY_METHOD(entity_type, entity_name, entities_member) \
entity_type *get_##entity_name##_by_key(uint32_t key, bool include_internal = false) { \
for (auto *obj : this->entities_member##_) { \
if (obj->get_entity_key() == key && (include_internal || !obj->is_internal())) \
if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) \
return obj; \
} \
return nullptr; \
+20 -32
View File
@@ -8,7 +8,7 @@ namespace esphome {
static const char *const TAG = "entity_base";
void EntityBase::configure_entity_(const char *name, uint32_t entity_key, uint32_t entity_fields) {
void EntityBase::configure_entity_(const char *name, uint32_t object_id_hash, uint32_t entity_fields) {
this->name_ = StringRef(name);
if (this->name_.empty()) {
#ifdef USE_DEVICES
@@ -30,15 +30,15 @@ void EntityBase::configure_entity_(const char *name, uint32_t entity_key, uint32
}
}
this->flags_.has_own_name = false;
// Dynamic name - must calculate key at runtime
this->calc_entity_key_();
// Dynamic name - must calculate hash at runtime
this->calc_object_id_();
} else {
this->flags_.has_own_name = true;
// Static name - use pre-computed key if provided
if (entity_key != 0) {
this->entity_key_ = entity_key;
// Static name - use pre-computed hash if provided
if (object_id_hash != 0) {
this->object_id_hash_ = object_id_hash;
} else {
this->calc_entity_key_();
this->calc_object_id_();
}
}
// Unpack entity string table indices and flags from entity_fields.
@@ -147,15 +147,9 @@ std::string EntityBase::get_icon() const {
}
#endif // !USE_ESP8266
// Calculate the entity key directly from the raw name (no transformations)
void EntityBase::calc_entity_key_() { this->entity_key_ = fnv1_hash_bytes(this->name_.c_str(), this->name_.size()); }
// Reconstruct the OLD (pre-2026.8.0) object_id-based hash for preference key compatibility.
// Named entities historically used the hash pre-computed by Python code generation, which
// sanitized per UTF-8 code point; entities without their own name computed the hash at
// runtime per byte. See https://github.com/esphome/backlog/issues/85
uint32_t EntityBase::calc_old_object_id_hash_() const {
return fnv1_hash_object_id(this->name_.c_str(), this->name_.size(), this->flags_.has_own_name);
// Calculate Object ID Hash directly from name using snake_case + sanitize
void EntityBase::calc_object_id_() {
this->object_id_hash_ = fnv1_hash_object_id(this->name_.c_str(), this->name_.size());
}
size_t EntityBase::write_object_id_to(char *buf, size_t buf_size) const {
@@ -173,22 +167,16 @@ StringRef EntityBase::get_object_id_to(std::span<char, OBJECT_ID_MAX_LEN> buf) c
}
ESPPreferenceObject EntityBase::make_entity_preference_(size_t size, uint32_t version) {
// The old key hashed the sanitized object_id, so multiple entity names could collide on
// one key and overwrite each other's stored preferences; the new key hashes the raw name.
// See: https://github.com/esphome/backlog/issues/85
uint32_t old_key = this->old_preference_key_base_() ^ version;
#ifdef USE_PREFERENCE_KEY_LOOKUP
uint32_t new_key = this->preference_key_base_() ^ version;
auto pref = global_preferences->make_preference(size, new_key);
// All in-tree entity preferences fit the stack buffer, so migration never hits the heap
SmallBufferWithHeapFallback<64> buffer(size);
migrate_preference(pref, buffer.get(), size, old_key, new_key);
return pref;
#else
// Slot-based backends keep the old key: it is only a validity tag on a positional slot,
// so collisions cannot corrupt data there and keeping it preserves stored state.
return global_preferences->make_preference(size, old_key);
#endif
// The key hashes the sanitized object_id, so multiple entity names can collide on one
// key and overwrite each other's stored preferences ("Living Room" and "living_room",
// or two UTF-8 names that both sanitize to underscores). Keys hashed from the raw name
// fix this, but they change the entity key API clients track, which the Home Assistant
// esphome integration cannot handle yet. See: https://github.com/esphome/backlog/issues/85
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
uint32_t key = this->get_preference_hash() ^ version;
#pragma GCC diagnostic pop
return global_preferences->make_preference(size, key);
}
#ifdef USE_ENTITY_ICON
+38 -42
View File
@@ -73,17 +73,8 @@ class EntityBase {
// Get whether this Entity has its own name or it should use the device friendly_name.
bool has_own_name() const { return this->flags_.has_own_name; }
// Get the unique key of this Entity: FNV-1 hash of the raw entity name.
// This is the key sent to API clients and used to route entity state.
uint32_t get_entity_key() const { return this->entity_key_; }
/// Returns the LEGACY object_id hash, unchanged from previous releases, so existing
/// callers keep getting stable values (for example preference keys). This is no longer
/// the key sent to API clients; that is get_entity_key().
ESPDEPRECATED("Use get_entity_key() for the entity key sent to API clients, or "
"make_entity_preference<T>() for preference storage. Will be removed in 2027.1.0.",
"2026.8.0")
uint32_t get_object_id_hash() const { return this->calc_old_object_id_hash_(); }
// Get the unique Object ID of this Entity
uint32_t get_object_id_hash() const { return this->object_id_hash_; }
/// Get object_id with zero heap allocation
/// For static case: returns StringRef to internal storage (buffer unused)
@@ -190,23 +181,39 @@ class EntityBase {
// Set has_state - for components that need to manually set this
void set_has_state(bool state) { this->flags_.has_state = state; }
/// Get this entity's device id, or 0 when devices are not compiled in (main device).
uint32_t get_device_id_or_zero() const {
#ifdef USE_DEVICES
return this->get_device_id();
#else
return 0;
#endif
}
/// Get the LEGACY preference key: FNV-1 hash of the sanitized object_id, XOR device_id.
/// Intentionally keeps the old algorithm so external callers that store preferences under
/// this key keep stable keys; make_entity_preference() migrates to the new raw-name key,
/// this method never will.
/**
* @brief Get a unique hash for storing preferences/settings for this entity.
*
* This method returns a hash that uniquely identifies the entity for the purpose of
* storing preferences (such as calibration, state, etc.). Unlike get_object_id_hash(),
* this hash also incorporates the device_id (if devices are enabled), ensuring uniqueness
* across multiple devices that may have entities with the same object_id.
*
* Use this method when storing or retrieving preferences/settings that should be unique
* per device-entity pair. Use get_object_id_hash() when you need a hash that identifies
* the entity regardless of the device it belongs to.
*
* For backward compatibility, if device_id is 0 (the main device), the hash is unchanged
* from previous versions, so existing single-device configurations will continue to work.
*
* @return uint32_t The unique hash for preferences, including device_id if available.
* @deprecated Use make_entity_preference<T>() instead, or preferences won't be migrated.
* See https://github.com/esphome/backlog/issues/85
*/
ESPDEPRECATED("Use make_entity_preference<T>() instead, or preferences won't be migrated. "
"See https://github.com/esphome/backlog/issues/85. Will be removed in 2027.1.0.",
"2026.8.0")
uint32_t get_preference_hash() { return this->old_preference_key_base_(); }
"2026.7.0")
uint32_t get_preference_hash() {
#ifdef USE_DEVICES
// Combine object_id_hash with device_id to ensure uniqueness across devices
// Note: device_id is 0 for the main device, so XORing with 0 preserves the original hash
// This ensures backward compatibility for existing single-device configurations
return this->get_object_id_hash() ^ this->get_device_id();
#else
// Without devices, just use object_id_hash as before
return this->get_object_id_hash();
#endif
}
/// Create a preference object for storing this entity's state/settings.
/// @tparam T The type of data to store (must be trivially copyable)
@@ -223,9 +230,9 @@ class EntityBase {
// before push_back, so codegen can emit a single combined call per entity.
friend class Application;
/// Combined entity setup from codegen: set name, entity key, entity string indices, and flags.
/// Combined entity setup from codegen: set name, object_id hash, entity string indices, and flags.
/// Bit layout of entity_fields is defined by the ENTITY_FIELD_*_SHIFT constants above.
void configure_entity_(const char *name, uint32_t entity_key, uint32_t entity_fields);
void configure_entity_(const char *name, uint32_t object_id_hash, uint32_t entity_fields);
#ifdef USE_DEVICES
// Codegen-only setter — only accessible from setup() via friend declaration.
@@ -233,24 +240,13 @@ class EntityBase {
#endif
/// Non-template helper for make_entity_preference() to avoid code bloat.
/// Migrates preferences from the old sanitized-object_id key to the raw-name key
/// on key-lookup platforms. See: https://github.com/esphome/backlog/issues/85
/// When the preference hash algorithm changes, migration logic goes here.
ESPPreferenceObject make_entity_preference_(size_t size, uint32_t version);
void calc_entity_key_();
/// Reconstruct the OLD (pre-2026.8.0) sanitized-object_id hash for preference keys.
uint32_t calc_old_object_id_hash_() const;
/// Preference key base for this entity: raw-name entity key XOR device_id.
uint32_t preference_key_base_() const { return this->entity_key_ ^ this->get_device_id_or_zero(); }
/// Legacy preference key base: sanitized-object_id hash XOR device_id.
/// Note: device_id is 0 for the main device, so XORing with 0 preserves the original hash.
uint32_t old_preference_key_base_() const { return this->calc_old_object_id_hash_() ^ this->get_device_id_or_zero(); }
void calc_object_id_();
StringRef name_;
uint32_t entity_key_{};
uint32_t object_id_hash_{};
#ifdef USE_DEVICES
Device *device_{};
#endif
+79 -111
View File
@@ -25,86 +25,25 @@ from esphome.core.config import (
from esphome.cpp_generator import MockObj, RawStatement, add, get_variable
from esphome.cpp_types import App
import esphome.final_validate as fv
from esphome.helpers import cpp_string_escape, fnv1_hash_name, sanitize, snake_case
from esphome.helpers import (
cpp_string_escape,
fnv1_hash,
fnv1_hash_object_id,
sanitize,
snake_case,
)
from esphome.types import ConfigType, EntityMetadata
_LOGGER = logging.getLogger(__name__)
DOMAIN = "entity_string_pool"
_OBJECT_ID_DOMAIN = "entity_object_ids"
@dataclass
class ObjectIdEntity:
"""An entity tracked by the sanitized object_id its name resolves to."""
name: str
platform: str
config: ConfigType
def _get_object_id_registry() -> dict[tuple[str, str, str], list[ObjectIdEntity]]:
"""(device_id, platform, sanitized object_id) -> entities resolving to it."""
return CORE.data.setdefault(_OBJECT_ID_DOMAIN, {})
def validate_no_object_id_conflicts(
reason: str,
conflict_filter: Callable[[list[ObjectIdEntity], ConfigType], bool] | None = None,
) -> Callable[[ConfigType], ConfigType]:
"""Create a final-validate step that rejects entities with colliding object_ids.
Entity keys are hashed from the raw name, so names that only differ in characters
lost during sanitizing (for example two UTF-8 names) validate fine in general.
Components that still address entities by the sanitized object_id string must
reject those configs until they are migrated to raw names.
Args:
reason: One sentence stating what the component builds from the object_id,
e.g. "mqtt builds default topics from the entity object_id"
conflict_filter: Optional predicate receiving the colliding entities and the
component config; return False when the component is not affected
Returns:
A validator function for use as (or within) FINAL_VALIDATE_SCHEMA
"""
def validator(config: ConfigType) -> ConfigType:
# Skip in testing_mode, which is used for grouped component testing
if CORE.testing_mode:
return config
conflicts = {
key: entities
for key, entities in _get_object_id_registry().items()
if len(entities) > 1
and (conflict_filter is None or conflict_filter(entities, config))
}
if not conflicts:
return config
lines = [f"{reason}, so these entities would conflict:"]
lines.extend(
f" - {platform} entities "
+ ", ".join(f"'{e.name}'" for e in entities)
+ (f" on device '{device_id}'" if device_id else "")
+ f" share the object_id '{object_id}'"
for (device_id, platform, object_id), entities in conflicts.items()
)
lines.append(
"To fix: Add unique ASCII characters (e.g., '1', '2', or 'A', 'B') "
"to distinguish the names"
)
raise cv.Invalid("\n".join(lines))
return validator
# Private config keys for storing registered string indices
_KEY_DC_IDX = "_entity_dc_idx"
_KEY_UOM_IDX = "_entity_uom_idx"
_KEY_ICON_IDX = "_entity_icon_idx"
_KEY_ENTITY_NAME = "_entity_name"
_KEY_ENTITY_KEY = "_entity_key"
_KEY_OBJECT_ID_HASH = "_entity_object_id_hash"
# Bit layout for entity_fields in configure_entity_().
# Keep in sync with ENTITY_FIELD_*_SHIFT constants in esphome/core/entity_base.h
@@ -367,7 +306,7 @@ def finalize_entity_strings(var: MockObj, config: ConfigType) -> None:
standalone ``var->configure_entity_(name, hash, packed)``.
"""
entity_name = config[_KEY_ENTITY_NAME]
entity_key = config[_KEY_ENTITY_KEY]
object_id_hash = config[_KEY_OBJECT_ID_HASH]
dc_idx = config.get(_KEY_DC_IDX, 0)
uom_idx = config.get(_KEY_UOM_IDX, 0)
icon_idx = config.get(_KEY_ICON_IDX, 0)
@@ -387,30 +326,57 @@ def finalize_entity_strings(var: MockObj, config: ConfigType) -> None:
register_method = config.get(_KEY_REGISTER_METHOD)
if register_method is not None:
expr = getattr(App, f"register_{register_method}")(
var, entity_name, entity_key, packed
var, entity_name, object_id_hash, packed
)
else:
expr = var.configure_entity_(entity_name, entity_key, packed)
expr = var.configure_entity_(entity_name, object_id_hash, packed)
if comment:
add(RawStatement(f"{expr}; // {comment}"))
else:
add(expr)
def get_base_entity_name(
def get_base_entity_object_id(
name: str, friendly_name: str | None, device_name: str | None = None
) -> str:
"""Return the base name whose hash becomes this entity's key on the device.
"""Calculate the base object ID for an entity that will be set via set_object_id().
Follows the name selection in C++ EntityBase::configure_entity_() (entity_base.cpp):
entity name, then sub-device name, then friendly name, then the device name.
This function calculates what object_id_c_str_ should be set to in C++.
This is a config-time approximation for duplicate checking: when
name_add_mac_suffix is enabled the device appends the MAC suffix at runtime,
which is unknown here and identical for every entity on the device, so
ignoring it cannot change whether two entities collide with each other.
The C++ EntityBase::write_object_id_to() (entity_base.cpp) works as:
- If !has_own_name && is_name_add_mac_suffix_enabled():
return str_sanitize(str_snake_case(App.get_friendly_name())) // Dynamic
- Else:
return object_id_c_str_ ?? "" // What we set via set_object_id()
Since we're calculating what to pass to set_object_id(), we always need to
generate the object_id the same way, regardless of name_add_mac_suffix setting.
Args:
name: The entity name (empty string if no name)
friendly_name: The friendly name from CORE.friendly_name
device_name: The device name if entity is on a sub-device
Returns:
The base object ID to use for duplicate checking and to pass to set_object_id()
"""
return name or device_name or friendly_name or CORE.name
if name:
# Entity has its own name (has_own_name will be true)
base_str = name
elif device_name:
# Entity has empty name and is on a sub-device
# C++ EntityBase::set_name() uses device->get_name() when device is set
base_str = device_name
elif friendly_name:
# Entity has empty name (has_own_name will be false)
# C++ uses App.get_friendly_name() which returns friendly_name or device name
base_str = friendly_name
else:
# Fallback to device name
base_str = CORE.name
return sanitize(snake_case(base_str))
def setup_entity(var_or_platform, config=None, platform=None):
@@ -469,15 +435,15 @@ async def _setup_entity_impl(var: MockObj, config: ConfigType, platform: str) ->
device: MockObj = await get_variable(device_id_obj)
add(var.set_device_(device))
# Pre-compute entity name and entity key for configure_entity_()
# Pre-compute entity name and object_id hash for configure_entity_()
# which is emitted later by finalize_entity_strings().
# For named entities: pre-compute the key from the raw entity name
# For empty-name entities: pass 0, C++ calculates the key at runtime from
# device name, friendly_name, or app name
# For named entities: pre-compute hash from entity name
# For empty-name entities: pass 0, C++ calculates hash at runtime from
# device name, friendly_name, or app name (bug-for-bug compatibility)
entity_name = config[CONF_NAME]
entity_key = fnv1_hash_name(entity_name) if entity_name else 0
object_id_hash = fnv1_hash_object_id(entity_name) if entity_name else 0
config[_KEY_ENTITY_NAME] = entity_name
config[_KEY_ENTITY_KEY] = entity_key
config[_KEY_OBJECT_ID_HASH] = object_id_hash
# Store flags for packing into configure_entity_()
config[_KEY_DISABLED_BY_DEFAULT] = int(config[CONF_DISABLED_BY_DEFAULT])
if CONF_INTERNAL in config:
@@ -590,13 +556,16 @@ def entity_duplicate_validator(platform: str) -> Callable[[ConfigType], ConfigTy
# Use the device ID string directly for uniqueness
device_id = device_id_obj.id
# Hash the same raw name the device hashes into the entity key at runtime.
# This handles empty names correctly by using device/friendly names.
base_name = get_base_entity_name(entity_name, CORE.friendly_name, device_name)
name_hash = fnv1_hash_name(base_name)
# Calculate what object_id will actually be used
# This handles empty names correctly by using device/friendly names
name_key = get_base_entity_object_id(
entity_name, CORE.friendly_name, device_name
)
# Check for duplicates: two entities on the same device and platform must not
# share an entity key, since the key is what routes state to API clients
# Check for duplicates by the FNV-1 hash of the object_id, which is the entity
# key that routes state to API clients. This rejects names that sanitize to the
# same object_id, and also two different object_ids whose 32-bit hashes collide.
name_hash = fnv1_hash(name_key)
unique_key = (device_id, platform, name_hash)
if unique_key in CORE.unique_ids:
# Get the existing entity metadata
@@ -621,14 +590,26 @@ def entity_duplicate_validator(platform: str) -> Callable[[ConfigType], ConfigTy
if existing_component != "unknown":
conflict_msg += f" from component '{existing_component}'"
# Different names can only clash here through a genuine hash collision
# Distinguish names that sanitize to the same object_id from a genuine
# 32-bit hash collision between two different object_ids
collision_msg = ""
if entity_name != existing_name:
collision_msg = (
f"\n The names '{entity_name}' and '{existing_name}' produce the"
f"\n same entity key hash ({name_hash:#010x})."
"\n To fix: Rename one of the entities"
existing_object_id = get_base_entity_object_id(
existing_name, CORE.friendly_name, existing_device or None
)
if existing_object_id == name_key:
collision_msg = (
f"\n Original names: '{entity_name}' and '{existing_name}'"
f"\n Both convert to ASCII ID: '{name_key}'"
"\n To fix: Add unique ASCII characters (e.g., '1', '2', or 'A', 'B')"
"\n to distinguish them"
)
else:
collision_msg = (
f"\n The object_ids '{name_key}' and '{existing_object_id}'"
f"\n produce the same entity key hash ({name_hash:#010x})."
"\n To fix: Rename one of the entities"
)
# Skip duplicate entity name validation when testing_mode is enabled
# This flag is used for grouped component testing
@@ -640,19 +621,6 @@ def entity_duplicate_validator(platform: str) -> Callable[[ConfigType], ConfigTy
f"{collision_msg}"
)
# Components that still address entities by the sanitized object_id reject
# colliding names in final validation via validate_no_object_id_conflicts(),
# so track every entity by the object_id its name resolves to. Scoped per
# device and platform to match the strictness configs had before entity keys
# moved to raw names: same-named entities on different sub-devices were
# already accepted then, internal entities were already skipped (above), and
# overlaps between platforms that share an MQTT component type (sensor and
# text_sensor both publish under "sensor") were already possible.
object_id = sanitize(snake_case(base_name))
_get_object_id_registry().setdefault(
(device_id, platform, object_id), []
).append(ObjectIdEntity(base_name, platform, config))
# Store metadata about this entity
entity_metadata: EntityMetadata = {
"name": entity_name,
+4 -25
View File
@@ -809,19 +809,6 @@ constexpr uint32_t FNV1_OFFSET_BASIS = 2166136261UL;
/// FNV-1 32-bit prime
constexpr uint32_t FNV1_PRIME = 16777619UL;
/// Calculate a FNV-1 hash over raw bytes with an explicit length. Unlike fnv1_hash(const char *),
/// each byte is hashed as an unsigned value, so results are platform-independent for bytes >= 0x80.
/// IMPORTANT: Must match Python fnv1_hash_name() in esphome/helpers.py, which hashes the UTF-8
/// encoded bytes of the name. Used to compute entity keys from raw names.
inline uint32_t fnv1_hash_bytes(const char *str, size_t len) {
uint32_t hash = FNV1_OFFSET_BASIS;
for (size_t i = 0; i < len; i++) {
hash *= FNV1_PRIME;
hash ^= static_cast<uint8_t>(str[i]);
}
return hash;
}
/// Extend a FNV-1 hash with an integer (hashes each byte).
template<std::integral T> constexpr uint32_t fnv1_hash_extend(uint32_t hash, T value) {
using UnsignedT = std::make_unsigned_t<T>;
@@ -1026,20 +1013,12 @@ template<size_t N> inline char *str_sanitize_to(char (&buffer)[N], const char *s
// str_sanitize moved to alloc_helpers.h - remove this comment before 2026.11.0
/// Calculate FNV-1 hash of a string while applying snake_case + sanitize transformations.
/// This is the LEGACY entity hash, kept only to reconstruct preference keys that existing
/// devices already have stored; see https://github.com/esphome/backlog/issues/85.
/// With per_code_point set, UTF-8 continuation bytes are skipped so each multi-byte character
/// contributes one underscore — this matches Python fnv1_hash_object_id() in esphome/helpers.py,
/// which produced the hash for named entities. The per-byte form (default) matches the old
/// runtime hash for entities without their own name. Do not change either behavior.
/// Known limitation: Python's lower() is Unicode aware, so the rare code points it maps to a
/// different number of characters or to ASCII (e.g. 'İ', the Kelvin sign) reconstruct wrong;
/// such names skip migration once and fall back to their defaults.
inline uint32_t fnv1_hash_object_id(const char *str, size_t len, bool per_code_point = false) {
/// This computes object_id hashes directly from names without creating an intermediate buffer.
/// 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) {
uint32_t hash = FNV1_OFFSET_BASIS;
for (size_t i = 0; i < len; i++) {
if (per_code_point && (static_cast<uint8_t>(str[i]) & 0xC0) == 0x80)
continue; // UTF-8 continuation byte, already counted via its lead byte
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])));
+7 -7
View File
@@ -24,9 +24,10 @@
#endif
// Key-lookup preference backends find stored data by key; their platforms add the
// USE_PREFERENCE_KEY_LOOKUP define from Python codegen, which enables preference key
// migration. Slot-based backends (ESP8266, RP2040) instead allocate a storage slot for
// every make_preference() call and use the key only as a validity tag on that slot;
// USE_PREFERENCE_KEY_LOOKUP define from Python codegen, which enables one-shot reads
// of stored data by key (the primitive preference key migrations need). Slot-based
// backends (ESP8266, RP2040) instead allocate a storage slot for every
// make_preference() call and use the key only as a validity tag on that slot;
// migration is not possible there, and key collisions cannot corrupt data.
namespace esphome {
@@ -104,10 +105,9 @@ concept PreferencesContract = requires(T prefs, size_t len, uint32_t type, bool
};
// Key-lookup platforms additionally provide load_from_key(), a one-shot read
// of a stored preference by key that migrate_preference() relies on; see the
// key-lookup note at the top of this file. Not part of PreferencesContract,
// so it is asserted in preferences.h only where USE_PREFERENCE_KEY_LOOKUP
// is set.
// of a stored preference by key; see the key-lookup note at the top of this
// file. Not part of PreferencesContract, so it is asserted in preferences.h
// only where USE_PREFERENCE_KEY_LOOKUP is set.
template<typename T>
concept PreferencesKeyLookupContract = requires(T prefs, uint32_t type, uint8_t *data, size_t len) {
{ prefs.load_from_key(type, data, len) } -> std::same_as<bool>;
-25
View File
@@ -1,25 +0,0 @@
#include "esphome/core/preferences.h"
#include "esphome/core/log.h"
#include <cinttypes>
namespace esphome {
#ifdef USE_PREFERENCE_KEY_LOOKUP
static const char *const TAG = "preferences";
bool migrate_preference(ESPPreferenceObject &new_pref, uint8_t *scratch, size_t size, uint32_t old_key,
uint32_t new_key) {
if (new_pref.load(scratch, size))
return true; // Current data present - never overwrite newer data with the old copy
// One-shot read by key: no backend is allocated for the old key, so boots with
// nothing to migrate (for example fresh installs) cost no heap
if (old_key == new_key || !global_preferences->load_from_key(old_key, scratch, size))
return false; // No data stored under the old key, nothing to migrate
if (!new_pref.save(scratch, size)) {
ESP_LOGW(TAG, "Pref migration %" PRIx32 " -> %" PRIx32 " failed", old_key, new_key);
}
return true;
}
#endif // USE_PREFERENCE_KEY_LOOKUP
} // namespace esphome
-12
View File
@@ -56,17 +56,5 @@ namespace esphome {
static_assert(PreferencesKeyLookupContract<ESPPreferences>,
"This platform emits USE_PREFERENCE_KEY_LOOKUP but its preferences manager does not provide "
"load_from_key() (esphome/core/preference_backend.h)");
/// Copy preference data stored under old_key into new_pref (created for new_key) if the keys
/// differ and new_pref has no data yet. scratch must hold at least size bytes.
/// Returns true when scratch holds the entity's current data (loaded or just migrated).
/// The old entry is intentionally left in place so a firmware downgrade still finds its data.
/// If saving under the new key fails, callers that consume scratch (like TextSaver) still get
/// valid data for this boot, callers that reload from the preference fall back to their
/// defaults, and the migration simply runs again on the next boot.
/// Only available on key-lookup preference backends; slot-based backends keep their old
/// keys instead. See: https://github.com/esphome/backlog/issues/85
bool migrate_preference(ESPPreferenceObject &new_pref, uint8_t *scratch, size_t size, uint32_t old_key,
uint32_t new_key);
} // namespace esphome
#endif // USE_PREFERENCE_KEY_LOOKUP