diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 96425b2ea93..cb009efeb07 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -11,7 +11,7 @@ import attr from homeassistant.backports.enum import StrEnum from homeassistant.const import EVENT_HOMEASSISTANT_STARTED from homeassistant.core import Event, HomeAssistant, callback -from homeassistant.exceptions import RequiredParameterMissing +from homeassistant.exceptions import HomeAssistantError, RequiredParameterMissing from homeassistant.loader import bind_hass import homeassistant.util.uuid as uuid_util @@ -420,10 +420,14 @@ class DeviceRegistry: """Update device attributes.""" old = self.devices[device_id] - changes: dict[str, Any] = {} + new_values: dict[str, Any] = {} # Dict with new key/value pairs + old_values: dict[str, Any] = {} # Dict with old key/value pairs config_entries = old.config_entries + if merge_identifiers is not UNDEFINED and new_identifiers is not UNDEFINED: + raise HomeAssistantError() + if isinstance(disabled_by, str) and not isinstance( disabled_by, DeviceEntryDisabler ): @@ -462,7 +466,8 @@ class DeviceRegistry: config_entries = config_entries - {remove_config_entry_id} if config_entries != old.config_entries: - changes["config_entries"] = config_entries + new_values["config_entries"] = config_entries + old_values["config_entries"] = old.config_entries for attr_name, setvalue in ( ("connections", merge_connections), @@ -471,10 +476,12 @@ class DeviceRegistry: old_value = getattr(old, attr_name) # If not undefined, check if `value` contains new items. if setvalue is not UNDEFINED and not setvalue.issubset(old_value): - changes[attr_name] = old_value | setvalue + new_values[attr_name] = old_value | setvalue + old_values[attr_name] = old_value if new_identifiers is not UNDEFINED: - changes["identifiers"] = new_identifiers + new_values["identifiers"] = new_identifiers + old_values["identifiers"] = old.identifiers for attr_name, value in ( ("configuration_url", configuration_url), @@ -491,25 +498,27 @@ class DeviceRegistry: ("via_device_id", via_device_id), ): if value is not UNDEFINED and value != getattr(old, attr_name): - changes[attr_name] = value + new_values[attr_name] = value + old_values[attr_name] = getattr(old, attr_name) if old.is_new: - changes["is_new"] = False + new_values["is_new"] = False - if not changes: + if not new_values: return old - new = attr.evolve(old, **changes) + new = attr.evolve(old, **new_values) self._update_device(old, new) self.async_schedule_save() - self.hass.bus.async_fire( - EVENT_DEVICE_REGISTRY_UPDATED, - { - "action": "create" if "is_new" in changes else "update", - "device_id": new.id, - }, - ) + data: dict[str, Any] = { + "action": "create" if old.is_new else "update", + "device_id": new.id, + } + if not old.is_new: + data["changes"] = old_values + + self.hass.bus.async_fire(EVENT_DEVICE_REGISTRY_UPDATED, data) return new diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index a0949bad03c..4e4150fe504 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -96,8 +96,12 @@ async def test_get_or_create_returns_same_entry( assert len(update_events) == 2 assert update_events[0]["action"] == "create" assert update_events[0]["device_id"] == entry.id + assert "changes" not in update_events[0] assert update_events[1]["action"] == "update" assert update_events[1]["device_id"] == entry.id + assert update_events[1]["changes"] == { + "connections": {("mac", "12:34:56:ab:cd:ef")} + } async def test_requirement_for_identifier_or_connection(registry): @@ -518,14 +522,19 @@ async def test_removing_config_entries(hass, registry, update_events): assert len(update_events) == 5 assert update_events[0]["action"] == "create" assert update_events[0]["device_id"] == entry.id + assert "changes" not in update_events[0] assert update_events[1]["action"] == "update" assert update_events[1]["device_id"] == entry2.id + assert update_events[1]["changes"] == {"config_entries": {"123"}} assert update_events[2]["action"] == "create" assert update_events[2]["device_id"] == entry3.id + assert "changes" not in update_events[2] assert update_events[3]["action"] == "update" assert update_events[3]["device_id"] == entry.id + assert update_events[3]["changes"] == {"config_entries": {"456", "123"}} assert update_events[4]["action"] == "remove" assert update_events[4]["device_id"] == entry3.id + assert "changes" not in update_events[4] async def test_deleted_device_removing_config_entries(hass, registry, update_events): @@ -568,14 +577,19 @@ async def test_deleted_device_removing_config_entries(hass, registry, update_eve assert len(update_events) == 5 assert update_events[0]["action"] == "create" assert update_events[0]["device_id"] == entry.id + assert "changes" not in update_events[0] assert update_events[1]["action"] == "update" assert update_events[1]["device_id"] == entry2.id + assert update_events[1]["changes"] == {"config_entries": {"123"}} assert update_events[2]["action"] == "create" assert update_events[2]["device_id"] == entry3.id + assert "changes" not in update_events[2]["device_id"] assert update_events[3]["action"] == "remove" assert update_events[3]["device_id"] == entry.id + assert "changes" not in update_events[3] assert update_events[4]["action"] == "remove" assert update_events[4]["device_id"] == entry3.id + assert "changes" not in update_events[4] registry.async_clear_config_entry("123") assert len(registry.devices) == 0 @@ -892,7 +906,7 @@ async def test_format_mac(registry): assert list(invalid_mac_entry.connections)[0][1] == invalid -async def test_update(registry): +async def test_update(hass, registry, update_events): """Verify that we can update some attributes of a device.""" entry = registry.async_get_or_create( config_entry_id="1234", @@ -940,6 +954,24 @@ async def test_update(registry): assert registry.async_get(updated_entry.id) is not None + await hass.async_block_till_done() + + assert len(update_events) == 2 + assert update_events[0]["action"] == "create" + assert update_events[0]["device_id"] == entry.id + assert "changes" not in update_events[0] + assert update_events[1]["action"] == "update" + assert update_events[1]["device_id"] == entry.id + assert update_events[1]["changes"] == { + "area_id": None, + "disabled_by": None, + "identifiers": {("bla", "123"), ("hue", "456")}, + "manufacturer": None, + "model": None, + "name_by_user": None, + "via_device_id": None, + } + async def test_update_remove_config_entries(hass, registry, update_events): """Make sure we do not get duplicate entries.""" @@ -989,17 +1021,22 @@ async def test_update_remove_config_entries(hass, registry, update_events): assert len(update_events) == 5 assert update_events[0]["action"] == "create" assert update_events[0]["device_id"] == entry.id + assert "changes" not in update_events[0] assert update_events[1]["action"] == "update" assert update_events[1]["device_id"] == entry2.id + assert update_events[1]["changes"] == {"config_entries": {"123"}} assert update_events[2]["action"] == "create" assert update_events[2]["device_id"] == entry3.id + assert "changes" not in update_events[2] assert update_events[3]["action"] == "update" assert update_events[3]["device_id"] == entry.id + assert update_events[3]["changes"] == {"config_entries": {"456", "123"}} assert update_events[4]["action"] == "remove" assert update_events[4]["device_id"] == entry3.id + assert "changes" not in update_events[4] -async def test_update_sw_version(registry): +async def test_update_sw_version(hass, registry, update_events): """Verify that we can update software version of a device.""" entry = registry.async_get_or_create( config_entry_id="1234", @@ -1016,8 +1053,18 @@ async def test_update_sw_version(registry): assert updated_entry != entry assert updated_entry.sw_version == sw_version + await hass.async_block_till_done() -async def test_update_hw_version(registry): + assert len(update_events) == 2 + assert update_events[0]["action"] == "create" + assert update_events[0]["device_id"] == entry.id + assert "changes" not in update_events[0] + assert update_events[1]["action"] == "update" + assert update_events[1]["device_id"] == entry.id + assert update_events[1]["changes"] == {"sw_version": None} + + +async def test_update_hw_version(hass, registry, update_events): """Verify that we can update hardware version of a device.""" entry = registry.async_get_or_create( config_entry_id="1234", @@ -1034,8 +1081,18 @@ async def test_update_hw_version(registry): assert updated_entry != entry assert updated_entry.hw_version == hw_version + await hass.async_block_till_done() -async def test_update_suggested_area(registry, area_registry): + assert len(update_events) == 2 + assert update_events[0]["action"] == "create" + assert update_events[0]["device_id"] == entry.id + assert "changes" not in update_events[0] + assert update_events[1]["action"] == "update" + assert update_events[1]["device_id"] == entry.id + assert update_events[1]["changes"] == {"hw_version": None} + + +async def test_update_suggested_area(hass, registry, area_registry, update_events): """Verify that we can update the suggested area version of a device.""" entry = registry.async_get_or_create( config_entry_id="1234", @@ -1061,6 +1118,16 @@ async def test_update_suggested_area(registry, area_registry): assert updated_entry.area_id == pool_area.id assert len(area_registry.areas) == 1 + await hass.async_block_till_done() + + assert len(update_events) == 2 + assert update_events[0]["action"] == "create" + assert update_events[0]["device_id"] == entry.id + assert "changes" not in update_events[0] + assert update_events[1]["action"] == "update" + assert update_events[1]["device_id"] == entry.id + assert update_events[1]["changes"] == {"area_id": None, "suggested_area": None} + async def test_cleanup_device_registry(hass, registry): """Test cleanup works.""" @@ -1221,12 +1288,16 @@ async def test_restore_device(hass, registry, update_events): assert len(update_events) == 4 assert update_events[0]["action"] == "create" assert update_events[0]["device_id"] == entry.id + assert "changes" not in update_events[0] assert update_events[1]["action"] == "remove" assert update_events[1]["device_id"] == entry.id + assert "changes" not in update_events[1] assert update_events[2]["action"] == "create" assert update_events[2]["device_id"] == entry2.id + assert "changes" not in update_events[2] assert update_events[3]["action"] == "create" assert update_events[3]["device_id"] == entry3.id + assert "changes" not in update_events[3] async def test_restore_simple_device(hass, registry, update_events): @@ -1266,12 +1337,16 @@ async def test_restore_simple_device(hass, registry, update_events): assert len(update_events) == 4 assert update_events[0]["action"] == "create" assert update_events[0]["device_id"] == entry.id + assert "changes" not in update_events[0] assert update_events[1]["action"] == "remove" assert update_events[1]["device_id"] == entry.id + assert "changes" not in update_events[1] assert update_events[2]["action"] == "create" assert update_events[2]["device_id"] == entry2.id + assert "changes" not in update_events[2] assert update_events[3]["action"] == "create" assert update_events[3]["device_id"] == entry3.id + assert "changes" not in update_events[3] async def test_restore_shared_device(hass, registry, update_events): @@ -1358,18 +1433,31 @@ async def test_restore_shared_device(hass, registry, update_events): assert len(update_events) == 7 assert update_events[0]["action"] == "create" assert update_events[0]["device_id"] == entry.id + assert "changes" not in update_events[0] assert update_events[1]["action"] == "update" assert update_events[1]["device_id"] == entry.id + assert update_events[1]["changes"] == { + "config_entries": {"123"}, + "identifiers": {("entry_123", "0123")}, + } assert update_events[2]["action"] == "remove" assert update_events[2]["device_id"] == entry.id + assert "changes" not in update_events[2] assert update_events[3]["action"] == "create" assert update_events[3]["device_id"] == entry.id + assert "changes" not in update_events[3] assert update_events[4]["action"] == "remove" assert update_events[4]["device_id"] == entry.id + assert "changes" not in update_events[4] assert update_events[5]["action"] == "create" assert update_events[5]["device_id"] == entry.id - assert update_events[1]["action"] == "update" - assert update_events[1]["device_id"] == entry.id + assert "changes" not in update_events[5] + assert update_events[6]["action"] == "update" + assert update_events[6]["device_id"] == entry.id + assert update_events[6]["changes"] == { + "config_entries": {"234"}, + "identifiers": {("entry_234", "2345")}, + } async def test_get_or_create_empty_then_set_default_values(hass, registry):