Skip to content

Commit 34d6938

Browse files
authored
Fix subentry ID is not updated when renaming the entity ID (home-assistant#157498)
1 parent 4bb8590 commit 34d6938

File tree

3 files changed

+256
-2
lines changed

3 files changed

+256
-2
lines changed

homeassistant/helpers/entity.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1553,7 +1553,9 @@ async def _async_process_registry_update_or_remove(
15531553
# Clear the remove future to handle entity added again after entity id change
15541554
self.__remove_future = None
15551555
self._platform_state = EntityPlatformState.NOT_ADDED
1556-
await self.platform.async_add_entities([self])
1556+
await self.platform.async_add_entities(
1557+
[self], config_subentry_id=registry_entry.config_subentry_id
1558+
)
15571559

15581560
@callback
15591561
def _async_unsubscribe_device_updates(self) -> None:

tests/helpers/snapshots/test_entity.ambr

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,192 @@
11
# serializer version: 1
2+
# name: test_change_entity_id_config_entry[None]
3+
EntityRegistryEntrySnapshot({
4+
'aliases': set({
5+
}),
6+
'area_id': None,
7+
'capabilities': None,
8+
'config_entry_id': <ANY>,
9+
'config_subentry_id': <ANY>,
10+
'device_class': None,
11+
'device_id': <ANY>,
12+
'disabled_by': None,
13+
'domain': 'test_domain',
14+
'entity_category': None,
15+
'entity_id': 'test_domain.test_5678',
16+
'has_entity_name': False,
17+
'hidden_by': None,
18+
'icon': None,
19+
'id': <ANY>,
20+
'labels': set({
21+
}),
22+
'name': None,
23+
'options': dict({
24+
}),
25+
'original_device_class': None,
26+
'original_icon': None,
27+
'original_name': None,
28+
'platform': 'test',
29+
'previous_unique_id': None,
30+
'suggested_object_id': None,
31+
'supported_features': 0,
32+
'translation_key': None,
33+
'unique_id': '5678',
34+
'unit_of_measurement': None,
35+
})
36+
# ---
37+
# name: test_change_entity_id_config_entry[None].1
38+
StateSnapshot({
39+
'attributes': ReadOnlyDict({
40+
}),
41+
'context': <ANY>,
42+
'entity_id': 'test_domain.test_5678',
43+
'last_changed': <ANY>,
44+
'last_reported': <ANY>,
45+
'last_updated': <ANY>,
46+
'state': 'unknown',
47+
})
48+
# ---
49+
# name: test_change_entity_id_config_entry[None].2
50+
EntityRegistryEntrySnapshot({
51+
'aliases': set({
52+
}),
53+
'area_id': None,
54+
'capabilities': None,
55+
'config_entry_id': <ANY>,
56+
'config_subentry_id': <ANY>,
57+
'device_class': None,
58+
'device_id': <ANY>,
59+
'disabled_by': None,
60+
'domain': 'test_domain',
61+
'entity_category': None,
62+
'entity_id': 'test_domain.test2',
63+
'has_entity_name': False,
64+
'hidden_by': None,
65+
'icon': None,
66+
'id': <ANY>,
67+
'labels': set({
68+
}),
69+
'name': None,
70+
'options': dict({
71+
}),
72+
'original_device_class': None,
73+
'original_icon': None,
74+
'original_name': None,
75+
'platform': 'test',
76+
'previous_unique_id': None,
77+
'suggested_object_id': None,
78+
'supported_features': 0,
79+
'translation_key': None,
80+
'unique_id': '5678',
81+
'unit_of_measurement': None,
82+
})
83+
# ---
84+
# name: test_change_entity_id_config_entry[None].3
85+
StateSnapshot({
86+
'attributes': ReadOnlyDict({
87+
}),
88+
'context': <ANY>,
89+
'entity_id': 'test_domain.test2',
90+
'last_changed': <ANY>,
91+
'last_reported': <ANY>,
92+
'last_updated': <ANY>,
93+
'state': 'unknown',
94+
})
95+
# ---
96+
# name: test_change_entity_id_config_entry[mock-subentry-id-1]
97+
EntityRegistryEntrySnapshot({
98+
'aliases': set({
99+
}),
100+
'area_id': None,
101+
'capabilities': None,
102+
'config_entry_id': <ANY>,
103+
'config_subentry_id': <ANY>,
104+
'device_class': None,
105+
'device_id': <ANY>,
106+
'disabled_by': None,
107+
'domain': 'test_domain',
108+
'entity_category': None,
109+
'entity_id': 'test_domain.test_5678',
110+
'has_entity_name': False,
111+
'hidden_by': None,
112+
'icon': None,
113+
'id': <ANY>,
114+
'labels': set({
115+
}),
116+
'name': None,
117+
'options': dict({
118+
}),
119+
'original_device_class': None,
120+
'original_icon': None,
121+
'original_name': None,
122+
'platform': 'test',
123+
'previous_unique_id': None,
124+
'suggested_object_id': None,
125+
'supported_features': 0,
126+
'translation_key': None,
127+
'unique_id': '5678',
128+
'unit_of_measurement': None,
129+
})
130+
# ---
131+
# name: test_change_entity_id_config_entry[mock-subentry-id-1].1
132+
StateSnapshot({
133+
'attributes': ReadOnlyDict({
134+
}),
135+
'context': <ANY>,
136+
'entity_id': 'test_domain.test_5678',
137+
'last_changed': <ANY>,
138+
'last_reported': <ANY>,
139+
'last_updated': <ANY>,
140+
'state': 'unknown',
141+
})
142+
# ---
143+
# name: test_change_entity_id_config_entry[mock-subentry-id-1].2
144+
EntityRegistryEntrySnapshot({
145+
'aliases': set({
146+
}),
147+
'area_id': None,
148+
'capabilities': None,
149+
'config_entry_id': <ANY>,
150+
'config_subentry_id': <ANY>,
151+
'device_class': None,
152+
'device_id': <ANY>,
153+
'disabled_by': None,
154+
'domain': 'test_domain',
155+
'entity_category': None,
156+
'entity_id': 'test_domain.test2',
157+
'has_entity_name': False,
158+
'hidden_by': None,
159+
'icon': None,
160+
'id': <ANY>,
161+
'labels': set({
162+
}),
163+
'name': None,
164+
'options': dict({
165+
}),
166+
'original_device_class': None,
167+
'original_icon': None,
168+
'original_name': None,
169+
'platform': 'test',
170+
'previous_unique_id': None,
171+
'suggested_object_id': None,
172+
'supported_features': 0,
173+
'translation_key': None,
174+
'unique_id': '5678',
175+
'unit_of_measurement': None,
176+
})
177+
# ---
178+
# name: test_change_entity_id_config_entry[mock-subentry-id-1].3
179+
StateSnapshot({
180+
'attributes': ReadOnlyDict({
181+
}),
182+
'context': <ANY>,
183+
'entity_id': 'test_domain.test2',
184+
'last_changed': <ANY>,
185+
'last_reported': <ANY>,
186+
'last_updated': <ANY>,
187+
'state': 'unknown',
188+
})
189+
# ---
2190
# name: test_entity_description_as_dataclass
3191
dict({
4192
'device_class': 'test',

tests/helpers/test_entity.py

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from syrupy.assertion import SnapshotAssertion
1717
import voluptuous as vol
1818

19-
from homeassistant.config_entries import ConfigEntry
19+
from homeassistant.config_entries import ConfigEntry, ConfigSubentryData
2020
from homeassistant.const import (
2121
ATTR_ATTRIBUTION,
2222
ATTR_DEVICE_CLASS,
@@ -1911,6 +1911,70 @@ async def async_will_remove_from_hass(self):
19111911
assert ent._platform_state == entity.EntityPlatformState.ADDED
19121912

19131913

1914+
@pytest.mark.parametrize("config_subentry_id", [None, "mock-subentry-id-1"])
1915+
async def test_change_entity_id_config_entry(
1916+
hass: HomeAssistant,
1917+
entity_registry: er.EntityRegistry,
1918+
snapshot: SnapshotAssertion,
1919+
config_subentry_id: str | None,
1920+
) -> None:
1921+
"""Test changing entity id does not effect the config entry."""
1922+
1923+
class MockEntity(entity.Entity):
1924+
_attr_unique_id = "5678"
1925+
1926+
async def async_setup_entry(
1927+
hass: HomeAssistant,
1928+
config_entry: ConfigEntry,
1929+
async_add_entities: AddConfigEntryEntitiesCallback,
1930+
) -> None:
1931+
"""Mock setup entry method."""
1932+
async_add_entities([MockEntity()], config_subentry_id=config_subentry_id)
1933+
1934+
platform = MockPlatform(async_setup_entry=async_setup_entry)
1935+
config_entry = MockConfigEntry(
1936+
entry_id="super-mock-id",
1937+
subentries_data=[
1938+
ConfigSubentryData(
1939+
data={},
1940+
subentry_id="mock-subentry-id-1",
1941+
subentry_type="test",
1942+
title="Mock title",
1943+
unique_id="test",
1944+
),
1945+
],
1946+
)
1947+
config_entry.add_to_hass(hass)
1948+
entity_platform = MockEntityPlatform(
1949+
hass, platform_name=config_entry.domain, platform=platform
1950+
)
1951+
1952+
assert await entity_platform.async_setup_entry(config_entry)
1953+
await hass.async_block_till_done()
1954+
1955+
ent = entity_registry.async_get(next(iter(hass.states.async_entity_ids())))
1956+
assert ent == snapshot
1957+
# The snapshot check asserts on any (sub)entry ID
1958+
assert ent.config_entry_id == config_entry.entry_id
1959+
assert ent.config_subentry_id == config_subentry_id
1960+
1961+
state = hass.states.async_all()[0]
1962+
assert state == snapshot
1963+
1964+
entity_registry.async_update_entity(
1965+
ent.entity_id, new_entity_id="test_domain.test2"
1966+
)
1967+
await hass.async_block_till_done(wait_background_tasks=True)
1968+
new_ent = entity_registry.async_get("test_domain.test2")
1969+
assert new_ent == snapshot
1970+
# The snapshot check asserts on any (sub)entry ID
1971+
assert new_ent.config_entry_id == config_entry.entry_id
1972+
assert new_ent.config_subentry_id == config_subentry_id
1973+
1974+
new_state = hass.states.get("test_domain.test2")
1975+
assert new_state == snapshot
1976+
1977+
19141978
def test_entity_description_as_dataclass(snapshot: SnapshotAssertion) -> None:
19151979
"""Test EntityDescription behaves like a dataclass."""
19161980

0 commit comments

Comments
 (0)