Skip to content

Commit 3b907df

Browse files
rsnodgrassSage-Ox
andcommitted
Add entity ID stability tests to prevent regressions
Co-Authored-By: SageOx <ox@sageox.ai>
1 parent 03e0605 commit 3b907df

File tree

1 file changed

+152
-0
lines changed

1 file changed

+152
-0
lines changed

tests/test_entity_id_stability.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
"""Tests to prevent entity ID regressions across versions.
2+
3+
WARNING: These tests protect user installations. Changing unique_id formats
4+
breaks automations, dashboards, and entity history. You MUST provide a
5+
migration path if changes are absolutely necessary.
6+
"""
7+
8+
import pytest
9+
10+
DOMAIN = 'lunos'
11+
12+
# Golden unique_id format documentation
13+
# These patterns are part of the public API - do not change without migration
14+
#
15+
# Fan entity unique_id format:
16+
# Pattern: {relay_w1}_{relay_w2}
17+
# Example: switch.lunos_w1_switch.lunos_w2
18+
#
19+
# The unique_id is constructed from the two relay entity IDs that control
20+
# the LUNOS fan speed (W1 and W2). This creates a stable identifier that
21+
# persists across Home Assistant restarts.
22+
GOLDEN_FORMATS = {
23+
'fan': '{relay_w1}_{relay_w2}',
24+
}
25+
26+
27+
def generate_fan_unique_id(relay_w1: str, relay_w2: str) -> str:
28+
"""Generate unique_id using same logic as LUNOSFan.__init__.
29+
30+
Mirrors: custom_components/lunos/fan.py line 155
31+
self._attr_unique_id = f'{relay_w1}_{relay_w2}'
32+
"""
33+
return f'{relay_w1}_{relay_w2}'
34+
35+
36+
class TestFanUniqueIdStability:
37+
"""Test fan entity unique_id format stability."""
38+
39+
@pytest.mark.parametrize(
40+
'relay_w1,relay_w2,expected',
41+
[
42+
# standard switch entities
43+
('switch.lunos_w1', 'switch.lunos_w2', 'switch.lunos_w1_switch.lunos_w2'),
44+
# different naming conventions
45+
('switch.bedroom_relay_1', 'switch.bedroom_relay_2', 'switch.bedroom_relay_1_switch.bedroom_relay_2'),
46+
# light entities (zigbee relays often appear as lights)
47+
('light.lunos_relay_w1', 'light.lunos_relay_w2', 'light.lunos_relay_w1_light.lunos_relay_w2'),
48+
# mixed domains (unusual but valid)
49+
('switch.w1_relay', 'light.w2_relay', 'switch.w1_relay_light.w2_relay'),
50+
# numeric suffixes
51+
('switch.relay_1', 'switch.relay_2', 'switch.relay_1_switch.relay_2'),
52+
# underscores in entity names
53+
('switch.living_room_lunos_w1', 'switch.living_room_lunos_w2', 'switch.living_room_lunos_w1_switch.living_room_lunos_w2'),
54+
],
55+
)
56+
def test_format_stability(self, relay_w1: str, relay_w2: str, expected: str):
57+
"""Ensure fan unique_id format matches golden values.
58+
59+
WARNING: If this test fails, you are about to break user automations,
60+
dashboards, and entity history. You MUST provide a migration path.
61+
"""
62+
result = generate_fan_unique_id(relay_w1, relay_w2)
63+
assert result == expected, (
64+
f'BREAKING CHANGE: fan unique_id format changed!\n'
65+
f' Relay W1: {relay_w1}\n'
66+
f' Relay W2: {relay_w2}\n'
67+
f' Expected: {expected}\n'
68+
f' Got: {result}\n'
69+
f'This will break existing user installations.'
70+
)
71+
72+
def test_unique_id_uses_full_entity_id(self):
73+
"""Verify unique_id includes full entity ID with domain prefix.
74+
75+
The unique_id must use the complete entity ID (domain.name) to ensure
76+
uniqueness when users have multiple LUNOS installations with similar
77+
naming patterns.
78+
"""
79+
relay_w1 = 'switch.lunos_w1'
80+
relay_w2 = 'switch.lunos_w2'
81+
result = generate_fan_unique_id(relay_w1, relay_w2)
82+
83+
# must include domain prefix
84+
assert result.startswith('switch.')
85+
assert '_switch.' in result
86+
87+
def test_unique_id_preserves_case(self):
88+
"""Verify unique_id preserves entity ID case exactly.
89+
90+
Entity IDs in Home Assistant are case-sensitive for matching purposes.
91+
The unique_id should preserve the exact case of the relay entity IDs.
92+
"""
93+
relay_w1 = 'switch.LUNOS_W1'
94+
relay_w2 = 'switch.LUNOS_W2'
95+
result = generate_fan_unique_id(relay_w1, relay_w2)
96+
97+
assert result == 'switch.LUNOS_W1_switch.LUNOS_W2'
98+
99+
def test_unique_id_is_deterministic(self):
100+
"""Verify same inputs always produce same unique_id.
101+
102+
Critical for entity registry persistence across restarts.
103+
"""
104+
relay_w1 = 'switch.lunos_w1'
105+
relay_w2 = 'switch.lunos_w2'
106+
107+
results = [generate_fan_unique_id(relay_w1, relay_w2) for _ in range(10)]
108+
assert len(set(results)) == 1, 'unique_id generation is not deterministic'
109+
110+
def test_different_relays_produce_different_ids(self):
111+
"""Verify different relay combinations produce unique IDs.
112+
113+
Essential for multi-fan installations where each LUNOS unit uses
114+
different relay pairs.
115+
"""
116+
id1 = generate_fan_unique_id('switch.w1_a', 'switch.w2_a')
117+
id2 = generate_fan_unique_id('switch.w1_b', 'switch.w2_b')
118+
id3 = generate_fan_unique_id('switch.w1_a', 'switch.w2_b')
119+
120+
assert id1 != id2
121+
assert id1 != id3
122+
assert id2 != id3
123+
124+
125+
class TestDeviceIdentifierStability:
126+
"""Test device registry identifier format stability.
127+
128+
Device identifiers use the same unique_id as the fan entity.
129+
See fan.py device_info property.
130+
"""
131+
132+
@pytest.mark.parametrize(
133+
'relay_w1,relay_w2',
134+
[
135+
('switch.lunos_w1', 'switch.lunos_w2'),
136+
('switch.bedroom_w1', 'switch.bedroom_w2'),
137+
('light.relay_1', 'light.relay_2'),
138+
],
139+
)
140+
def test_device_identifier_matches_unique_id(self, relay_w1: str, relay_w2: str):
141+
"""Verify device identifier uses same format as entity unique_id.
142+
143+
The device_info property creates identifiers as:
144+
identifiers={(DOMAIN, self._attr_unique_id)}
145+
146+
This must stay in sync with entity unique_id generation.
147+
"""
148+
unique_id = generate_fan_unique_id(relay_w1, relay_w2)
149+
device_identifier = (DOMAIN, unique_id)
150+
151+
assert device_identifier[0] == DOMAIN
152+
assert device_identifier[1] == unique_id

0 commit comments

Comments
 (0)