mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 06:33:51 +00: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