From 620a329431231a9d44c45b75928b8c87bd66ef3b Mon Sep 17 00:00:00 2001 From: Christian Lackas Date: Thu, 24 Sep 2026 16:23:01 +0200 Subject: [PATCH] Treat unknown homematicip_cloud device types as devices (#182394) Co-authored-by: Markus Tuominen <3738613+Markus98@users.noreply.github.com> --- .../components/homematicip_cloud/entity.py | 13 ++--- .../components/homematicip_cloud/conftest.py | 47 +++++++++++++++++++ .../homematicip_cloud/test_device.py | 34 ++++++++++++++ 3 files changed, 88 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/entity.py b/homeassistant/components/homematicip_cloud/entity.py index 78627a16cf3a..9573567e2fb3 100644 --- a/homeassistant/components/homematicip_cloud/entity.py +++ b/homeassistant/components/homematicip_cloud/entity.py @@ -5,7 +5,7 @@ import logging from typing import TYPE_CHECKING, Any, override from homematicip.base.functionalChannels import FunctionalChannel -from homematicip.device import Device +from homematicip.device import BaseDevice from homematicip.group import Group from homeassistant.const import ATTR_ID @@ -133,7 +133,7 @@ class HomematicipGenericEntity(Entity): def device_info(self) -> DeviceInfo | None: """Return device specific attributes.""" # Only physical devices should be HA devices. - if isinstance(self._device, Device): + if isinstance(self._device, BaseDevice): device_id = str(self._device.id) home_id = str(self._device.homeId) @@ -152,7 +152,7 @@ class HomematicipGenericEntity(Entity): # Serial numbers of Homematic IP device (DOMAIN, device_id) }, - manufacturer=self._device.oem, + manufacturer=getattr(self._device, "oem", None), model=self._device.modelType, name=device_name, sw_version=self._device.firmwareVersion, @@ -325,13 +325,14 @@ class HomematicipGenericEntity(Entity): @override def available(self) -> bool: """Return if entity is available.""" - return not self._device.unreach + # BaseDevice, the fallback for an unknown device type, has no unreach. + return not getattr(self._device, "unreach", False) @property @override def unique_id(self) -> str: """Return a unique ID.""" - if not isinstance(self._device, Device): + if not isinstance(self._device, BaseDevice): return f"{self._device.id}_{self._feature_id}" channel_index = self.get_channel_index() return f"{self._device.id}_{channel_index}_{self._feature_id}" @@ -352,7 +353,7 @@ class HomematicipGenericEntity(Entity): """Return the state attributes of the generic entity.""" state_attr = {} - if isinstance(self._device, Device): + if isinstance(self._device, BaseDevice): for attr, attr_key in DEVICE_ATTRIBUTES.items(): if attr_value := getattr(self._device, attr, None): state_attr[attr_key] = attr_value diff --git a/tests/components/homematicip_cloud/conftest.py b/tests/components/homematicip_cloud/conftest.py index af9337cec926..d8701ad194d0 100644 --- a/tests/components/homematicip_cloud/conftest.py +++ b/tests/components/homematicip_cloud/conftest.py @@ -70,6 +70,53 @@ async def default_mock_hap_factory_fixture( return HomeFactory(hass, mock_connection, hmip_config_entry) +@pytest.fixture(name="unknown_type_device_data") +def unknown_type_device_data_fixture() -> dict[str, Any]: + """Return fixture data for a device whose type the library does not know. + + The type is invented so that it stays unknown as devices get added. + """ + device_id = "3014F711000000000UNKNOWN" + return { + "connectionType": "HMIP_RF", + "deviceArchetype": "HMIP", + "firmwareVersion": "1.0.10", + "functionalChannels": { + "0": { + "deviceId": device_id, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [], + "index": 0, + "label": "", + }, + "1": { + "deviceId": device_id, + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 1, + "groups": [], + "index": 1, + "label": "", + }, + "2": { + "deviceId": device_id, + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 2, + "groups": [], + "index": 2, + "label": "", + }, + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": device_id, + "label": "Unknown Device", + "lastStatusUpdate": 1614066137987, + "modelType": "HmIP-UNKNOWN", + "permanentlyReachable": True, + "type": "SOME_FUTURE_DEVICE", + } + + @pytest.fixture(name="full_flush_lock_controller_device_data") def full_flush_lock_controller_device_data_fixture() -> dict[str, Any]: """Return fixture data for an HmIP-FLC device.""" diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py index 364ff675300b..5b677bd95e7a 100644 --- a/tests/components/homematicip_cloud/test_device.py +++ b/tests/components/homematicip_cloud/test_device.py @@ -1,5 +1,6 @@ """Common tests for HomematicIP devices.""" +from typing import Any from unittest.mock import patch from homematicip.base.enums import EventType @@ -330,3 +331,36 @@ async def test_hmip_child_device_links_to_access_point( assert access_point_device is not None assert child_device.via_device_id == access_point_device.id + + +async def test_hmip_unknown_device_type( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + default_mock_hap_factory: HomeFactory, + unknown_type_device_data: dict[str, Any], +) -> None: + """Test a device whose type the library does not know yet.""" + await default_mock_hap_factory.async_get_mock_hap( + test_devices=["Unknown Device"], extra_devices=[unknown_type_device_data] + ) + + entities = [ + entry + for entry in entity_registry.entities.values() + if entry.unique_id.startswith(unknown_type_device_data["id"]) + ] + assert sorted(entry.unique_id for entry in entities) == [ + "3014F711000000000UNKNOWN_1_button", + "3014F711000000000UNKNOWN_2_button", + ] + + device = device_registry.async_get(entities[0].device_id) + assert device is not None + assert (DOMAIN, unknown_type_device_data["id"]) in device.identifiers + assert {entry.device_id for entry in entities} == {device.id} + + for entry in entities: + state = hass.states.get(entry.entity_id) + assert state is not None + assert state.state != STATE_UNAVAILABLE