From 057bb6ea0ac2db1220d9a1090ce8b617d914b9fa Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 20 Dec 2022 23:43:31 +0100 Subject: [PATCH] Add aliases to device registry items (#84246) --- .../components/config/device_registry.py | 6 +++ homeassistant/helpers/device_registry.py | 10 ++++- .../components/config/test_device_registry.py | 40 +++++++++++++++++++ tests/helpers/test_device_registry.py | 17 ++++++-- 4 files changed, 68 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/config/device_registry.py b/homeassistant/components/config/device_registry.py index 42d2386977f..348fc475cb8 100644 --- a/homeassistant/components/config/device_registry.py +++ b/homeassistant/components/config/device_registry.py @@ -75,6 +75,7 @@ async def async_setup(hass): @websocket_api.websocket_command( { vol.Required("type"): "config/device_registry/update", + vol.Optional("aliases"): list, vol.Optional("area_id"): vol.Any(str, None), vol.Required("device_id"): str, # We only allow setting disabled_by user via API. @@ -95,6 +96,10 @@ def websocket_update_device( msg.pop("type") msg_id = msg.pop("id") + if "aliases" in msg: + # Convert aliases to a set + msg["aliases"] = set(msg["aliases"]) + if msg.get("disabled_by") is not None: msg["disabled_by"] = DeviceEntryDisabler(msg["disabled_by"]) @@ -160,6 +165,7 @@ async def websocket_remove_config_entry_from_device( def _entry_dict(entry): """Convert entry to API format.""" return { + "aliases": entry.aliases, "area_id": entry.area_id, "configuration_url": entry.configuration_url, "config_entries": list(entry.config_entries), diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index c631af14ee5..90c9695b9de 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -32,7 +32,7 @@ DATA_REGISTRY = "device_registry" EVENT_DEVICE_REGISTRY_UPDATED = "device_registry_updated" STORAGE_KEY = "core.device_registry" STORAGE_VERSION_MAJOR = 1 -STORAGE_VERSION_MINOR = 3 +STORAGE_VERSION_MINOR = 4 SAVE_DELAY = 10 CLEANUP_DELAY = 10 @@ -70,6 +70,7 @@ class DeviceEntryType(StrEnum): class DeviceEntry: """Device Registry Entry.""" + aliases: set[str] = attr.ib(factory=set) area_id: str | None = attr.ib(default=None) config_entries: set[str] = attr.ib(converter=set, factory=set) configuration_url: str | None = attr.ib(default=None) @@ -174,6 +175,9 @@ class DeviceRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]): # Version 1.3 adds hw_version for device in old_data["devices"]: device["hw_version"] = None + if old_minor_version < 4: + for device in old_data["devices"]: + device["aliases"] = [] if old_major_version > 1: raise NotImplementedError @@ -376,6 +380,7 @@ class DeviceRegistry: device_id: str, *, add_config_entry_id: str | UndefinedType = UNDEFINED, + aliases: set[str] | UndefinedType = UNDEFINED, area_id: str | None | UndefinedType = UNDEFINED, configuration_url: str | None | UndefinedType = UNDEFINED, disabled_by: DeviceEntryDisabler | None | UndefinedType = UNDEFINED, @@ -464,6 +469,7 @@ class DeviceRegistry: old_values["identifiers"] = old.identifiers for attr_name, value in ( + ("aliases", aliases), ("area_id", area_id), ("configuration_url", configuration_url), ("disabled_by", disabled_by), @@ -542,6 +548,7 @@ class DeviceRegistry: if data is not None: for device in data["devices"]: devices[device["id"]] = DeviceEntry( + aliases=set(device["aliases"]), area_id=device["area_id"], config_entries=set(device["config_entries"]), configuration_url=device["configuration_url"], @@ -589,6 +596,7 @@ class DeviceRegistry: data["devices"] = [ { + "aliases": list(entry.aliases), "area_id": entry.area_id, "config_entries": list(entry.config_entries), "configuration_url": entry.configuration_url, diff --git a/tests/components/config/test_device_registry.py b/tests/components/config/test_device_registry.py index 4f47e463751..3582601f7e3 100644 --- a/tests/components/config/test_device_registry.py +++ b/tests/components/config/test_device_registry.py @@ -1,5 +1,6 @@ """Test device_registry API.""" import pytest +from pytest_unordered import unordered from homeassistant.components.config import device_registry from homeassistant.helpers import device_registry as helpers_dr @@ -52,6 +53,7 @@ async def test_list_devices(hass, client, registry): assert msg["result"] == [ { + "aliases": [], "area_id": None, "config_entries": ["1234"], "configuration_url": None, @@ -68,6 +70,7 @@ async def test_list_devices(hass, client, registry): "via_device_id": None, }, { + "aliases": [], "area_id": None, "config_entries": ["1234"], "configuration_url": None, @@ -93,6 +96,7 @@ async def test_list_devices(hass, client, registry): assert msg["result"] == [ { + "aliases": [], "area_id": None, "config_entries": ["1234"], "configuration_url": None, @@ -160,6 +164,42 @@ async def test_update_device(hass, client, registry, payload_key, payload_value) assert isinstance(device.disabled_by, (helpers_dr.DeviceEntryDisabler, type(None))) +@pytest.mark.parametrize("aliases", (["alias_1", "alias_2"], ["alias_1", "alias_1"])) +async def test_update_aliases(hass, client, registry, aliases): + """Test update entry.""" + device = registry.async_get_or_create( + config_entry_id="1234", + connections={("ethernet", "12:34:56:78:90:AB:CD:EF")}, + identifiers={("bridgeid", "0123")}, + manufacturer="manufacturer", + model="model", + ) + + assert not device.aliases == {} + + await client.send_json( + { + "id": 1, + "type": "config/device_registry/update", + "device_id": device.id, + "aliases": aliases, + } + ) + + msg = await client.receive_json() + await hass.async_block_till_done() + assert len(registry.devices) == 1 + + device = registry.async_get_device( + identifiers={("bridgeid", "0123")}, + connections={("ethernet", "12:34:56:78:90:AB:CD:EF")}, + ) + + # Test that the aliases list is stored by the registry as a set + assert msg["result"]["aliases"] == unordered(list(set(aliases))) + assert device.aliases == set(aliases) + + async def test_remove_config_entry_from_device(hass, hass_ws_client): """Test removing config entry from device.""" assert await async_setup_component(hass, "config", {}) diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 2c9a7956874..7888054aaf7 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -176,6 +176,7 @@ async def test_loading_from_storage(hass, hass_storage): "data": { "devices": [ { + "aliases": ["alias_1", "alias_2"], "area_id": "12345A", "config_entries": ["1234"], "configuration_url": "configuration_url", @@ -218,6 +219,7 @@ async def test_loading_from_storage(hass, hass_storage): model="model", ) assert entry == device_registry.DeviceEntry( + aliases={"alias_1", "alias_2"}, area_id="12345A", config_entries={"1234"}, configuration_url="configuration_url", @@ -261,8 +263,8 @@ async def test_loading_from_storage(hass, hass_storage): @pytest.mark.parametrize("load_registries", [False]) -async def test_migration_1_1_to_1_3(hass, hass_storage): - """Test migration from version 1.1 to 1.3.""" +async def test_migration_from_1_1(hass, hass_storage): + """Test migration from version 1.1.""" hass_storage[device_registry.STORAGE_KEY] = { "version": 1, "minor_version": 1, @@ -337,6 +339,7 @@ async def test_migration_1_1_to_1_3(hass, hass_storage): "data": { "devices": [ { + "aliases": [], "area_id": None, "config_entries": ["1234"], "configuration_url": None, @@ -354,6 +357,7 @@ async def test_migration_1_1_to_1_3(hass, hass_storage): "via_device_id": None, }, { + "aliases": [], "area_id": None, "config_entries": [None], "configuration_url": None, @@ -385,8 +389,8 @@ async def test_migration_1_1_to_1_3(hass, hass_storage): @pytest.mark.parametrize("load_registries", [False]) -async def test_migration_1_2_to_1_3(hass, hass_storage): - """Test migration from version 1.2 to 1.3.""" +async def test_migration_from_1_2(hass, hass_storage): + """Test migration from version 1.2.""" hass_storage[device_registry.STORAGE_KEY] = { "version": 1, "minor_version": 2, @@ -460,6 +464,7 @@ async def test_migration_1_2_to_1_3(hass, hass_storage): "data": { "devices": [ { + "aliases": [], "area_id": None, "config_entries": ["1234"], "configuration_url": None, @@ -477,6 +482,7 @@ async def test_migration_1_2_to_1_3(hass, hass_storage): "via_device_id": None, }, { + "aliases": [], "area_id": None, "config_entries": [None], "configuration_url": None, @@ -908,6 +914,7 @@ async def test_update(hass, registry, update_events): with patch.object(registry, "async_schedule_save") as mock_save: updated_entry = registry.async_update_device( entry.id, + aliases={"alias_1", "alias_2"}, area_id="12345A", configuration_url="configuration_url", disabled_by=device_registry.DeviceEntryDisabler.USER, @@ -926,6 +933,7 @@ async def test_update(hass, registry, update_events): assert mock_save.call_count == 1 assert updated_entry != entry assert updated_entry == device_registry.DeviceEntry( + aliases={"alias_1", "alias_2"}, area_id="12345A", config_entries={"1234"}, configuration_url="configuration_url", @@ -968,6 +976,7 @@ async def test_update(hass, registry, update_events): assert update_events[1]["action"] == "update" assert update_events[1]["device_id"] == entry.id assert update_events[1]["changes"] == { + "aliases": set(), "area_id": None, "configuration_url": None, "disabled_by": None,