Adapt bosch_shc to set via_device_id in DeviceInfo (#177711)

This commit is contained in:
Erik Montnemery
2026-08-03 17:47:58 +02:00
committed by GitHub
parent cfae509a3c
commit 653eef9b3f
6 changed files with 122 additions and 44 deletions
@@ -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
@@ -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,
+14 -36
View File
@@ -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
+7 -1
View File
@@ -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}"
+12 -3
View File
@@ -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
+78
View File
@@ -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