1
0
mirror of https://github.com/esphome/esphome.git synced 2025-09-01 10:52:19 +01:00

[core] Fix preference storage to account for device_id (#10333)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
J. Nick Koston
2025-08-25 02:22:16 +02:00
committed by GitHub
parent 6004367ee2
commit d2752b38c9
31 changed files with 369 additions and 30 deletions

View File

@@ -327,7 +327,7 @@ void Climate::add_on_control_callback(std::function<void(ClimateCall &)> &&callb
static const uint32_t RESTORE_STATE_VERSION = 0x848EA6ADUL;
optional<ClimateDeviceRestoreState> Climate::restore_state_() {
this->rtc_ = global_preferences->make_preference<ClimateDeviceRestoreState>(this->get_object_id_hash() ^
this->rtc_ = global_preferences->make_preference<ClimateDeviceRestoreState>(this->get_preference_hash() ^
RESTORE_STATE_VERSION);
ClimateDeviceRestoreState recovered{};
if (!this->rtc_.load(&recovered))

View File

@@ -194,7 +194,7 @@ void Cover::publish_state(bool save) {
}
}
optional<CoverRestoreState> Cover::restore_state_() {
this->rtc_ = global_preferences->make_preference<CoverRestoreState>(this->get_object_id_hash());
this->rtc_ = global_preferences->make_preference<CoverRestoreState>(this->get_preference_hash());
CoverRestoreState recovered{};
if (!this->rtc_.load(&recovered))
return {};

View File

@@ -41,7 +41,7 @@ void DutyTimeSensor::setup() {
uint32_t seconds = 0;
if (this->restore_) {
this->pref_ = global_preferences->make_preference<uint32_t>(this->get_object_id_hash());
this->pref_ = global_preferences->make_preference<uint32_t>(this->get_preference_hash());
this->pref_.load(&seconds);
}

View File

@@ -148,7 +148,8 @@ void Fan::publish_state() {
constexpr uint32_t RESTORE_STATE_VERSION = 0x71700ABA;
optional<FanRestoreState> Fan::restore_state_() {
FanRestoreState recovered{};
this->rtc_ = global_preferences->make_preference<FanRestoreState>(this->get_object_id_hash() ^ RESTORE_STATE_VERSION);
this->rtc_ =
global_preferences->make_preference<FanRestoreState>(this->get_preference_hash() ^ RESTORE_STATE_VERSION);
bool restored = this->rtc_.load(&recovered);
switch (this->restore_mode_) {

View File

@@ -351,7 +351,7 @@ ClimateTraits HaierClimateBase::traits() { return traits_; }
void HaierClimateBase::initialization() {
constexpr uint32_t restore_settings_version = 0xA77D21EF;
this->base_rtc_ =
global_preferences->make_preference<HaierBaseSettings>(this->get_object_id_hash() ^ restore_settings_version);
global_preferences->make_preference<HaierBaseSettings>(this->get_preference_hash() ^ restore_settings_version);
HaierBaseSettings recovered;
if (!this->base_rtc_.load(&recovered)) {
recovered = {false, true};

View File

@@ -516,7 +516,7 @@ void HonClimate::initialization() {
HaierClimateBase::initialization();
constexpr uint32_t restore_settings_version = 0x57EB59DDUL;
this->hon_rtc_ =
global_preferences->make_preference<HonSettings>(this->get_object_id_hash() ^ restore_settings_version);
global_preferences->make_preference<HonSettings>(this->get_preference_hash() ^ restore_settings_version);
HonSettings recovered;
if (this->hon_rtc_.load(&recovered)) {
this->settings_ = recovered;

View File

@@ -10,7 +10,7 @@ static const char *const TAG = "integration";
void IntegrationSensor::setup() {
if (this->restore_) {
this->pref_ = global_preferences->make_preference<float>(this->get_object_id_hash());
this->pref_ = global_preferences->make_preference<float>(this->get_preference_hash());
float preference_value = 0;
this->pref_.load(&preference_value);
this->result_ = preference_value;

View File

@@ -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<float>(this->presence_timeout_number_->get_object_id_hash());
this->pref_ = global_preferences->make_preference<float>(this->presence_timeout_number_->get_preference_hash());
this->set_presence_timeout();
}
#endif

View File

@@ -41,7 +41,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<LightStateRTCState>(this->get_object_id_hash());
this->rtc_ = global_preferences->make_preference<LightStateRTCState>(this->get_preference_hash());
// 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 ||
@@ -54,7 +54,7 @@ void LightState::setup() {
break;
case LIGHT_RESTORE_AND_OFF:
case LIGHT_RESTORE_AND_ON:
this->rtc_ = global_preferences->make_preference<LightStateRTCState>(this->get_object_id_hash());
this->rtc_ = global_preferences->make_preference<LightStateRTCState>(this->get_preference_hash());
this->rtc_.load(&recovered);
recovered.state = (this->restore_mode_ == LIGHT_RESTORE_AND_ON);
break;

View File

@@ -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<float>(this->get_object_id_hash());
this->pref_ = global_preferences->make_preference<float>(this->get_preference_hash());
if (this->pref_.load(&value)) {
this->control_lambda_(value);
}

View File

@@ -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<size_t>(this->get_object_id_hash());
this->pref_ = global_preferences->make_preference<size_t>(this->get_preference_hash());
if (this->pref_.load(&index))
this->widget_->set_selected_index(index, LV_ANIM_OFF);
}

View File

@@ -15,7 +15,7 @@ void ValueRangeTrigger::setup() {
float local_min = this->min_.value(0.0);
float local_max = this->max_.value(0.0);
convert hash = {.from = (local_max - local_min)};
uint32_t myhash = hash.to ^ this->parent_->get_object_id_hash();
uint32_t myhash = hash.to ^ this->parent_->get_preference_hash();
this->rtc_ = global_preferences->make_preference<bool>(myhash);
bool initial_state;
if (this->rtc_.load(&initial_state)) {

View File

@@ -17,7 +17,7 @@ void OpenthermNumber::setup() {
if (!this->restore_value_) {
value = this->initial_value_;
} else {
this->pref_ = global_preferences->make_preference<float>(this->get_object_id_hash());
this->pref_ = global_preferences->make_preference<float>(this->get_preference_hash());
if (!this->pref_.load(&value)) {
if (!std::isnan(this->initial_value_)) {
value = this->initial_value_;

View File

@@ -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<int32_t>(this->get_object_id_hash());
this->rtc_ = global_preferences->make_preference<int32_t>(this->get_preference_hash());
if (!this->rtc_.load(&initial_value)) {
initial_value = 0;
}

View File

@@ -40,7 +40,7 @@ class ValueRangeTrigger : public Trigger<float>, public Component {
template<typename V> void set_max(V max) { this->max_ = max; }
void setup() override {
this->rtc_ = global_preferences->make_preference<bool>(this->parent_->get_object_id_hash());
this->rtc_ = global_preferences->make_preference<bool>(this->parent_->get_preference_hash());
bool initial_state;
if (this->rtc_.load(&initial_state)) {
this->previous_in_range_ = initial_state;

View File

@@ -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<VolumeRestoreState>(this->get_object_id_hash());
this->pref_ = global_preferences->make_preference<VolumeRestoreState>(this->get_preference_hash());
VolumeRestoreState volume_restore_state;
if (this->pref_.load(&volume_restore_state)) {

View File

@@ -81,7 +81,7 @@ void SprinklerControllerNumber::setup() {
if (!this->restore_value_) {
value = this->initial_value_;
} else {
this->pref_ = global_preferences->make_preference<float>(this->get_object_id_hash());
this->pref_ = global_preferences->make_preference<float>(this->get_preference_hash());
if (!this->pref_.load(&value)) {
if (!std::isnan(this->initial_value_)) {
value = this->initial_value_;

View File

@@ -32,7 +32,7 @@ optional<bool> Switch::get_initial_state() {
if (!(restore_mode & RESTORE_MODE_PERSISTENT_MASK))
return {};
this->rtc_ = global_preferences->make_preference<bool>(this->get_object_id_hash());
this->rtc_ = global_preferences->make_preference<bool>(this->get_preference_hash());
bool initial_state;
if (!this->rtc_.load(&initial_state))
return {};

View File

@@ -86,7 +86,7 @@ void TemplateAlarmControlPanel::setup() {
break;
case ALARM_CONTROL_PANEL_RESTORE_DEFAULT_DISARMED: {
uint8_t value;
this->pref_ = global_preferences->make_preference<uint8_t>(this->get_object_id_hash());
this->pref_ = global_preferences->make_preference<uint8_t>(this->get_preference_hash());
if (this->pref_.load(&value)) {
this->current_state_ = static_cast<alarm_control_panel::AlarmControlPanelState>(value);
} else {

View File

@@ -20,7 +20,7 @@ void TemplateDate::setup() {
} else {
datetime::DateEntityRestoreState temp;
this->pref_ =
global_preferences->make_preference<datetime::DateEntityRestoreState>(194434030U ^ this->get_object_id_hash());
global_preferences->make_preference<datetime::DateEntityRestoreState>(194434030U ^ this->get_preference_hash());
if (this->pref_.load(&temp)) {
temp.apply(this);
return;

View File

@@ -19,8 +19,8 @@ void TemplateDateTime::setup() {
state = this->initial_value_;
} else {
datetime::DateTimeEntityRestoreState temp;
this->pref_ = global_preferences->make_preference<datetime::DateTimeEntityRestoreState>(194434090U ^
this->get_object_id_hash());
this->pref_ = global_preferences->make_preference<datetime::DateTimeEntityRestoreState>(
194434090U ^ this->get_preference_hash());
if (this->pref_.load(&temp)) {
temp.apply(this);
return;

View File

@@ -20,7 +20,7 @@ void TemplateTime::setup() {
} else {
datetime::TimeEntityRestoreState temp;
this->pref_ =
global_preferences->make_preference<datetime::TimeEntityRestoreState>(194434060U ^ this->get_object_id_hash());
global_preferences->make_preference<datetime::TimeEntityRestoreState>(194434060U ^ this->get_preference_hash());
if (this->pref_.load(&temp)) {
temp.apply(this);
return;

View File

@@ -14,7 +14,7 @@ void TemplateNumber::setup() {
if (!this->restore_value_) {
value = this->initial_value_;
} else {
this->pref_ = global_preferences->make_preference<float>(this->get_object_id_hash());
this->pref_ = global_preferences->make_preference<float>(this->get_preference_hash());
if (!this->pref_.load(&value)) {
if (!std::isnan(this->initial_value_)) {
value = this->initial_value_;

View File

@@ -16,7 +16,7 @@ void TemplateSelect::setup() {
ESP_LOGD(TAG, "State from initial: %s", value.c_str());
} else {
size_t index;
this->pref_ = global_preferences->make_preference<size_t>(this->get_object_id_hash());
this->pref_ = global_preferences->make_preference<size_t>(this->get_preference_hash());
if (!this->pref_.load(&index)) {
value = this->initial_option_;
ESP_LOGD(TAG, "State from initial (could not load stored index): %s", value.c_str());

View File

@@ -15,7 +15,7 @@ void TemplateText::setup() {
if (!this->pref_) {
ESP_LOGD(TAG, "State from initial: %s", value.c_str());
} else {
uint32_t key = this->get_object_id_hash();
uint32_t key = this->get_preference_hash();
key += this->traits.get_min_length() << 2;
key += this->traits.get_max_length() << 4;
key += fnv1_hash(this->traits.get_pattern()) << 6;

View File

@@ -10,7 +10,7 @@ void TotalDailyEnergy::setup() {
float initial_value = 0;
if (this->restore_) {
this->pref_ = global_preferences->make_preference<float>(this->get_object_id_hash());
this->pref_ = global_preferences->make_preference<float>(this->get_preference_hash());
this->pref_.load(&initial_value);
}
this->publish_state_and_save(initial_value);

View File

@@ -8,7 +8,7 @@ static const char *const TAG = "tuya.number";
void TuyaNumber::setup() {
if (this->restore_value_) {
this->pref_ = global_preferences->make_preference<float>(this->get_object_id_hash());
this->pref_ = global_preferences->make_preference<float>(this->get_preference_hash());
}
this->parent_->register_listener(this->number_id_, [this](const TuyaDatapoint &datapoint) {

View File

@@ -155,7 +155,7 @@ void Valve::publish_state(bool save) {
}
}
optional<ValveRestoreState> Valve::restore_state_() {
this->rtc_ = global_preferences->make_preference<ValveRestoreState>(this->get_object_id_hash());
this->rtc_ = global_preferences->make_preference<ValveRestoreState>(this->get_preference_hash());
ValveRestoreState recovered{};
if (!this->rtc_.load(&recovered))
return {};

View File

@@ -85,6 +85,35 @@ class EntityBase {
// Set has_state - for components that need to manually set this
void set_has_state(bool state) { this->flags_.has_state = state; }
/**
* @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.
*/
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
}
protected:
friend class api::APIConnection;

View File

@@ -0,0 +1,165 @@
esphome:
name: multi-device-preferences-test
# Define multiple devices for testing preference storage
devices:
- id: device_a
name: Device A
- id: device_b
name: Device B
host:
api: # Port will be automatically injected
logger:
level: DEBUG
# Test entities with restore modes to verify preference storage
# Switches with same name on different devices - test restore mode
switch:
- platform: template
name: Light
id: light_device_a
device_id: device_a
restore_mode: RESTORE_DEFAULT_OFF
turn_on_action:
- lambda: |-
ESP_LOGI("test", "Device A Light turned ON");
turn_off_action:
- lambda: |-
ESP_LOGI("test", "Device A Light turned OFF");
- platform: template
name: Light
id: light_device_b
device_id: device_b
restore_mode: RESTORE_DEFAULT_ON # Different default to test uniqueness
turn_on_action:
- lambda: |-
ESP_LOGI("test", "Device B Light turned ON");
turn_off_action:
- lambda: |-
ESP_LOGI("test", "Device B Light turned OFF");
- platform: template
name: Light
id: light_main
restore_mode: RESTORE_DEFAULT_OFF
turn_on_action:
- lambda: |-
ESP_LOGI("test", "Main Light turned ON");
turn_off_action:
- lambda: |-
ESP_LOGI("test", "Main Light turned OFF");
# Numbers with restore to test preference storage
number:
- platform: template
name: Setpoint
id: setpoint_device_a
device_id: device_a
min_value: 10.0
max_value: 30.0
step: 0.5
restore_value: true
initial_value: 20.0
set_action:
- lambda: |-
ESP_LOGI("test", "Device A Setpoint set to %.1f", x);
id(setpoint_device_a).state = x;
- platform: template
name: Setpoint
id: setpoint_device_b
device_id: device_b
min_value: 10.0
max_value: 30.0
step: 0.5
restore_value: true
initial_value: 25.0 # Different initial to test uniqueness
set_action:
- lambda: |-
ESP_LOGI("test", "Device B Setpoint set to %.1f", x);
id(setpoint_device_b).state = x;
- platform: template
name: Setpoint
id: setpoint_main
min_value: 10.0
max_value: 30.0
step: 0.5
restore_value: true
initial_value: 22.0
set_action:
- lambda: |-
ESP_LOGI("test", "Main Setpoint set to %.1f", x);
id(setpoint_main).state = x;
# Selects with restore to test preference storage
select:
- platform: template
name: Mode
id: mode_device_a
device_id: device_a
options:
- "Auto"
- "Manual"
- "Off"
restore_value: true
initial_option: "Auto"
set_action:
- lambda: |-
ESP_LOGI("test", "Device A Mode set to %s", x.c_str());
id(mode_device_a).state = x;
- platform: template
name: Mode
id: mode_device_b
device_id: device_b
options:
- "Auto"
- "Manual"
- "Off"
restore_value: true
initial_option: "Manual" # Different initial to test uniqueness
set_action:
- lambda: |-
ESP_LOGI("test", "Device B Mode set to %s", x.c_str());
id(mode_device_b).state = x;
- platform: template
name: Mode
id: mode_main
options:
- "Auto"
- "Manual"
- "Off"
restore_value: true
initial_option: "Off"
set_action:
- lambda: |-
ESP_LOGI("test", "Main Mode set to %s", x.c_str());
id(mode_main).state = x;
# Button to trigger preference logging test
button:
- platform: template
name: Test Preferences
on_press:
- lambda: |-
ESP_LOGI("test", "Testing preference storage uniqueness:");
ESP_LOGI("test", "Device A Light state: %s", id(light_device_a).state ? "ON" : "OFF");
ESP_LOGI("test", "Device B Light state: %s", id(light_device_b).state ? "ON" : "OFF");
ESP_LOGI("test", "Main Light state: %s", id(light_main).state ? "ON" : "OFF");
ESP_LOGI("test", "Device A Setpoint: %.1f", id(setpoint_device_a).state);
ESP_LOGI("test", "Device B Setpoint: %.1f", id(setpoint_device_b).state);
ESP_LOGI("test", "Main Setpoint: %.1f", id(setpoint_main).state);
ESP_LOGI("test", "Device A Mode: %s", id(mode_device_a).state.c_str());
ESP_LOGI("test", "Device B Mode: %s", id(mode_device_b).state.c_str());
ESP_LOGI("test", "Main Mode: %s", id(mode_main).state.c_str());
// Log preference hashes for entities that actually store preferences
ESP_LOGI("test", "Device A Switch Pref Hash: %u", id(light_device_a).get_preference_hash());
ESP_LOGI("test", "Device B Switch Pref Hash: %u", id(light_device_b).get_preference_hash());
ESP_LOGI("test", "Main Switch Pref Hash: %u", id(light_main).get_preference_hash());
ESP_LOGI("test", "Device A Number Pref Hash: %u", id(setpoint_device_a).get_preference_hash());
ESP_LOGI("test", "Device B Number Pref Hash: %u", id(setpoint_device_b).get_preference_hash());
ESP_LOGI("test", "Main Number Pref Hash: %u", id(setpoint_main).get_preference_hash());

View File

@@ -0,0 +1,144 @@
"""Test multi-device preference storage functionality."""
from __future__ import annotations
import asyncio
import re
from aioesphomeapi import ButtonInfo, NumberInfo, SelectInfo, SwitchInfo
import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_multi_device_preferences(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test that entities with same names on different devices have unique preference storage."""
loop = asyncio.get_running_loop()
log_lines: list[str] = []
preferences_logged = loop.create_future()
# Patterns to match preference hash logs
switch_hash_pattern_device = re.compile(r"Device ([AB]) Switch Pref Hash: (\d+)")
switch_hash_pattern_main = re.compile(r"Main Switch Pref Hash: (\d+)")
number_hash_pattern_device = re.compile(r"Device ([AB]) Number Pref Hash: (\d+)")
number_hash_pattern_main = re.compile(r"Main Number Pref Hash: (\d+)")
switch_hashes: dict[str, int] = {}
number_hashes: dict[str, int] = {}
def check_output(line: str) -> None:
"""Check log output for preference hash information."""
log_lines.append(line)
# Look for device switch preference hash logs
match = switch_hash_pattern_device.search(line)
if match:
device = match.group(1)
hash_value = int(match.group(2))
switch_hashes[device] = hash_value
# Look for main switch preference hash
match = switch_hash_pattern_main.search(line)
if match:
hash_value = int(match.group(1))
switch_hashes["Main"] = hash_value
# Look for device number preference hash logs
match = number_hash_pattern_device.search(line)
if match:
device = match.group(1)
hash_value = int(match.group(2))
number_hashes[device] = hash_value
# Look for main number preference hash
match = number_hash_pattern_main.search(line)
if match:
hash_value = int(match.group(1))
number_hashes["Main"] = hash_value
# If we have all hashes, complete the future
if (
len(switch_hashes) == 3
and len(number_hashes) == 3
and not preferences_logged.done()
):
preferences_logged.set_result(True)
async with (
run_compiled(yaml_config, line_callback=check_output),
api_client_connected() as client,
):
# Get entity list
entities, _ = await client.list_entities_services()
# Verify we have the expected entities with duplicate names on different devices
# Check switches (3 with name "Light")
switches = [
e for e in entities if isinstance(e, SwitchInfo) and e.name == "Light"
]
assert len(switches) == 3, f"Expected 3 'Light' switches, got {len(switches)}"
# Check numbers (3 with name "Setpoint")
numbers = [
e for e in entities if isinstance(e, NumberInfo) and e.name == "Setpoint"
]
assert len(numbers) == 3, f"Expected 3 'Setpoint' numbers, got {len(numbers)}"
# Check selects (3 with name "Mode")
selects = [
e for e in entities if isinstance(e, SelectInfo) and e.name == "Mode"
]
assert len(selects) == 3, f"Expected 3 'Mode' selects, got {len(selects)}"
# Find the test button entity to trigger preference logging
buttons = [e for e in entities if isinstance(e, ButtonInfo)]
test_button = next((b for b in buttons if b.name == "Test Preferences"), None)
assert test_button is not None, "Test Preferences button not found"
# Press the button to trigger logging
client.button_command(test_button.key)
# Wait for preference hashes to be logged
try:
await asyncio.wait_for(preferences_logged, timeout=5.0)
except TimeoutError:
pytest.fail("Preference hashes not logged within timeout")
# Verify all switch preference hashes are unique
assert len(switch_hashes) == 3, (
f"Expected 3 devices with switches, got {switch_hashes}"
)
switch_hash_values = list(switch_hashes.values())
assert len(switch_hash_values) == len(set(switch_hash_values)), (
f"Switch preference hashes are not unique: {switch_hashes}"
)
# Verify all number preference hashes are unique
assert len(number_hashes) == 3, (
f"Expected 3 devices with numbers, got {number_hashes}"
)
number_hash_values = list(number_hashes.values())
assert len(number_hash_values) == len(set(number_hash_values)), (
f"Number preference hashes are not unique: {number_hashes}"
)
# Verify Device A and Device B have different hashes (they have device_id set)
assert switch_hashes["A"] != switch_hashes["B"], (
f"Device A and B switches should have different hashes: A={switch_hashes['A']}, B={switch_hashes['B']}"
)
assert number_hashes["A"] != number_hashes["B"], (
f"Device A and B numbers should have different hashes: A={number_hashes['A']}, B={number_hashes['B']}"
)
# Verify Main device hash is different from both A and B
assert switch_hashes["Main"] != switch_hashes["A"], (
f"Main and Device A switches should have different hashes: Main={switch_hashes['Main']}, A={switch_hashes['A']}"
)
assert switch_hashes["Main"] != switch_hashes["B"], (
f"Main and Device B switches should have different hashes: Main={switch_hashes['Main']}, B={switch_hashes['B']}"
)