diff --git a/homeassistant/components/bosch_shc/select.py b/homeassistant/components/bosch_shc/select.py index bee48f8af4b4..523d5113bedc 100644 --- a/homeassistant/components/bosch_shc/select.py +++ b/homeassistant/components/bosch_shc/select.py @@ -4,8 +4,9 @@ from collections.abc import Callable, Coroutine, Sequence from dataclasses import dataclass from typing import TYPE_CHECKING, Any, override -from boschshcpy import OutdoorSirenService, SHCOutdoorSiren +from boschshcpy import OutdoorSirenService, SHCMotionDetector2, SHCOutdoorSiren from boschshcpy.device import SHCDevice +from boschshcpy.services_impl import PirSensorConfigurationService from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.const import EntityCategory @@ -52,6 +53,23 @@ SIREN_SOUND_LEVEL_DESCRIPTION = SHCSelectEntityDescription[SHCOutdoorSiren]( ) +async def _motion_select_option(device: SHCMotionDetector2, option: str) -> None: + """Write the Motion Detector II's motion sensitivity.""" + await device.async_set_motion_sensitivity( + PirSensorConfigurationService.MotionSensitivity[option.upper()] + ) + + +MOTION_SENSITIVITY_DESCRIPTION = SHCSelectEntityDescription[SHCMotionDetector2]( + key="motion_sensitivity", + translation_key="motion_sensitivity", + entity_category=EntityCategory.CONFIG, + options=["high", "middle", "low"], + current_option_fn=lambda device, options: device.motion_sensitivity.name.lower(), + select_option_fn=_motion_select_option, +) + + async def async_setup_entry( hass: HomeAssistant, config_entry: BoschConfigEntry, @@ -75,6 +93,25 @@ async def async_setup_entry( if siren.siren is not None ) + motion_detectors: list[SHCMotionDetector2] = [] + for detector in session.device_helper.motion_detectors2: + try: + _ = detector.motion_sensitivity + except AttributeError: + continue + motion_detectors.append(detector) + + async_add_entities( + SHCSelect( + hass=hass, + device=detector, + parent_id=shc_info.unique_id, + entry_id=config_entry.entry_id, + description=MOTION_SENSITIVITY_DESCRIPTION, + ) + for detector in motion_detectors + ) + class SHCSelect[_DeviceT: SHCDevice](SHCEntity, SelectEntity): """Generic SHC select entity, driven by a SHCSelectEntityDescription.""" diff --git a/homeassistant/components/bosch_shc/strings.json b/homeassistant/components/bosch_shc/strings.json index 942933f9e374..aa97d877aafd 100644 --- a/homeassistant/components/bosch_shc/strings.json +++ b/homeassistant/components/bosch_shc/strings.json @@ -71,6 +71,14 @@ } }, "select": { + "motion_sensitivity": { + "name": "Motion sensitivity", + "state": { + "high": "High", + "low": "Low", + "middle": "Medium" + } + }, "siren_sound_level": { "name": "Siren volume", "state": { diff --git a/tests/components/bosch_shc/conftest.py b/tests/components/bosch_shc/conftest.py index ac66e05b931e..48ea139a5abe 100644 --- a/tests/components/bosch_shc/conftest.py +++ b/tests/components/bosch_shc/conftest.py @@ -44,6 +44,7 @@ from boschshcpy import ( ) from boschshcpy.services_impl import ( OutdoorSirenService, + PirSensorConfigurationService, PresenceSimulationConfigurationService, ValveTappetService, ) @@ -633,6 +634,8 @@ def motion_detector2_device( supports_smart_sensitivity: bool = False, smart_sensitivity_enabled: bool = False, latestmotion: str = "", + motion_sensitivity: PirSensorConfigurationService.MotionSensitivity + | None = PirSensorConfigurationService.MotionSensitivity.MIDDLE, ) -> SHCMotionDetector2: """Build a minimal device double for the motion_detectors2 bucket.""" device = create_autospec(SHCMotionDetector2, instance=True, spec_set=True) @@ -650,6 +653,10 @@ def motion_detector2_device( device.supports_smart_sensitivity = supports_smart_sensitivity device.smart_sensitivity_enabled = smart_sensitivity_enabled device.latestmotion = latestmotion + if motion_sensitivity is None: + del device.motion_sensitivity + else: + device.motion_sensitivity = motion_sensitivity return device diff --git a/tests/components/bosch_shc/test_select.py b/tests/components/bosch_shc/test_select.py index 4c86af6a421b..b9334dcbe7d1 100644 --- a/tests/components/bosch_shc/test_select.py +++ b/tests/components/bosch_shc/test_select.py @@ -2,18 +2,19 @@ from unittest.mock import MagicMock -from boschshcpy.services_impl import OutdoorSirenService +from boschshcpy.services_impl import OutdoorSirenService, PirSensorConfigurationService import pytest from homeassistant.components.select import DOMAIN as SELECT_DOMAIN from homeassistant.const import ATTR_ENTITY_ID, SERVICE_SELECT_OPTION from homeassistant.core import HomeAssistant -from .conftest import outdoor_siren_device, setup_integration +from .conftest import motion_detector2_device, outdoor_siren_device, setup_integration from tests.common import MockConfigEntry SOUND_LEVEL_ENTITY_ID = "select.outdoor_siren_siren_volume" +MOTION_SENSITIVITY_ENTITY_ID = "select.motion_detector_motion_sensitivity" @pytest.mark.parametrize( @@ -82,3 +83,71 @@ async def test_outdoor_siren_no_siren_service( await setup_integration(hass, mock_config_entry) assert hass.states.get(SOUND_LEVEL_ENTITY_ID) is None + + +@pytest.mark.parametrize( + "device_buckets", + [ + { + "motion_detectors2": [ + motion_detector2_device( + motion_sensitivity=PirSensorConfigurationService.MotionSensitivity.LOW + ) + ] + } + ], + indirect=True, +) +@pytest.mark.usefixtures("mock_session") +async def test_motion_sensitivity_current_option( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """The Motion Detector II's current motion sensitivity is reported.""" + await setup_integration(hass, mock_config_entry) + + state = hass.states.get(MOTION_SENSITIVITY_ENTITY_ID) + assert state is not None + assert state.state == "low" + + +@pytest.mark.parametrize( + "device_buckets", + [{"motion_detectors2": [motion_detector2_device()]}], + indirect=True, +) +@pytest.mark.usefixtures("mock_session") +async def test_motion_sensitivity_select_option( + hass: HomeAssistant, + mock_session: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Selecting a motion sensitivity writes it to the device.""" + await setup_integration(hass, mock_config_entry) + device = mock_session.device_helper.motion_detectors2[0] + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + {ATTR_ENTITY_ID: MOTION_SENSITIVITY_ENTITY_ID, "option": "high"}, + blocking=True, + ) + device.async_set_motion_sensitivity.assert_awaited_once_with( + PirSensorConfigurationService.MotionSensitivity.HIGH + ) + + +@pytest.mark.parametrize( + "device_buckets", + [{"motion_detectors2": [motion_detector2_device(motion_sensitivity=None)]}], + indirect=True, +) +@pytest.mark.usefixtures("mock_session") +async def test_motion_sensitivity_not_supported( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """No select entity is created without the motion sensitivity service.""" + await setup_integration(hass, mock_config_entry) + + assert hass.states.get(MOTION_SENSITIVITY_ENTITY_ID) is None