Skip to content

Commit 2dca78e

Browse files
authored
Improve entity registry handling of device changes (home-assistant#148425)
1 parent e0179a7 commit 2dca78e

File tree

4 files changed

+87
-42
lines changed

4 files changed

+87
-42
lines changed

homeassistant/helpers/device_registry.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -144,13 +144,21 @@ class DeviceInfo(TypedDict, total=False):
144144
LOW_PRIO_CONFIG_ENTRY_DOMAINS = {"homekit_controller", "matter", "mqtt", "upnp"}
145145

146146

147-
class _EventDeviceRegistryUpdatedData_CreateRemove(TypedDict):
148-
"""EventDeviceRegistryUpdated data for action type 'create' and 'remove'."""
147+
class _EventDeviceRegistryUpdatedData_Create(TypedDict):
148+
"""EventDeviceRegistryUpdated data for action type 'create'."""
149149

150-
action: Literal["create", "remove"]
150+
action: Literal["create"]
151151
device_id: str
152152

153153

154+
class _EventDeviceRegistryUpdatedData_Remove(TypedDict):
155+
"""EventDeviceRegistryUpdated data for action type 'remove'."""
156+
157+
action: Literal["remove"]
158+
device_id: str
159+
device: DeviceEntry
160+
161+
154162
class _EventDeviceRegistryUpdatedData_Update(TypedDict):
155163
"""EventDeviceRegistryUpdated data for action type 'update'."""
156164

@@ -160,7 +168,8 @@ class _EventDeviceRegistryUpdatedData_Update(TypedDict):
160168

161169

162170
type EventDeviceRegistryUpdatedData = (
163-
_EventDeviceRegistryUpdatedData_CreateRemove
171+
_EventDeviceRegistryUpdatedData_Create
172+
| _EventDeviceRegistryUpdatedData_Remove
164173
| _EventDeviceRegistryUpdatedData_Update
165174
)
166175

@@ -1309,8 +1318,8 @@ def async_remove_device(self, device_id: str) -> None:
13091318
self.async_update_device(other_device.id, via_device_id=None)
13101319
self.hass.bus.async_fire_internal(
13111320
EVENT_DEVICE_REGISTRY_UPDATED,
1312-
_EventDeviceRegistryUpdatedData_CreateRemove(
1313-
action="remove", device_id=device_id
1321+
_EventDeviceRegistryUpdatedData_Remove(
1322+
action="remove", device_id=device_id, device=device
13141323
),
13151324
)
13161325
self.async_schedule_save()

homeassistant/helpers/entity_registry.py

Lines changed: 39 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1103,8 +1103,17 @@ def async_device_modified(
11031103
entities = async_entries_for_device(
11041104
self, event.data["device_id"], include_disabled_entities=True
11051105
)
1106+
removed_device = event.data["device"]
11061107
for entity in entities:
1107-
self.async_remove(entity.entity_id)
1108+
config_entry_id = entity.config_entry_id
1109+
if (
1110+
config_entry_id in removed_device.config_entries
1111+
and entity.config_subentry_id
1112+
in removed_device.config_entries_subentries[config_entry_id]
1113+
):
1114+
self.async_remove(entity.entity_id)
1115+
else:
1116+
self.async_update_entity(entity.entity_id, device_id=None)
11081117
return
11091118

11101119
if event.data["action"] != "update":
@@ -1121,29 +1130,38 @@ def async_device_modified(
11211130

11221131
# Remove entities which belong to config entries no longer associated with the
11231132
# device
1124-
entities = async_entries_for_device(
1125-
self, event.data["device_id"], include_disabled_entities=True
1126-
)
1127-
for entity in entities:
1128-
if (
1129-
entity.config_entry_id is not None
1130-
and entity.config_entry_id not in device.config_entries
1131-
):
1132-
self.async_remove(entity.entity_id)
1133+
if old_config_entries := event.data["changes"].get("config_entries"):
1134+
entities = async_entries_for_device(
1135+
self, event.data["device_id"], include_disabled_entities=True
1136+
)
1137+
for entity in entities:
1138+
config_entry_id = entity.config_entry_id
1139+
if (
1140+
entity.config_entry_id in old_config_entries
1141+
and entity.config_entry_id not in device.config_entries
1142+
):
1143+
self.async_remove(entity.entity_id)
11331144

11341145
# Remove entities which belong to config subentries no longer associated with the
11351146
# device
1136-
entities = async_entries_for_device(
1137-
self, event.data["device_id"], include_disabled_entities=True
1138-
)
1139-
for entity in entities:
1140-
if (
1141-
(config_entry_id := entity.config_entry_id) is not None
1142-
and config_entry_id in device.config_entries
1143-
and entity.config_subentry_id
1144-
not in device.config_entries_subentries[config_entry_id]
1145-
):
1146-
self.async_remove(entity.entity_id)
1147+
if old_config_entries_subentries := event.data["changes"].get(
1148+
"config_entries_subentries"
1149+
):
1150+
entities = async_entries_for_device(
1151+
self, event.data["device_id"], include_disabled_entities=True
1152+
)
1153+
for entity in entities:
1154+
config_entry_id = entity.config_entry_id
1155+
config_subentry_id = entity.config_subentry_id
1156+
if (
1157+
config_entry_id in device.config_entries
1158+
and config_entry_id in old_config_entries_subentries
1159+
and config_subentry_id
1160+
in old_config_entries_subentries[config_entry_id]
1161+
and config_subentry_id
1162+
not in device.config_entries_subentries[config_entry_id]
1163+
):
1164+
self.async_remove(entity.entity_id)
11471165

11481166
# Re-enable disabled entities if the device is no longer disabled
11491167
if not device.disabled:

tests/helpers/test_device_registry.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1652,6 +1652,7 @@ async def test_removing_config_entries(
16521652
assert update_events[4].data == {
16531653
"action": "remove",
16541654
"device_id": entry3.id,
1655+
"device": entry3,
16551656
}
16561657

16571658

@@ -1724,10 +1725,12 @@ async def test_deleted_device_removing_config_entries(
17241725
assert update_events[3].data == {
17251726
"action": "remove",
17261727
"device_id": entry.id,
1728+
"device": entry2,
17271729
}
17281730
assert update_events[4].data == {
17291731
"action": "remove",
17301732
"device_id": entry3.id,
1733+
"device": entry3,
17311734
}
17321735

17331736
device_registry.async_clear_config_entry(config_entry_1.entry_id)
@@ -1973,6 +1976,7 @@ async def test_removing_config_subentries(
19731976
assert update_events[7].data == {
19741977
"action": "remove",
19751978
"device_id": entry.id,
1979+
"device": entry,
19761980
}
19771981

19781982

@@ -2102,6 +2106,7 @@ async def test_deleted_device_removing_config_subentries(
21022106
assert update_events[4].data == {
21032107
"action": "remove",
21042108
"device_id": entry.id,
2109+
"device": entry4,
21052110
}
21062111

21072112
device_registry.async_clear_config_subentry(config_entry_1.entry_id, None)
@@ -2925,6 +2930,7 @@ async def test_update_remove_config_entries(
29252930
assert update_events[6].data == {
29262931
"action": "remove",
29272932
"device_id": entry3.id,
2933+
"device": entry3,
29282934
}
29292935

29302936

@@ -3104,6 +3110,7 @@ async def test_update_remove_config_subentries(
31043110
config_entry_3.entry_id: {None},
31053111
}
31063112

3113+
entry_before_remove = entry
31073114
entry = device_registry.async_update_device(
31083115
entry_id,
31093116
remove_config_entry_id=config_entry_3.entry_id,
@@ -3201,6 +3208,7 @@ async def test_update_remove_config_subentries(
32013208
assert update_events[7].data == {
32023209
"action": "remove",
32033210
"device_id": entry_id,
3211+
"device": entry_before_remove,
32043212
}
32053213

32063214

@@ -3422,7 +3430,7 @@ async def test_restore_device(
34223430
)
34233431

34243432
# Apply user customizations
3425-
device_registry.async_update_device(
3433+
entry = device_registry.async_update_device(
34263434
entry.id,
34273435
area_id="12345A",
34283436
disabled_by=dr.DeviceEntryDisabler.USER,
@@ -3543,6 +3551,7 @@ async def test_restore_device(
35433551
assert update_events[2].data == {
35443552
"action": "remove",
35453553
"device_id": entry.id,
3554+
"device": entry,
35463555
}
35473556
assert update_events[3].data == {
35483557
"action": "create",
@@ -3865,6 +3874,7 @@ async def test_restore_shared_device(
38653874
assert update_events[3].data == {
38663875
"action": "remove",
38673876
"device_id": entry.id,
3877+
"device": updated_device,
38683878
}
38693879
assert update_events[4].data == {
38703880
"action": "create",
@@ -3873,6 +3883,7 @@ async def test_restore_shared_device(
38733883
assert update_events[5].data == {
38743884
"action": "remove",
38753885
"device_id": entry.id,
3886+
"device": entry2,
38763887
}
38773888
assert update_events[6].data == {
38783889
"action": "create",

tests/helpers/test_entity_registry.py

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1684,20 +1684,23 @@ async def test_remove_config_entry_from_device_removes_entities_2(
16841684
await hass.async_block_till_done()
16851685

16861686
assert device_registry.async_get(device_entry.id)
1687+
# Entities which are not tied to the removed config entry should not be removed
16871688
assert entity_registry.async_is_registered(entry_1.entity_id)
1688-
# Entities with a config entry not in the device are removed
1689-
assert not entity_registry.async_is_registered(entry_2.entity_id)
1689+
assert entity_registry.async_is_registered(entry_2.entity_id)
16901690

1691-
# Remove the second config entry from the device
1691+
# Remove the second config entry from the device (this removes the device)
16921692
device_registry.async_update_device(
16931693
device_entry.id, remove_config_entry_id=config_entry_2.entry_id
16941694
)
16951695
await hass.async_block_till_done()
16961696

16971697
assert not device_registry.async_get(device_entry.id)
1698-
# The device is removed, both entities are now removed
1699-
assert not entity_registry.async_is_registered(entry_1.entity_id)
1700-
assert not entity_registry.async_is_registered(entry_2.entity_id)
1698+
# Entities which are not tied to a config entry in the device should not be removed
1699+
assert entity_registry.async_is_registered(entry_1.entity_id)
1700+
assert entity_registry.async_is_registered(entry_2.entity_id)
1701+
# Check the device link is set to None
1702+
assert entity_registry.async_get(entry_1.entity_id).device_id is None
1703+
assert entity_registry.async_get(entry_2.entity_id).device_id is None
17011704

17021705

17031706
async def test_remove_config_subentry_from_device_removes_entities(
@@ -1921,12 +1924,12 @@ async def test_remove_config_subentry_from_device_removes_entities_2(
19211924
await hass.async_block_till_done()
19221925

19231926
assert device_registry.async_get(device_entry.id)
1927+
# Entities with a config subentry not in the device are not removed
19241928
assert entity_registry.async_is_registered(entry_1.entity_id)
1925-
# Entities with a config subentry not in the device are removed
1926-
assert not entity_registry.async_is_registered(entry_2.entity_id)
1927-
assert not entity_registry.async_is_registered(entry_3.entity_id)
1929+
assert entity_registry.async_is_registered(entry_2.entity_id)
1930+
assert entity_registry.async_is_registered(entry_3.entity_id)
19281931

1929-
# Remove the second config subentry from the device
1932+
# Remove the second config subentry from the device, this removes the device
19301933
device_registry.async_update_device(
19311934
device_entry.id,
19321935
remove_config_entry_id=config_entry_1.entry_id,
@@ -1935,10 +1938,14 @@ async def test_remove_config_subentry_from_device_removes_entities_2(
19351938
await hass.async_block_till_done()
19361939

19371940
assert not device_registry.async_get(device_entry.id)
1938-
# All entities are now removed
1939-
assert not entity_registry.async_is_registered(entry_1.entity_id)
1940-
assert not entity_registry.async_is_registered(entry_2.entity_id)
1941-
assert not entity_registry.async_is_registered(entry_3.entity_id)
1941+
# Entities with a config subentry not in the device are not removed
1942+
assert entity_registry.async_is_registered(entry_1.entity_id)
1943+
assert entity_registry.async_is_registered(entry_2.entity_id)
1944+
assert entity_registry.async_is_registered(entry_3.entity_id)
1945+
# Check the device link is set to None
1946+
assert entity_registry.async_get(entry_1.entity_id).device_id is None
1947+
assert entity_registry.async_get(entry_2.entity_id).device_id is None
1948+
assert entity_registry.async_get(entry_3.entity_id).device_id is None
19421949

19431950

19441951
async def test_update_device_race(

0 commit comments

Comments
 (0)