diff --git a/homeassistant/components/bosch_shc/binary_sensor.py b/homeassistant/components/bosch_shc/binary_sensor.py index 7f5dc3dd7052..7b99579fbe9c 100644 --- a/homeassistant/components/bosch_shc/binary_sensor.py +++ b/homeassistant/components/bosch_shc/binary_sensor.py @@ -1,5 +1,7 @@ """Platform for binarysensor integration.""" +from collections.abc import Callable +from dataclasses import dataclass from typing import TYPE_CHECKING, override from boschshcpy import ( @@ -13,6 +15,7 @@ from boschshcpy.device import SHCDevice from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, + BinarySensorEntityDescription, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback @@ -23,6 +26,19 @@ from .entity import SHCEntity PARALLEL_UPDATES = 0 +@dataclass(frozen=True, kw_only=True) +class SHCShutterContactSensorEntityDescription(BinarySensorEntityDescription): + """Describes a SHC shutter contact binary sensor.""" + + is_on_fn: Callable[[SHCShutterContact], bool] + + +SHUTTER_CONTACT_DESCRIPTION = SHCShutterContactSensorEntityDescription( + key="shutter_contact", + is_on_fn=lambda device: device.state is ShutterContactService.State.OPEN, +) + + async def async_setup_entry( hass: HomeAssistant, config_entry: BoschConfigEntry, @@ -41,6 +57,7 @@ async def async_setup_entry( device=binary_sensor, parent_id=shc_info.unique_id, entry_id=config_entry.entry_id, + entity_description=SHUTTER_CONTACT_DESCRIPTION, ) for binary_sensor in ( *session.device_helper.shutter_contacts, @@ -76,11 +93,18 @@ class ShutterContactSensor(SHCEntity, BinarySensorEntity): _attr_name = None _device: SHCShutterContact + entity_description: SHCShutterContactSensorEntityDescription def __init__( - self, hass: HomeAssistant, device: SHCDevice, parent_id: str, entry_id: str + self, + hass: HomeAssistant, + device: SHCDevice, + parent_id: str, + entry_id: str, + entity_description: SHCShutterContactSensorEntityDescription, ) -> None: """Initialize an SHC shutter contact sensor.""" + self.entity_description = entity_description super().__init__(hass, device, parent_id, entry_id) switcher: dict[str | None, BinarySensorDeviceClass] = { "ENTRANCE_DOOR": BinarySensorDeviceClass.DOOR, @@ -96,7 +120,7 @@ class ShutterContactSensor(SHCEntity, BinarySensorEntity): @override def is_on(self) -> bool: """Return the state of the sensor.""" - return self._device.state is ShutterContactService.State.OPEN + return self.entity_description.is_on_fn(self._device) class BatterySensor(SHCEntity, BinarySensorEntity): diff --git a/tests/components/bosch_shc/conftest.py b/tests/components/bosch_shc/conftest.py index 025cfe83df0b..e9100a4e41d6 100644 --- a/tests/components/bosch_shc/conftest.py +++ b/tests/components/bosch_shc/conftest.py @@ -16,6 +16,7 @@ from boschshcpy import ( SHCMicromoduleRelay, SHCMotionDetector2, SHCPresenceSimulationSystem, + SHCShutterContact, SHCShutterContact2, SHCShutterControl, SHCSmartPlug, @@ -24,6 +25,7 @@ from boschshcpy import ( SHCThermostat, SHCThermostatGen2, SHCTwinguard, + ShutterContactService, ShutterControlService, ThermostatService, ) @@ -367,6 +369,28 @@ def presence_simulation_system_device( return device +def shutter_contact_device( + device_id: str = "hdm:ZigBee:shuttercontact1", + name: str = "Shutter contact", + device_class: str = "GENERIC", + state: ShutterContactService.State = ShutterContactService.State.CLOSED, +) -> SHCShutterContact: + """Build a minimal device double for the shutter_contacts bucket.""" + device = create_autospec(SHCShutterContact, instance=True, spec_set=True) + device.name = name + device.id = device_id + device.root_device_id = "test-mac" + device.serial = f"serial-{device_id}" + device.manufacturer = "Bosch" + device.device_model = "SWD" + device.device_class = device_class + device.device_services = [] + device.deleted = False + device.status = "AVAILABLE" + device.state = state + return device + + def shutter_contact2_device( device_id: str = "hdm:ZigBee:shuttercontact1", name: str = "Shutter contact", diff --git a/tests/components/bosch_shc/test_binary_sensor.py b/tests/components/bosch_shc/test_binary_sensor.py new file mode 100644 index 000000000000..0036e6fdab69 --- /dev/null +++ b/tests/components/bosch_shc/test_binary_sensor.py @@ -0,0 +1,76 @@ +"""Tests for the Bosch SHC binary_sensor platform.""" + +from boschshcpy import ShutterContactService +import pytest + +from homeassistant.core import HomeAssistant + +from .conftest import setup_integration, shutter_contact_device + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize( + "device_buckets", + [ + { + "shutter_contacts": [ + shutter_contact_device(state=ShutterContactService.State.CLOSED) + ] + } + ], + indirect=True, +) +@pytest.mark.usefixtures("mock_session") +async def test_shutter_contact_closed( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """A closed Shutter Contact is reported as off.""" + await setup_integration(hass, mock_config_entry) + + state = hass.states.get("binary_sensor.shutter_contact") + assert state is not None + assert state.state == "off" + + +@pytest.mark.parametrize( + "device_buckets", + [ + { + "shutter_contacts": [ + shutter_contact_device(state=ShutterContactService.State.OPEN) + ] + } + ], + indirect=True, +) +@pytest.mark.usefixtures("mock_session") +async def test_shutter_contact_open( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """An open Shutter Contact is reported as on.""" + await setup_integration(hass, mock_config_entry) + + state = hass.states.get("binary_sensor.shutter_contact") + assert state is not None + assert state.state == "on" + + +@pytest.mark.parametrize( + "device_buckets", + [{"shutter_contacts": [shutter_contact_device(device_class="ENTRANCE_DOOR")]}], + indirect=True, +) +@pytest.mark.usefixtures("mock_session") +async def test_shutter_contact_device_class( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """A Shutter Contact's device_class maps to the binary_sensor device class.""" + await setup_integration(hass, mock_config_entry) + + state = hass.states.get("binary_sensor.shutter_contact") + assert state is not None + assert state.attributes["device_class"] == "door"