mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
Improve entity registry handling of device changes (#148425)
This commit is contained in:
parent
e0179a7d45
commit
2dca78efbb
@ -144,13 +144,21 @@ DEVICE_INFO_KEYS = set.union(*(itm for itm in DEVICE_INFO_TYPES.values()))
|
|||||||
LOW_PRIO_CONFIG_ENTRY_DOMAINS = {"homekit_controller", "matter", "mqtt", "upnp"}
|
LOW_PRIO_CONFIG_ENTRY_DOMAINS = {"homekit_controller", "matter", "mqtt", "upnp"}
|
||||||
|
|
||||||
|
|
||||||
class _EventDeviceRegistryUpdatedData_CreateRemove(TypedDict):
|
class _EventDeviceRegistryUpdatedData_Create(TypedDict):
|
||||||
"""EventDeviceRegistryUpdated data for action type 'create' and 'remove'."""
|
"""EventDeviceRegistryUpdated data for action type 'create'."""
|
||||||
|
|
||||||
action: Literal["create", "remove"]
|
action: Literal["create"]
|
||||||
device_id: str
|
device_id: str
|
||||||
|
|
||||||
|
|
||||||
|
class _EventDeviceRegistryUpdatedData_Remove(TypedDict):
|
||||||
|
"""EventDeviceRegistryUpdated data for action type 'remove'."""
|
||||||
|
|
||||||
|
action: Literal["remove"]
|
||||||
|
device_id: str
|
||||||
|
device: DeviceEntry
|
||||||
|
|
||||||
|
|
||||||
class _EventDeviceRegistryUpdatedData_Update(TypedDict):
|
class _EventDeviceRegistryUpdatedData_Update(TypedDict):
|
||||||
"""EventDeviceRegistryUpdated data for action type 'update'."""
|
"""EventDeviceRegistryUpdated data for action type 'update'."""
|
||||||
|
|
||||||
@ -160,7 +168,8 @@ class _EventDeviceRegistryUpdatedData_Update(TypedDict):
|
|||||||
|
|
||||||
|
|
||||||
type EventDeviceRegistryUpdatedData = (
|
type EventDeviceRegistryUpdatedData = (
|
||||||
_EventDeviceRegistryUpdatedData_CreateRemove
|
_EventDeviceRegistryUpdatedData_Create
|
||||||
|
| _EventDeviceRegistryUpdatedData_Remove
|
||||||
| _EventDeviceRegistryUpdatedData_Update
|
| _EventDeviceRegistryUpdatedData_Update
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1309,8 +1318,8 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
|
|||||||
self.async_update_device(other_device.id, via_device_id=None)
|
self.async_update_device(other_device.id, via_device_id=None)
|
||||||
self.hass.bus.async_fire_internal(
|
self.hass.bus.async_fire_internal(
|
||||||
EVENT_DEVICE_REGISTRY_UPDATED,
|
EVENT_DEVICE_REGISTRY_UPDATED,
|
||||||
_EventDeviceRegistryUpdatedData_CreateRemove(
|
_EventDeviceRegistryUpdatedData_Remove(
|
||||||
action="remove", device_id=device_id
|
action="remove", device_id=device_id, device=device
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
self.async_schedule_save()
|
self.async_schedule_save()
|
||||||
|
@ -1103,8 +1103,17 @@ class EntityRegistry(BaseRegistry):
|
|||||||
entities = async_entries_for_device(
|
entities = async_entries_for_device(
|
||||||
self, event.data["device_id"], include_disabled_entities=True
|
self, event.data["device_id"], include_disabled_entities=True
|
||||||
)
|
)
|
||||||
|
removed_device = event.data["device"]
|
||||||
for entity in entities:
|
for entity in entities:
|
||||||
|
config_entry_id = entity.config_entry_id
|
||||||
|
if (
|
||||||
|
config_entry_id in removed_device.config_entries
|
||||||
|
and entity.config_subentry_id
|
||||||
|
in removed_device.config_entries_subentries[config_entry_id]
|
||||||
|
):
|
||||||
self.async_remove(entity.entity_id)
|
self.async_remove(entity.entity_id)
|
||||||
|
else:
|
||||||
|
self.async_update_entity(entity.entity_id, device_id=None)
|
||||||
return
|
return
|
||||||
|
|
||||||
if event.data["action"] != "update":
|
if event.data["action"] != "update":
|
||||||
@ -1121,26 +1130,35 @@ class EntityRegistry(BaseRegistry):
|
|||||||
|
|
||||||
# Remove entities which belong to config entries no longer associated with the
|
# Remove entities which belong to config entries no longer associated with the
|
||||||
# device
|
# device
|
||||||
|
if old_config_entries := event.data["changes"].get("config_entries"):
|
||||||
entities = async_entries_for_device(
|
entities = async_entries_for_device(
|
||||||
self, event.data["device_id"], include_disabled_entities=True
|
self, event.data["device_id"], include_disabled_entities=True
|
||||||
)
|
)
|
||||||
for entity in entities:
|
for entity in entities:
|
||||||
|
config_entry_id = entity.config_entry_id
|
||||||
if (
|
if (
|
||||||
entity.config_entry_id is not None
|
entity.config_entry_id in old_config_entries
|
||||||
and entity.config_entry_id not in device.config_entries
|
and entity.config_entry_id not in device.config_entries
|
||||||
):
|
):
|
||||||
self.async_remove(entity.entity_id)
|
self.async_remove(entity.entity_id)
|
||||||
|
|
||||||
# Remove entities which belong to config subentries no longer associated with the
|
# Remove entities which belong to config subentries no longer associated with the
|
||||||
# device
|
# device
|
||||||
|
if old_config_entries_subentries := event.data["changes"].get(
|
||||||
|
"config_entries_subentries"
|
||||||
|
):
|
||||||
entities = async_entries_for_device(
|
entities = async_entries_for_device(
|
||||||
self, event.data["device_id"], include_disabled_entities=True
|
self, event.data["device_id"], include_disabled_entities=True
|
||||||
)
|
)
|
||||||
for entity in entities:
|
for entity in entities:
|
||||||
|
config_entry_id = entity.config_entry_id
|
||||||
|
config_subentry_id = entity.config_subentry_id
|
||||||
if (
|
if (
|
||||||
(config_entry_id := entity.config_entry_id) is not None
|
config_entry_id in device.config_entries
|
||||||
and config_entry_id in device.config_entries
|
and config_entry_id in old_config_entries_subentries
|
||||||
and entity.config_subentry_id
|
and config_subentry_id
|
||||||
|
in old_config_entries_subentries[config_entry_id]
|
||||||
|
and config_subentry_id
|
||||||
not in device.config_entries_subentries[config_entry_id]
|
not in device.config_entries_subentries[config_entry_id]
|
||||||
):
|
):
|
||||||
self.async_remove(entity.entity_id)
|
self.async_remove(entity.entity_id)
|
||||||
|
@ -1652,6 +1652,7 @@ async def test_removing_config_entries(
|
|||||||
assert update_events[4].data == {
|
assert update_events[4].data == {
|
||||||
"action": "remove",
|
"action": "remove",
|
||||||
"device_id": entry3.id,
|
"device_id": entry3.id,
|
||||||
|
"device": entry3,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -1724,10 +1725,12 @@ async def test_deleted_device_removing_config_entries(
|
|||||||
assert update_events[3].data == {
|
assert update_events[3].data == {
|
||||||
"action": "remove",
|
"action": "remove",
|
||||||
"device_id": entry.id,
|
"device_id": entry.id,
|
||||||
|
"device": entry2,
|
||||||
}
|
}
|
||||||
assert update_events[4].data == {
|
assert update_events[4].data == {
|
||||||
"action": "remove",
|
"action": "remove",
|
||||||
"device_id": entry3.id,
|
"device_id": entry3.id,
|
||||||
|
"device": entry3,
|
||||||
}
|
}
|
||||||
|
|
||||||
device_registry.async_clear_config_entry(config_entry_1.entry_id)
|
device_registry.async_clear_config_entry(config_entry_1.entry_id)
|
||||||
@ -1973,6 +1976,7 @@ async def test_removing_config_subentries(
|
|||||||
assert update_events[7].data == {
|
assert update_events[7].data == {
|
||||||
"action": "remove",
|
"action": "remove",
|
||||||
"device_id": entry.id,
|
"device_id": entry.id,
|
||||||
|
"device": entry,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -2102,6 +2106,7 @@ async def test_deleted_device_removing_config_subentries(
|
|||||||
assert update_events[4].data == {
|
assert update_events[4].data == {
|
||||||
"action": "remove",
|
"action": "remove",
|
||||||
"device_id": entry.id,
|
"device_id": entry.id,
|
||||||
|
"device": entry4,
|
||||||
}
|
}
|
||||||
|
|
||||||
device_registry.async_clear_config_subentry(config_entry_1.entry_id, None)
|
device_registry.async_clear_config_subentry(config_entry_1.entry_id, None)
|
||||||
@ -2925,6 +2930,7 @@ async def test_update_remove_config_entries(
|
|||||||
assert update_events[6].data == {
|
assert update_events[6].data == {
|
||||||
"action": "remove",
|
"action": "remove",
|
||||||
"device_id": entry3.id,
|
"device_id": entry3.id,
|
||||||
|
"device": entry3,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -3104,6 +3110,7 @@ async def test_update_remove_config_subentries(
|
|||||||
config_entry_3.entry_id: {None},
|
config_entry_3.entry_id: {None},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entry_before_remove = entry
|
||||||
entry = device_registry.async_update_device(
|
entry = device_registry.async_update_device(
|
||||||
entry_id,
|
entry_id,
|
||||||
remove_config_entry_id=config_entry_3.entry_id,
|
remove_config_entry_id=config_entry_3.entry_id,
|
||||||
@ -3201,6 +3208,7 @@ async def test_update_remove_config_subentries(
|
|||||||
assert update_events[7].data == {
|
assert update_events[7].data == {
|
||||||
"action": "remove",
|
"action": "remove",
|
||||||
"device_id": entry_id,
|
"device_id": entry_id,
|
||||||
|
"device": entry_before_remove,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -3422,7 +3430,7 @@ async def test_restore_device(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Apply user customizations
|
# Apply user customizations
|
||||||
device_registry.async_update_device(
|
entry = device_registry.async_update_device(
|
||||||
entry.id,
|
entry.id,
|
||||||
area_id="12345A",
|
area_id="12345A",
|
||||||
disabled_by=dr.DeviceEntryDisabler.USER,
|
disabled_by=dr.DeviceEntryDisabler.USER,
|
||||||
@ -3543,6 +3551,7 @@ async def test_restore_device(
|
|||||||
assert update_events[2].data == {
|
assert update_events[2].data == {
|
||||||
"action": "remove",
|
"action": "remove",
|
||||||
"device_id": entry.id,
|
"device_id": entry.id,
|
||||||
|
"device": entry,
|
||||||
}
|
}
|
||||||
assert update_events[3].data == {
|
assert update_events[3].data == {
|
||||||
"action": "create",
|
"action": "create",
|
||||||
@ -3865,6 +3874,7 @@ async def test_restore_shared_device(
|
|||||||
assert update_events[3].data == {
|
assert update_events[3].data == {
|
||||||
"action": "remove",
|
"action": "remove",
|
||||||
"device_id": entry.id,
|
"device_id": entry.id,
|
||||||
|
"device": updated_device,
|
||||||
}
|
}
|
||||||
assert update_events[4].data == {
|
assert update_events[4].data == {
|
||||||
"action": "create",
|
"action": "create",
|
||||||
@ -3873,6 +3883,7 @@ async def test_restore_shared_device(
|
|||||||
assert update_events[5].data == {
|
assert update_events[5].data == {
|
||||||
"action": "remove",
|
"action": "remove",
|
||||||
"device_id": entry.id,
|
"device_id": entry.id,
|
||||||
|
"device": entry2,
|
||||||
}
|
}
|
||||||
assert update_events[6].data == {
|
assert update_events[6].data == {
|
||||||
"action": "create",
|
"action": "create",
|
||||||
|
@ -1684,20 +1684,23 @@ async def test_remove_config_entry_from_device_removes_entities_2(
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert device_registry.async_get(device_entry.id)
|
assert device_registry.async_get(device_entry.id)
|
||||||
|
# Entities which are not tied to the removed config entry should not be removed
|
||||||
assert entity_registry.async_is_registered(entry_1.entity_id)
|
assert entity_registry.async_is_registered(entry_1.entity_id)
|
||||||
# Entities with a config entry not in the device are removed
|
assert entity_registry.async_is_registered(entry_2.entity_id)
|
||||||
assert not entity_registry.async_is_registered(entry_2.entity_id)
|
|
||||||
|
|
||||||
# Remove the second config entry from the device
|
# Remove the second config entry from the device (this removes the device)
|
||||||
device_registry.async_update_device(
|
device_registry.async_update_device(
|
||||||
device_entry.id, remove_config_entry_id=config_entry_2.entry_id
|
device_entry.id, remove_config_entry_id=config_entry_2.entry_id
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert not device_registry.async_get(device_entry.id)
|
assert not device_registry.async_get(device_entry.id)
|
||||||
# The device is removed, both entities are now removed
|
# Entities which are not tied to a config entry in the device should not be removed
|
||||||
assert not entity_registry.async_is_registered(entry_1.entity_id)
|
assert entity_registry.async_is_registered(entry_1.entity_id)
|
||||||
assert not entity_registry.async_is_registered(entry_2.entity_id)
|
assert entity_registry.async_is_registered(entry_2.entity_id)
|
||||||
|
# Check the device link is set to None
|
||||||
|
assert entity_registry.async_get(entry_1.entity_id).device_id is None
|
||||||
|
assert entity_registry.async_get(entry_2.entity_id).device_id is None
|
||||||
|
|
||||||
|
|
||||||
async def test_remove_config_subentry_from_device_removes_entities(
|
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(
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert device_registry.async_get(device_entry.id)
|
assert device_registry.async_get(device_entry.id)
|
||||||
|
# Entities with a config subentry not in the device are not removed
|
||||||
assert entity_registry.async_is_registered(entry_1.entity_id)
|
assert entity_registry.async_is_registered(entry_1.entity_id)
|
||||||
# Entities with a config subentry not in the device are removed
|
assert entity_registry.async_is_registered(entry_2.entity_id)
|
||||||
assert not entity_registry.async_is_registered(entry_2.entity_id)
|
assert entity_registry.async_is_registered(entry_3.entity_id)
|
||||||
assert not entity_registry.async_is_registered(entry_3.entity_id)
|
|
||||||
|
|
||||||
# Remove the second config subentry from the device
|
# Remove the second config subentry from the device, this removes the device
|
||||||
device_registry.async_update_device(
|
device_registry.async_update_device(
|
||||||
device_entry.id,
|
device_entry.id,
|
||||||
remove_config_entry_id=config_entry_1.entry_id,
|
remove_config_entry_id=config_entry_1.entry_id,
|
||||||
@ -1935,10 +1938,14 @@ async def test_remove_config_subentry_from_device_removes_entities_2(
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert not device_registry.async_get(device_entry.id)
|
assert not device_registry.async_get(device_entry.id)
|
||||||
# All entities are now removed
|
# Entities with a config subentry not in the device are not removed
|
||||||
assert not entity_registry.async_is_registered(entry_1.entity_id)
|
assert entity_registry.async_is_registered(entry_1.entity_id)
|
||||||
assert not entity_registry.async_is_registered(entry_2.entity_id)
|
assert entity_registry.async_is_registered(entry_2.entity_id)
|
||||||
assert not entity_registry.async_is_registered(entry_3.entity_id)
|
assert entity_registry.async_is_registered(entry_3.entity_id)
|
||||||
|
# Check the device link is set to None
|
||||||
|
assert entity_registry.async_get(entry_1.entity_id).device_id is None
|
||||||
|
assert entity_registry.async_get(entry_2.entity_id).device_id is None
|
||||||
|
assert entity_registry.async_get(entry_3.entity_id).device_id is None
|
||||||
|
|
||||||
|
|
||||||
async def test_update_device_race(
|
async def test_update_device_race(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user