mirror of
https://github.com/esphome/esphome.git
synced 2025-09-02 03:12:20 +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:
165
tests/integration/fixtures/multi_device_preferences.yaml
Normal file
165
tests/integration/fixtures/multi_device_preferences.yaml
Normal 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());
|
144
tests/integration/test_multi_device_preferences.py
Normal file
144
tests/integration/test_multi_device_preferences.py
Normal 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']}"
|
||||
)
|
Reference in New Issue
Block a user