bosch_shc: add Motion Detector II motion sensitivity select (#183732)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Thomas
2026-10-04 09:06:37 +02:00
committed by GitHub
co-authored by Claude Sonnet 5.5
parent 6a686b9105
commit 534202fe0a
4 changed files with 124 additions and 3 deletions
+38 -1
View File
@@ -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."""
@@ -71,6 +71,14 @@
}
},
"select": {
"motion_sensitivity": {
"name": "Motion sensitivity",
"state": {
"high": "High",
"low": "Low",
"middle": "Medium"
}
},
"siren_sound_level": {
"name": "Siren volume",
"state": {
+7
View File
@@ -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
+71 -2
View File
@@ -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