From e27e4c356184882ccde9d155c96341eeebab329e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 14 Oct 2021 20:16:56 +0200 Subject: [PATCH] Add support for device configuration URL (#57539) Co-authored-by: Paulus Schoutsen --- .../components/config/device_registry.py | 1 + homeassistant/components/shelly/__init__.py | 2 + homeassistant/helpers/device_registry.py | 10 ++++ homeassistant/helpers/entity.py | 1 + homeassistant/helpers/entity_platform.py | 11 ++++ .../components/config/test_device_registry.py | 2 + tests/components/shelly/conftest.py | 4 +- .../components/shelly/test_device_trigger.py | 2 +- tests/helpers/test_entity_platform.py | 55 ++++++++++++++++++- 9 files changed, 83 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/config/device_registry.py b/homeassistant/components/config/device_registry.py index 1dd8fbe4167..1cc63297352 100644 --- a/homeassistant/components/config/device_registry.py +++ b/homeassistant/components/config/device_registry.py @@ -68,6 +68,7 @@ def _entry_dict(entry): """Convert entry to API format.""" return { "area_id": entry.area_id, + "configuration_url": entry.configuration_url, "config_entries": list(entry.config_entries), "connections": list(entry.connections), "disabled_by": entry.disabled_by, diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index cb48f34cba6..da1603e3201 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -399,6 +399,7 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator): manufacturer="Shelly", model=aioshelly.const.MODEL_NAMES.get(self.model, self.model), sw_version=sw_version, + configuration_url=f"http://{self.entry.data[CONF_HOST]}", ) self.device_id = entry.id self.device.subscribe_updates(self.async_set_updated_data) @@ -635,6 +636,7 @@ class RpcDeviceWrapper(update_coordinator.DataUpdateCoordinator): manufacturer="Shelly", model=aioshelly.const.MODEL_NAMES.get(self.model, self.model), sw_version=sw_version, + configuration_url=f"http://{self.entry.data[CONF_HOST]}", ) self.device_id = entry.id self.device.subscribe_updates(self.async_set_updated_data) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 9348c112942..b41df3d6aa0 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -55,6 +55,7 @@ class DeviceEntry: 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) connections: set[tuple[str, str]] = attr.ib(converter=set, factory=set) disabled_by: str | None = attr.ib( default=None, @@ -244,6 +245,7 @@ class DeviceRegistry: self, *, config_entry_id: str, + configuration_url: str | None | UndefinedType = UNDEFINED, connections: set[tuple[str, str]] | None = None, default_manufacturer: str | None | UndefinedType = UNDEFINED, default_model: str | None | UndefinedType = UNDEFINED, @@ -302,6 +304,7 @@ class DeviceRegistry: device = self._async_update_device( device.id, add_config_entry_id=config_entry_id, + configuration_url=configuration_url, disabled_by=disabled_by, entry_type=entry_type, manufacturer=manufacturer, @@ -326,6 +329,7 @@ class DeviceRegistry: *, add_config_entry_id: str | UndefinedType = UNDEFINED, area_id: str | None | UndefinedType = UNDEFINED, + configuration_url: str | None | UndefinedType = UNDEFINED, disabled_by: str | None | UndefinedType = UNDEFINED, manufacturer: str | None | UndefinedType = UNDEFINED, model: str | None | UndefinedType = UNDEFINED, @@ -342,6 +346,7 @@ class DeviceRegistry: device_id, add_config_entry_id=add_config_entry_id, area_id=area_id, + configuration_url=configuration_url, disabled_by=disabled_by, manufacturer=manufacturer, model=model, @@ -361,6 +366,7 @@ class DeviceRegistry: *, add_config_entry_id: str | UndefinedType = UNDEFINED, area_id: str | None | UndefinedType = UNDEFINED, + configuration_url: str | None | UndefinedType = UNDEFINED, disabled_by: str | None | UndefinedType = UNDEFINED, entry_type: str | None | UndefinedType = UNDEFINED, manufacturer: str | None | UndefinedType = UNDEFINED, @@ -424,6 +430,7 @@ class DeviceRegistry: changes["identifiers"] = new_identifiers for attr_name, value in ( + ("configuration_url", configuration_url), ("disabled_by", disabled_by), ("entry_type", entry_type), ("manufacturer", manufacturer), @@ -514,6 +521,8 @@ class DeviceRegistry: name_by_user=device.get("name_by_user"), # Introduced in 0.119 disabled_by=device.get("disabled_by"), + # Introduced in 2021.11 + configuration_url=device.get("configuration_url"), ) # Introduced in 0.111 for device in data.get("deleted_devices", []): @@ -556,6 +565,7 @@ class DeviceRegistry: "area_id": entry.area_id, "name_by_user": entry.name_by_user, "disabled_by": entry.disabled_by, + "configuration_url": entry.configuration_url, } for entry in self.devices.values() ] diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 584fa4f0554..2ad0f934973 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -158,6 +158,7 @@ def get_unit_of_measurement(hass: HomeAssistant, entity_id: str) -> str | None: class DeviceInfo(TypedDict, total=False): """Entity device information for device registry.""" + configuration_url: str | None connections: set[tuple[str, str]] default_manufacturer: str default_model: str diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 0d966cde313..871ac92e3a6 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -473,6 +473,17 @@ class EntityPlatform: if key in device_info: processed_dev_info[key] = device_info[key] # type: ignore[misc] + if "configuration_url" in device_info: + try: + processed_dev_info["configuration_url"] = cv.url( + device_info["configuration_url"] + ) + except vol.Invalid: + _LOGGER.warning( + "Ignoring invalid device configuration_url '%s'", + device_info["configuration_url"], + ) + try: device = device_registry.async_get_or_create(**processed_dev_info) # type: ignore[arg-type] device_id = device.id diff --git a/tests/components/config/test_device_registry.py b/tests/components/config/test_device_registry.py index 4e10413f14f..0d9170f0a83 100644 --- a/tests/components/config/test_device_registry.py +++ b/tests/components/config/test_device_registry.py @@ -58,6 +58,7 @@ async def test_list_devices(hass, client, registry): "area_id": None, "name_by_user": None, "disabled_by": None, + "configuration_url": None, }, { "config_entries": ["1234"], @@ -72,6 +73,7 @@ async def test_list_devices(hass, client, registry): "area_id": None, "name_by_user": None, "disabled_by": None, + "configuration_url": None, }, ] diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index 9dbba7732ac..a0d4a27bbc4 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -108,7 +108,7 @@ async def coap_wrapper(hass): config_entry = MockConfigEntry( domain=DOMAIN, - data={"sleep_period": 0, "model": "SHSW-25"}, + data={"sleep_period": 0, "model": "SHSW-25", "host": "1.2.3.4"}, unique_id="12345678", ) config_entry.add_to_hass(hass) @@ -140,7 +140,7 @@ async def rpc_wrapper(hass): config_entry = MockConfigEntry( domain=DOMAIN, - data={"sleep_period": 0, "model": "SNSW-001P16EU", "gen": 2}, + data={"sleep_period": 0, "model": "SNSW-001P16EU", "gen": 2, "host": "1.2.3.4"}, unique_id="12345678", ) config_entry.add_to_hass(hass) diff --git a/tests/components/shelly/test_device_trigger.py b/tests/components/shelly/test_device_trigger.py index 41fbad2f8e3..a81662159c2 100644 --- a/tests/components/shelly/test_device_trigger.py +++ b/tests/components/shelly/test_device_trigger.py @@ -109,7 +109,7 @@ async def test_get_triggers_button(hass): config_entry = MockConfigEntry( domain=DOMAIN, - data={"sleep_period": 43200, "model": "SHBTN-1"}, + data={"sleep_period": 43200, "model": "SHBTN-1", "host": "1.2.3.4"}, unique_id="12345678", ) config_entry.add_to_hass(hass) diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 65a46f33cd8..9f213801355 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -832,6 +832,7 @@ async def test_device_info_called(hass): unique_id="qwer", device_info={ "identifiers": {("hue", "1234")}, + "configuration_url": "http://192.168.0.100/config", "connections": {(dr.CONNECTION_NETWORK_MAC, "abcd")}, "manufacturer": "test-manuf", "model": "test-model", @@ -860,13 +861,14 @@ async def test_device_info_called(hass): device = registry.async_get_device({("hue", "1234")}) assert device is not None assert device.identifiers == {("hue", "1234")} + assert device.configuration_url == "http://192.168.0.100/config" assert device.connections == {(dr.CONNECTION_NETWORK_MAC, "abcd")} + assert device.entry_type == "service" assert device.manufacturer == "test-manuf" assert device.model == "test-model" assert device.name == "test-name" - assert device.sw_version == "test-sw" assert device.suggested_area == "Heliport" - assert device.entry_type == "service" + assert device.sw_version == "test-sw" assert device.via_device_id == via.id @@ -916,6 +918,55 @@ async def test_device_info_not_overrides(hass): assert device2.model == "test-model" +async def test_device_info_invalid_url(hass, caplog): + """Test device info is forwarded correctly.""" + registry = dr.async_get(hass) + registry.async_get_or_create( + config_entry_id="123", + connections=set(), + identifiers={("hue", "via-id")}, + manufacturer="manufacturer", + model="via", + ) + + async def async_setup_entry(hass, config_entry, async_add_entities): + """Mock setup entry method.""" + async_add_entities( + [ + # Valid device info, but invalid url + MockEntity( + unique_id="qwer", + device_info={ + "identifiers": {("hue", "1234")}, + "configuration_url": "foo://192.168.0.100/config", + }, + ), + ] + ) + return True + + platform = MockPlatform(async_setup_entry=async_setup_entry) + config_entry = MockConfigEntry(entry_id="super-mock-id") + entity_platform = MockEntityPlatform( + hass, platform_name=config_entry.domain, platform=platform + ) + + assert await entity_platform.async_setup_entry(config_entry) + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids()) == 1 + + device = registry.async_get_device({("hue", "1234")}) + assert device is not None + assert device.identifiers == {("hue", "1234")} + assert device.configuration_url is None + + assert ( + "Ignoring invalid device configuration_url 'foo://192.168.0.100/config'" + in caplog.text + ) + + async def test_entity_disabled_by_integration(hass): """Test entity disabled by integration.""" component = EntityComponent(_LOGGER, DOMAIN, hass, timedelta(seconds=20))