[core] Let entities choose flash or RTC storage for their preferences (#21017)

Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
Michele Porelli
2026-10-18 06:58:40 +00:00
committed by GitHub
co-authored by J. Nick Koston
parent 0bb78186f2
commit 032a2b7200
3 changed files with 91 additions and 5 deletions
+10 -3
View File
@@ -136,7 +136,7 @@ StringRef EntityBase::get_object_id_to(std::span<char, OBJECT_ID_MAX_LEN> 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
+10 -2
View File
@@ -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<typename T> ESPPreferenceObject make_entity_preference(uint32_t version, bool in_flash) {
static_assert(std::is_trivially_copyable<T>::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_();
@@ -0,0 +1,71 @@
#ifdef USE_HOST
#include <gtest/gtest.h>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#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<uint32_t>(7, in_flash);
ASSERT_TRUE(with_storage.save(&saved));
global_preferences->sync();
uint32_t loaded = 0;
auto without_storage = entity.make_entity_preference<uint32_t>(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<uint32_t>(1, true);
ASSERT_TRUE(v1.save(&saved));
global_preferences->sync();
uint32_t loaded = 0;
auto v2 = entity.make_entity_preference<uint32_t>(2, true);
EXPECT_FALSE(v2.load(&loaded)) << "a different version must give a different key";
}
} // namespace esphome::host::testing
#endif // USE_HOST