From 032a2b7200a8895f9821cc6bbbfaa91f18b9ba78 Mon Sep 17 00:00:00 2001 From: Michele Porelli Date: Sat, 17 Oct 2026 23:58:40 -0700 Subject: [PATCH] [core] Let entities choose flash or RTC storage for their preferences (#21017) Co-authored-by: J. Nick Koston --- esphome/core/entity_base.cpp | 13 +++- esphome/core/entity_base.h | 12 +++- .../host/entity_preference_test.cpp | 71 +++++++++++++++++++ 3 files changed, 91 insertions(+), 5 deletions(-) create mode 100644 tests/components/host/entity_preference_test.cpp diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index cbb14a37302..51cdb19eb42 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -136,7 +136,7 @@ StringRef EntityBase::get_object_id_to(std::span buf) c return StringRef(buf.data(), len); } -ESPPreferenceObject EntityBase::make_entity_preference_(size_t size, uint32_t version) { +uint32_t EntityBase::entity_preference_key_(uint32_t version) { // 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 @@ -144,9 +144,16 @@ ESPPreferenceObject EntityBase::make_entity_preference_(size_t size, uint32_t ve // 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; + return this->get_preference_hash() ^ version; #pragma GCC diagnostic pop - return global_preferences->make_preference(size, key); +} + +ESPPreferenceObject EntityBase::make_entity_preference_(size_t size, uint32_t version) { + return global_preferences->make_preference(size, this->entity_preference_key_(version)); +} + +ESPPreferenceObject EntityBase::make_entity_preference_(size_t size, uint32_t version, bool in_flash) { + return global_preferences->make_preference(size, this->entity_preference_key_(version), in_flash); } #ifdef USE_ENTITY_ICON diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 8796e9f067a..3250ad74730 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -190,6 +190,12 @@ class EntityBase { return this->make_entity_preference_(sizeof(T), version); } + /// As make_entity_preference(), stored in flash or RTC memory where the backend supports the choice. + template ESPPreferenceObject make_entity_preference(uint32_t version, bool in_flash) { + static_assert(std::is_trivially_copyable::value, "T must be trivially copyable"); + return this->make_entity_preference_(sizeof(T), version, in_flash); + } + protected: friend void ::setup(); friend void ::original_setup(); @@ -206,9 +212,11 @@ class EntityBase { void set_device_(Device *device) { this->device_ = device; } #endif - /// Non-template helper for make_entity_preference() to avoid code bloat. - /// When the preference hash algorithm changes, migration logic goes here. + /// Non-template helpers for make_entity_preference() to avoid code bloat. ESPPreferenceObject make_entity_preference_(size_t size, uint32_t version); + ESPPreferenceObject make_entity_preference_(size_t size, uint32_t version, bool in_flash); + /// Key for both helpers; preference hash migration goes here. + uint32_t entity_preference_key_(uint32_t version); void calc_object_id_(); diff --git a/tests/components/host/entity_preference_test.cpp b/tests/components/host/entity_preference_test.cpp new file mode 100644 index 00000000000..3ee6f21132d --- /dev/null +++ b/tests/components/host/entity_preference_test.cpp @@ -0,0 +1,71 @@ +#ifdef USE_HOST +#include +#include +#include +#include +#include "esphome/components/host/preferences.h" +#include "esphome/core/application.h" +#include "esphome/core/entity_base.h" + +namespace esphome::host::testing { +namespace fs = std::filesystem; + +/// Same object_id_hash_ means same preference key. +class TestEntity : public EntityBase { + public: + void set_test_id(uint32_t hash) { this->object_id_hash_ = hash; } +}; + +class EntityPreferenceTest : public ::testing::Test { + protected: + void SetUp() override { + this->temp_dir_ = fs::temp_directory_path() / "esphome_entity_preference_test"; + fs::remove_all(this->temp_dir_); + fs::create_directories(this->temp_dir_); + setenv("ESPHOME_PREFDIR", this->temp_dir_.c_str(), 1); + App.pre_setup("test_entity_pref", strlen("test_entity_pref"), "", 0); + setup_preferences(); + // Clear the in-memory store so values do not leak between tests. + host_preferences->reset(); + } + + void TearDown() override { + unsetenv("ESPHOME_PREFDIR"); + fs::remove_all(this->temp_dir_); + } + + fs::path temp_dir_; +}; + +// in_flash must not change the key; host ignores the storage type, so only the key is checked. +TEST_F(EntityPreferenceTest, InFlashOverloadKeepsTheEntityKey) { + TestEntity entity; + entity.set_test_id(0x6001); + for (const bool in_flash : {false, true}) { + const uint32_t saved = in_flash ? 0xF1A5 : 0x0C7C; + auto with_storage = entity.make_entity_preference(7, in_flash); + ASSERT_TRUE(with_storage.save(&saved)); + global_preferences->sync(); + + uint32_t loaded = 0; + auto without_storage = entity.make_entity_preference(7); + ASSERT_TRUE(without_storage.load(&loaded)) << "in_flash=" << in_flash; + EXPECT_EQ(loaded, saved) << "in_flash=" << in_flash; + } +} + +TEST_F(EntityPreferenceTest, InFlashOverloadStillAppliesTheVersion) { + TestEntity entity; + entity.set_test_id(0x6002); + const uint32_t saved = 42; + auto v1 = entity.make_entity_preference(1, true); + ASSERT_TRUE(v1.save(&saved)); + global_preferences->sync(); + + uint32_t loaded = 0; + auto v2 = entity.make_entity_preference(2, true); + EXPECT_FALSE(v2.load(&loaded)) << "a different version must give a different key"; +} + +} // namespace esphome::host::testing +#endif // USE_HOST