diff --git a/homeassistant/components/bosch_shc/binary_sensor.py b/homeassistant/components/bosch_shc/binary_sensor.py index f77313556719..7f5dc3dd7052 100644 --- a/homeassistant/components/bosch_shc/binary_sensor.py +++ b/homeassistant/components/bosch_shc/binary_sensor.py @@ -37,6 +37,7 @@ async def async_setup_entry( entities: list[BinarySensorEntity] = [ ShutterContactSensor( + hass=hass, device=binary_sensor, parent_id=shc_info.unique_id, entry_id=config_entry.entry_id, @@ -49,6 +50,7 @@ async def async_setup_entry( entities.extend( BatterySensor( + hass=hass, device=binary_sensor, parent_id=shc_info.unique_id, entry_id=config_entry.entry_id, @@ -75,9 +77,11 @@ class ShutterContactSensor(SHCEntity, BinarySensorEntity): _attr_name = None _device: SHCShutterContact - def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: + def __init__( + self, hass: HomeAssistant, device: SHCDevice, parent_id: str, entry_id: str + ) -> None: """Initialize an SHC shutter contact sensor.""" - super().__init__(device, parent_id, entry_id) + super().__init__(hass, device, parent_id, entry_id) switcher: dict[str | None, BinarySensorDeviceClass] = { "ENTRANCE_DOOR": BinarySensorDeviceClass.DOOR, "REGULAR_WINDOW": BinarySensorDeviceClass.WINDOW, @@ -101,9 +105,11 @@ class BatterySensor(SHCEntity, BinarySensorEntity): _attr_device_class = BinarySensorDeviceClass.BATTERY _device: SHCBatteryDevice - def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: + def __init__( + self, hass: HomeAssistant, device: SHCDevice, parent_id: str, entry_id: str + ) -> None: """Initialize an SHC battery reporting sensor.""" - super().__init__(device, parent_id, entry_id) + super().__init__(hass, device, parent_id, entry_id) self._attr_unique_id = f"{device.serial}_battery" @property diff --git a/homeassistant/components/bosch_shc/cover.py b/homeassistant/components/bosch_shc/cover.py index 536c36786879..9512db0db494 100644 --- a/homeassistant/components/bosch_shc/cover.py +++ b/homeassistant/components/bosch_shc/cover.py @@ -33,6 +33,7 @@ async def async_setup_entry( async_add_entities( ShutterControlCover( + hass=hass, device=cover, parent_id=shc_info.unique_id, entry_id=config_entry.entry_id, diff --git a/homeassistant/components/bosch_shc/entity.py b/homeassistant/components/bosch_shc/entity.py index b43a07741057..cbea42da4914 100644 --- a/homeassistant/components/bosch_shc/entity.py +++ b/homeassistant/components/bosch_shc/entity.py @@ -2,7 +2,7 @@ from typing import override -from boschshcpy import SHCDevice, SHCIntrusionSystem +from boschshcpy import SHCDevice from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr @@ -30,9 +30,7 @@ class SHCBaseEntity(Entity): _attr_should_poll = False _attr_has_entity_name = True - def __init__( - self, device: SHCDevice | SHCIntrusionSystem, parent_id: str, entry_id: str - ) -> None: + def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: """Initialize the generic SHC device.""" self._device = device self._entry_id = entry_id @@ -67,16 +65,25 @@ class SHCEntity(SHCBaseEntity): _device: SHCDevice - def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: + def __init__( + self, hass: HomeAssistant, device: SHCDevice, parent_id: str, entry_id: str + ) -> None: """Initialize generic SHC device.""" self._attr_unique_id = device.serial - self._attr_device_info = DeviceInfo( + device_info = DeviceInfo( identifiers={(DOMAIN, device.id)}, manufacturer=device.manufacturer, model=device.device_model, name=device.name, - via_device=(DOMAIN, device.root_device_id), ) + # boschshcpy may render the hub identifier (shc_info.unique_id) and a + # device's root_device_id differently, so the lookup can miss; link only + # when it resolves instead of raising out of setup. + if hub := dr.async_get(hass).async_get_device_by_identifier( + (DOMAIN, device.root_device_id), entry_id + ): + device_info["via_device_id"] = hub.id + self._attr_device_info = device_info super().__init__(device=device, parent_id=parent_id, entry_id=entry_id) @override @@ -102,32 +109,3 @@ class SHCEntity(SHCBaseEntity): def available(self) -> bool: """Return false if status is unavailable.""" return self._device.status == "AVAILABLE" - - -class SHCDomainEntity(SHCBaseEntity): - """Representation of a SHC domain service entity.""" - - _device: SHCIntrusionSystem - - def __init__( - self, domain: SHCIntrusionSystem, parent_id: str, entry_id: str - ) -> None: - """Initialize the generic SHC device.""" - self._attr_unique_id = domain.id - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, domain.id)}, - manufacturer=domain.manufacturer, - model=domain.device_model, - name=domain.name, - via_device=( - DOMAIN, - parent_id, - ), - ) - super().__init__(device=domain, parent_id=parent_id, entry_id=entry_id) - - @property - @override - def available(self) -> bool: - """Return false if status is unavailable.""" - return self._device.system_availability diff --git a/homeassistant/components/bosch_shc/sensor.py b/homeassistant/components/bosch_shc/sensor.py index 56da180b2ebf..114decd4880b 100644 --- a/homeassistant/components/bosch_shc/sensor.py +++ b/homeassistant/components/bosch_shc/sensor.py @@ -212,6 +212,7 @@ async def async_setup_entry( entities: list[SensorEntity] = [ SHCSensor( + hass, device, description, shc_info.unique_id, @@ -226,6 +227,7 @@ async def async_setup_entry( entities.extend( SHCSensor( + hass, device, description, shc_info.unique_id, @@ -240,6 +242,7 @@ async def async_setup_entry( entities.extend( SHCSensor( + hass, device, description, shc_info.unique_id, @@ -263,6 +266,7 @@ async def async_setup_entry( ] entities.extend( SHCSensor( + hass, device, description, shc_info.unique_id, @@ -274,6 +278,7 @@ async def async_setup_entry( entities.extend( SHCSensor( + hass, device, description, shc_info.unique_id, @@ -297,13 +302,14 @@ class SHCSensor[_DeviceT: SHCDevice](SHCEntity, SensorEntity): def __init__( self, + hass: HomeAssistant, device: _DeviceT, entity_description: SHCSensorEntityDescription[_DeviceT], parent_id: str, entry_id: str, ) -> None: """Initialize sensor.""" - super().__init__(device, parent_id, entry_id) + super().__init__(hass, device, parent_id, entry_id) self._device: _DeviceT = device self.entity_description = entity_description self._attr_unique_id = f"{device.serial}_{entity_description.key}" diff --git a/homeassistant/components/bosch_shc/switch.py b/homeassistant/components/bosch_shc/switch.py index ac3f9e110353..091ca8e5a74f 100644 --- a/homeassistant/components/bosch_shc/switch.py +++ b/homeassistant/components/bosch_shc/switch.py @@ -89,6 +89,7 @@ async def async_setup_entry( entities: list[SwitchEntity] = [ SHCSwitch( + hass=hass, device=switch, parent_id=shc_info.unique_id, entry_id=config_entry.entry_id, @@ -99,6 +100,7 @@ async def async_setup_entry( entities.extend( SHCRoutingSwitch( + hass=hass, device=switch, parent_id=shc_info.unique_id, entry_id=config_entry.entry_id, @@ -108,6 +110,7 @@ async def async_setup_entry( entities.extend( SHCSwitch( + hass=hass, device=switch, parent_id=shc_info.unique_id, entry_id=config_entry.entry_id, @@ -118,6 +121,7 @@ async def async_setup_entry( entities.extend( SHCSwitch( + hass=hass, device=switch, parent_id=shc_info.unique_id, entry_id=config_entry.entry_id, @@ -128,6 +132,7 @@ async def async_setup_entry( entities.extend( SHCSwitch( + hass=hass, device=switch, parent_id=shc_info.unique_id, entry_id=config_entry.entry_id, @@ -138,6 +143,7 @@ async def async_setup_entry( entities.extend( SHCSwitch( + hass=hass, device=switch, parent_id=shc_info.unique_id, entry_id=config_entry.entry_id, @@ -156,13 +162,14 @@ class SHCSwitch(SHCEntity, SwitchEntity): def __init__( self, + hass: HomeAssistant, device: SHCDevice, parent_id: str, entry_id: str, description: SHCSwitchEntityDescription, ) -> None: """Initialize a SHC switch.""" - super().__init__(device, parent_id, entry_id) + super().__init__(hass, device, parent_id, entry_id) self.entity_description = description @property @@ -202,9 +209,11 @@ class SHCRoutingSwitch(SHCEntity, SwitchEntity): _attr_entity_category = EntityCategory.CONFIG _device: SHCSmartPlug - def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: + def __init__( + self, hass: HomeAssistant, device: SHCDevice, parent_id: str, entry_id: str + ) -> None: """Initialize an SHC routing switch.""" - super().__init__(device, parent_id, entry_id) + super().__init__(hass, device, parent_id, entry_id) self._attr_unique_id = f"{device.serial}_routing" @property diff --git a/tests/components/bosch_shc/test_entity.py b/tests/components/bosch_shc/test_entity.py new file mode 100644 index 000000000000..63904273ae28 --- /dev/null +++ b/tests/components/bosch_shc/test_entity.py @@ -0,0 +1,78 @@ +"""Tests for the Bosch SHC entity base classes.""" + +from unittest.mock import MagicMock + +from boschshcpy import SHCDevice + +from homeassistant.components.bosch_shc.const import DOMAIN +from homeassistant.components.bosch_shc.entity import SHCEntity +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from tests.common import MockConfigEntry + + +async def test_shc_entity_via_device_id( + hass: HomeAssistant, device_registry: dr.DeviceRegistry +) -> None: + """Test SHCEntity links its device to the SHC hub via via_device_id.""" + entry = MockConfigEntry(domain=DOMAIN) + entry.add_to_hass(hass) + + hub_device = device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, "root-serial")}, + manufacturer="Bosch", + name="Bosch SHC", + model="SmartHomeController", + ) + + device = MagicMock(spec=SHCDevice) + device.serial = "child-serial" + device.manufacturer = "Bosch" + device.device_model = "SWD" + device.name = "Shutter Contact" + device.id = "child-id" + device.root_device_id = "root-serial" + + entity = SHCEntity( + hass=hass, device=device, parent_id="root-serial", entry_id=entry.entry_id + ) + + assert entity.device_info is not None + assert entity.device_info["via_device_id"] == hub_device.id + + +async def test_shc_entity_via_device_id_mismatch( + hass: HomeAssistant, device_registry: dr.DeviceRegistry +) -> None: + """Test SHCEntity sets up without a link when the hub identifier does not match. + + boschshcpy may render the hub identifier and a device's root_device_id + differently, so the lookup can miss; setup must not crash in that case. + """ + entry = MockConfigEntry(domain=DOMAIN) + entry.add_to_hass(hass) + + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, "hub-serial")}, + manufacturer="Bosch", + name="Bosch SHC", + model="SmartHomeController", + ) + + device = MagicMock(spec=SHCDevice) + device.serial = "child-serial" + device.manufacturer = "Bosch" + device.device_model = "SWD" + device.name = "Shutter Contact" + device.id = "child-id" + device.root_device_id = "root-serial-mismatch" + + entity = SHCEntity( + hass=hass, device=device, parent_id="hub-serial", entry_id=entry.entry_id + ) + + assert entity.device_info is not None + assert "via_device_id" not in entity.device_info