diff --git a/esphome/components/bl0940/number/calibration_number.cpp b/esphome/components/bl0940/number/calibration_number.cpp index e83c3add1f..5e775004bd 100644 --- a/esphome/components/bl0940/number/calibration_number.cpp +++ b/esphome/components/bl0940/number/calibration_number.cpp @@ -9,7 +9,7 @@ static const char *const TAG = "bl0940.number"; void CalibrationNumber::setup() { float value = 0.0f; if (this->restore_value_) { - this->pref_ = global_preferences->make_preference(this->get_preference_hash()); + this->pref_ = this->make_entity_preference(); if (!this->pref_.load(&value)) { value = 0.0f; } diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 816bd5dfcb..ba6de4ff61 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -360,8 +360,7 @@ void Climate::add_on_control_callback(std::function &&callb static const uint32_t RESTORE_STATE_VERSION = 0x848EA6ADUL; optional Climate::restore_state_() { - this->rtc_ = global_preferences->make_preference(this->get_preference_hash() ^ - RESTORE_STATE_VERSION); + this->rtc_ = this->make_entity_preference(RESTORE_STATE_VERSION); ClimateDeviceRestoreState recovered{}; if (!this->rtc_.load(&recovered)) return {}; diff --git a/esphome/components/cover/cover.cpp b/esphome/components/cover/cover.cpp index 97b8c2213e..88cca077dd 100644 --- a/esphome/components/cover/cover.cpp +++ b/esphome/components/cover/cover.cpp @@ -187,7 +187,7 @@ void Cover::publish_state(bool save) { } } optional Cover::restore_state_() { - this->rtc_ = global_preferences->make_preference(this->get_preference_hash()); + this->rtc_ = this->make_entity_preference(); CoverRestoreState recovered{}; if (!this->rtc_.load(&recovered)) return {}; diff --git a/esphome/components/duty_time/duty_time_sensor.cpp b/esphome/components/duty_time/duty_time_sensor.cpp index f77f1fcf53..561040623d 100644 --- a/esphome/components/duty_time/duty_time_sensor.cpp +++ b/esphome/components/duty_time/duty_time_sensor.cpp @@ -41,7 +41,7 @@ void DutyTimeSensor::setup() { uint32_t seconds = 0; if (this->restore_) { - this->pref_ = global_preferences->make_preference(this->get_preference_hash()); + this->pref_ = this->make_entity_preference(); this->pref_.load(&seconds); } diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index 02fde730eb..a983babe1c 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -227,8 +227,7 @@ void Fan::publish_state() { constexpr uint32_t RESTORE_STATE_VERSION = 0x71700ABA; optional Fan::restore_state_() { FanRestoreState recovered{}; - this->rtc_ = - global_preferences->make_preference(this->get_preference_hash() ^ RESTORE_STATE_VERSION); + this->rtc_ = this->make_entity_preference(RESTORE_STATE_VERSION); bool restored = this->rtc_.load(&recovered); switch (this->restore_mode_) { diff --git a/esphome/components/haier/haier_base.cpp b/esphome/components/haier/haier_base.cpp index cd2673a272..1882aa439e 100644 --- a/esphome/components/haier/haier_base.cpp +++ b/esphome/components/haier/haier_base.cpp @@ -350,8 +350,7 @@ ClimateTraits HaierClimateBase::traits() { return traits_; } void HaierClimateBase::initialization() { constexpr uint32_t restore_settings_version = 0xA77D21EF; - this->base_rtc_ = - global_preferences->make_preference(this->get_preference_hash() ^ restore_settings_version); + this->base_rtc_ = this->make_entity_preference(restore_settings_version); HaierBaseSettings recovered; if (!this->base_rtc_.load(&recovered)) { recovered = {false, true}; diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp index 23d28bfd47..d98d273957 100644 --- a/esphome/components/haier/hon_climate.cpp +++ b/esphome/components/haier/hon_climate.cpp @@ -515,8 +515,7 @@ haier_protocol::HaierMessage HonClimate::get_power_message(bool state) { void HonClimate::initialization() { HaierClimateBase::initialization(); constexpr uint32_t restore_settings_version = 0x57EB59DDUL; - this->hon_rtc_ = - global_preferences->make_preference(this->get_preference_hash() ^ restore_settings_version); + this->hon_rtc_ = this->make_entity_preference(restore_settings_version); HonSettings recovered; if (this->hon_rtc_.load(&recovered)) { this->settings_ = recovered; diff --git a/esphome/components/integration/integration_sensor.cpp b/esphome/components/integration/integration_sensor.cpp index 80c718dc8d..b084801d3b 100644 --- a/esphome/components/integration/integration_sensor.cpp +++ b/esphome/components/integration/integration_sensor.cpp @@ -10,7 +10,7 @@ static const char *const TAG = "integration"; void IntegrationSensor::setup() { if (this->restore_) { - this->pref_ = global_preferences->make_preference(this->get_preference_hash()); + this->pref_ = this->make_entity_preference(); float preference_value = 0; this->pref_.load(&preference_value); this->result_ = preference_value; diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 58d469b2a7..07809023cd 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -184,7 +184,7 @@ static inline bool validate_header_footer(const uint8_t *header_footer, const ui void LD2450Component::setup() { #ifdef USE_NUMBER if (this->presence_timeout_number_ != nullptr) { - this->pref_ = global_preferences->make_preference(this->presence_timeout_number_->get_preference_hash()); + this->pref_ = this->presence_timeout_number_->make_entity_preference(); this->set_presence_timeout(); } #endif diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 91bb2e2f1f..ed86bf58da 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -44,7 +44,7 @@ void LightState::setup() { case LIGHT_RESTORE_DEFAULT_ON: case LIGHT_RESTORE_INVERTED_DEFAULT_OFF: case LIGHT_RESTORE_INVERTED_DEFAULT_ON: - this->rtc_ = global_preferences->make_preference(this->get_preference_hash()); + this->rtc_ = this->make_entity_preference(); // Attempt to load from preferences, else fall back to default values if (!this->rtc_.load(&recovered)) { recovered.state = (this->restore_mode_ == LIGHT_RESTORE_DEFAULT_ON || @@ -57,7 +57,7 @@ void LightState::setup() { break; case LIGHT_RESTORE_AND_OFF: case LIGHT_RESTORE_AND_ON: - this->rtc_ = global_preferences->make_preference(this->get_preference_hash()); + this->rtc_ = this->make_entity_preference(); this->rtc_.load(&recovered); recovered.state = (this->restore_mode_ == LIGHT_RESTORE_AND_ON); break; diff --git a/esphome/components/lvgl/number/lvgl_number.h b/esphome/components/lvgl/number/lvgl_number.h index d9885bc7fb..44409a0ad5 100644 --- a/esphome/components/lvgl/number/lvgl_number.h +++ b/esphome/components/lvgl/number/lvgl_number.h @@ -21,7 +21,7 @@ class LVGLNumber : public number::Number, public Component { void setup() override { float value = this->value_lambda_(); if (this->restore_) { - this->pref_ = global_preferences->make_preference(this->get_preference_hash()); + this->pref_ = this->make_entity_preference(); if (this->pref_.load(&value)) { this->control_lambda_(value); } diff --git a/esphome/components/lvgl/select/lvgl_select.h b/esphome/components/lvgl/select/lvgl_select.h index 70bb3e7bcb..ba03920a88 100644 --- a/esphome/components/lvgl/select/lvgl_select.h +++ b/esphome/components/lvgl/select/lvgl_select.h @@ -20,7 +20,7 @@ class LVGLSelect : public select::Select, public Component { this->set_options_(); if (this->restore_) { size_t index; - this->pref_ = global_preferences->make_preference(this->get_preference_hash()); + this->pref_ = this->make_entity_preference(); if (this->pref_.load(&index)) this->widget_->set_selected_index(index, LV_ANIM_OFF); } diff --git a/esphome/components/opentherm/number/opentherm_number.cpp b/esphome/components/opentherm/number/opentherm_number.cpp index f0c69144c8..bdb02a605c 100644 --- a/esphome/components/opentherm/number/opentherm_number.cpp +++ b/esphome/components/opentherm/number/opentherm_number.cpp @@ -17,7 +17,7 @@ void OpenthermNumber::setup() { if (!this->restore_value_) { value = this->initial_value_; } else { - this->pref_ = global_preferences->make_preference(this->get_preference_hash()); + this->pref_ = this->make_entity_preference(); if (!this->pref_.load(&value)) { if (!std::isnan(this->initial_value_)) { value = this->initial_value_; diff --git a/esphome/components/rotary_encoder/rotary_encoder.cpp b/esphome/components/rotary_encoder/rotary_encoder.cpp index 26e20664f2..c652944120 100644 --- a/esphome/components/rotary_encoder/rotary_encoder.cpp +++ b/esphome/components/rotary_encoder/rotary_encoder.cpp @@ -132,7 +132,7 @@ void RotaryEncoderSensor::setup() { int32_t initial_value = 0; switch (this->restore_mode_) { case ROTARY_ENCODER_RESTORE_DEFAULT_ZERO: - this->rtc_ = global_preferences->make_preference(this->get_preference_hash()); + this->rtc_ = this->make_entity_preference(); if (!this->rtc_.load(&initial_value)) { initial_value = 0; } diff --git a/esphome/components/sensor/automation.h b/esphome/components/sensor/automation.h index 996c7fc9b5..b4de712727 100644 --- a/esphome/components/sensor/automation.h +++ b/esphome/components/sensor/automation.h @@ -39,7 +39,7 @@ class ValueRangeTrigger : public Trigger, public Component { template void set_max(V max) { this->max_ = max; } void setup() override { - this->rtc_ = global_preferences->make_preference(this->parent_->get_preference_hash()); + this->rtc_ = this->parent_->make_entity_preference(); bool initial_state; if (this->rtc_.load(&initial_state)) { this->previous_in_range_ = initial_state; diff --git a/esphome/components/speaker/media_player/speaker_media_player.cpp b/esphome/components/speaker/media_player/speaker_media_player.cpp index 9a3a47bac8..172bc980a8 100644 --- a/esphome/components/speaker/media_player/speaker_media_player.cpp +++ b/esphome/components/speaker/media_player/speaker_media_player.cpp @@ -55,7 +55,7 @@ void SpeakerMediaPlayer::setup() { this->media_control_command_queue_ = xQueueCreate(MEDIA_CONTROLS_QUEUE_LENGTH, sizeof(MediaCallCommand)); - this->pref_ = global_preferences->make_preference(this->get_preference_hash()); + this->pref_ = this->make_entity_preference(); VolumeRestoreState volume_restore_state; if (this->pref_.load(&volume_restore_state)) { diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index 369ee5e6ff..2a60eb042b 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -16,7 +16,7 @@ void SprinklerControllerNumber::setup() { if (!this->restore_value_) { value = this->initial_value_; } else { - this->pref_ = global_preferences->make_preference(this->get_preference_hash()); + this->pref_ = this->make_entity_preference(); if (!this->pref_.load(&value)) { if (!std::isnan(this->initial_value_)) { value = this->initial_value_; diff --git a/esphome/components/switch/switch.cpp b/esphome/components/switch/switch.cpp index 069533fa78..d880139c5f 100644 --- a/esphome/components/switch/switch.cpp +++ b/esphome/components/switch/switch.cpp @@ -34,7 +34,7 @@ optional Switch::get_initial_state() { if (!(restore_mode & RESTORE_MODE_PERSISTENT_MASK)) return {}; - this->rtc_ = global_preferences->make_preference(this->get_preference_hash()); + this->rtc_ = this->make_entity_preference(); bool initial_state; if (!this->rtc_.load(&initial_state)) return {}; diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp index 028d6f0879..1a5aef6b8d 100644 --- a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp @@ -82,7 +82,7 @@ void TemplateAlarmControlPanel::setup() { this->current_state_ = ACP_STATE_DISARMED; if (this->restore_mode_ == ALARM_CONTROL_PANEL_RESTORE_DEFAULT_DISARMED) { uint8_t value; - this->pref_ = global_preferences->make_preference(this->get_preference_hash()); + this->pref_ = this->make_entity_preference(); if (this->pref_.load(&value)) { this->current_state_ = static_cast(value); } diff --git a/esphome/components/template/datetime/template_date.cpp b/esphome/components/template/datetime/template_date.cpp index 303d5ae2b0..be1d875a7e 100644 --- a/esphome/components/template/datetime/template_date.cpp +++ b/esphome/components/template/datetime/template_date.cpp @@ -18,8 +18,7 @@ void TemplateDate::setup() { state = this->initial_value_; } else { datetime::DateEntityRestoreState temp; - this->pref_ = - global_preferences->make_preference(194434030U ^ this->get_preference_hash()); + this->pref_ = this->make_entity_preference(194434030U); if (this->pref_.load(&temp)) { temp.apply(this); return; diff --git a/esphome/components/template/datetime/template_datetime.cpp b/esphome/components/template/datetime/template_datetime.cpp index 81a823f53e..e134f2b654 100644 --- a/esphome/components/template/datetime/template_datetime.cpp +++ b/esphome/components/template/datetime/template_datetime.cpp @@ -18,8 +18,7 @@ void TemplateDateTime::setup() { state = this->initial_value_; } else { datetime::DateTimeEntityRestoreState temp; - this->pref_ = global_preferences->make_preference( - 194434090U ^ this->get_preference_hash()); + this->pref_ = this->make_entity_preference(194434090U); if (this->pref_.load(&temp)) { temp.apply(this); return; diff --git a/esphome/components/template/datetime/template_time.cpp b/esphome/components/template/datetime/template_time.cpp index 21f843dcc7..586e126e3b 100644 --- a/esphome/components/template/datetime/template_time.cpp +++ b/esphome/components/template/datetime/template_time.cpp @@ -18,8 +18,7 @@ void TemplateTime::setup() { state = this->initial_value_; } else { datetime::TimeEntityRestoreState temp; - this->pref_ = - global_preferences->make_preference(194434060U ^ this->get_preference_hash()); + this->pref_ = this->make_entity_preference(194434060U); if (this->pref_.load(&temp)) { temp.apply(this); return; diff --git a/esphome/components/template/number/template_number.cpp b/esphome/components/template/number/template_number.cpp index 76fef82225..885265cf5d 100644 --- a/esphome/components/template/number/template_number.cpp +++ b/esphome/components/template/number/template_number.cpp @@ -13,7 +13,7 @@ void TemplateNumber::setup() { if (!this->restore_value_) { value = this->initial_value_; } else { - this->pref_ = global_preferences->make_preference(this->get_preference_hash()); + this->pref_ = this->make_entity_preference(); if (!this->pref_.load(&value)) { if (!std::isnan(this->initial_value_)) { value = this->initial_value_; diff --git a/esphome/components/template/select/template_select.cpp b/esphome/components/template/select/template_select.cpp index 818abfc1d7..fa34aa9fa7 100644 --- a/esphome/components/template/select/template_select.cpp +++ b/esphome/components/template/select/template_select.cpp @@ -11,7 +11,7 @@ void TemplateSelect::setup() { size_t index = this->initial_option_index_; if (this->restore_value_) { - this->pref_ = global_preferences->make_preference(this->get_preference_hash()); + this->pref_ = this->make_entity_preference(); size_t restored_index; if (this->pref_.load(&restored_index) && this->has_index(restored_index)) { index = restored_index; diff --git a/esphome/components/total_daily_energy/total_daily_energy.cpp b/esphome/components/total_daily_energy/total_daily_energy.cpp index 818696f99b..e7a45a5edf 100644 --- a/esphome/components/total_daily_energy/total_daily_energy.cpp +++ b/esphome/components/total_daily_energy/total_daily_energy.cpp @@ -10,7 +10,7 @@ void TotalDailyEnergy::setup() { float initial_value = 0; if (this->restore_) { - this->pref_ = global_preferences->make_preference(this->get_preference_hash()); + this->pref_ = this->make_entity_preference(); this->pref_.load(&initial_value); } this->publish_state_and_save(initial_value); diff --git a/esphome/components/tuya/number/tuya_number.cpp b/esphome/components/tuya/number/tuya_number.cpp index 44b22167de..fd22e642c6 100644 --- a/esphome/components/tuya/number/tuya_number.cpp +++ b/esphome/components/tuya/number/tuya_number.cpp @@ -8,7 +8,7 @@ static const char *const TAG = "tuya.number"; void TuyaNumber::setup() { if (this->restore_value_) { - this->pref_ = global_preferences->make_preference(this->get_preference_hash()); + this->pref_ = this->make_entity_preference(); } this->parent_->register_listener(this->number_id_, [this](const TuyaDatapoint &datapoint) { diff --git a/esphome/components/valve/valve.cpp b/esphome/components/valve/valve.cpp index a9086747ce..df077e9eb7 100644 --- a/esphome/components/valve/valve.cpp +++ b/esphome/components/valve/valve.cpp @@ -161,7 +161,7 @@ void Valve::publish_state(bool save) { } } optional Valve::restore_state_() { - this->rtc_ = global_preferences->make_preference(this->get_preference_hash()); + this->rtc_ = this->make_entity_preference(); ValveRestoreState recovered{}; if (!this->rtc_.load(&recovered)) return {}; diff --git a/esphome/components/water_heater/water_heater.cpp b/esphome/components/water_heater/water_heater.cpp index 7b947057e1..3f14ad04c6 100644 --- a/esphome/components/water_heater/water_heater.cpp +++ b/esphome/components/water_heater/water_heater.cpp @@ -146,9 +146,7 @@ void WaterHeaterCall::validate_() { } } -void WaterHeater::setup() { - this->pref_ = global_preferences->make_preference(this->get_preference_hash()); -} +void WaterHeater::setup() { this->pref_ = this->make_entity_preference(); } void WaterHeater::publish_state() { auto traits = this->get_traits(); diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index 8508b93411..047cc024c1 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -92,6 +92,33 @@ StringRef EntityBase::get_object_id_to(std::span buf) c uint32_t EntityBase::get_object_id_hash() { return this->object_id_hash_; } +ESPPreferenceObject EntityBase::make_entity_preference_(size_t size, uint32_t version) { + // This helper exists to centralize preference creation so we can fix hash collisions. + // + // PROBLEM: get_preference_hash() uses fnv1_hash on the sanitized object_id, which can + // collide when different entity names sanitize to the same string (e.g., "Living Room" + // and "living_room" both become "living_room"). This causes entities to overwrite + // each other's stored preferences. + // + // FUTURE MIGRATION (when implementing get_preference_hash_v2): + // uint32_t old_key = this->get_preference_hash() ^ version; + // uint32_t new_key = this->get_preference_hash_v2() ^ version; + // if (old_key != new_key) { + // auto old_pref = global_preferences->make_preference(size, old_key); + // auto new_pref = global_preferences->make_preference(size, new_key); + // SmallBufferWithHeapFallback<64> buffer(size); + // if (old_pref.load(buffer.data(), size)) { + // new_pref.save(buffer.data(), size); + // } + // } + // return global_preferences->make_preference(size, new_key); + // + // NOTE: Will need raw load/save methods on ESPPreferenceObject that take uint8_t* and size. + // + uint32_t key = this->get_preference_hash() ^ version; + return global_preferences->make_preference(size, key); +} + std::string EntityBase_DeviceClass::get_device_class() { if (this->device_class_ == nullptr) { return ""; diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index f91bd9b20c..a9aa5e0076 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -6,6 +6,7 @@ #include "string_ref.h" #include "helpers.h" #include "log.h" +#include "preferences.h" #ifdef USE_DEVICES #include "device.h" @@ -151,7 +152,18 @@ class EntityBase { #endif } + /// Create a preference object for storing this entity's state/settings. + /// @tparam T The type of data to store (must be trivially copyable) + /// @param version Optional version hash XORed with preference key (change when struct layout changes) + template ESPPreferenceObject make_entity_preference(uint32_t version = 0) { + return this->make_entity_preference_(sizeof(T), version); + } + protected: + /// Non-template helper for make_entity_preference() to avoid code bloat. + /// When preference hash algorithm changes, migration logic goes here. + ESPPreferenceObject make_entity_preference_(size_t size, uint32_t version); + void calc_object_id_(); StringRef name_;