mirror of
https://github.com/home-assistant/core.git
synced 2026-10-06 22:38:02 -04:00
bosch_shc: add select platform (Outdoor Siren sound level) (#183117)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
cf6bbf52b6
commit
06aabd6e5e
@@ -20,6 +20,7 @@ PLATFORMS = [
|
||||
Platform.COVER,
|
||||
Platform.EVENT,
|
||||
Platform.NUMBER,
|
||||
Platform.SELECT,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
Platform.VALVE,
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
"""Platform for select integration."""
|
||||
|
||||
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.device import SHCDevice
|
||||
|
||||
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import BoschConfigEntry
|
||||
from .entity import SHCEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class SHCSelectEntityDescription[_DeviceT: SHCDevice](SelectEntityDescription):
|
||||
"""Describes a SHC select entity."""
|
||||
|
||||
current_option_fn: Callable[[_DeviceT, Sequence[str] | None], str | None]
|
||||
select_option_fn: Callable[[_DeviceT, str], Coroutine[Any, Any, None]]
|
||||
|
||||
|
||||
def _siren_current_option(
|
||||
device: SHCOutdoorSiren, options: Sequence[str] | None
|
||||
) -> str | None:
|
||||
"""Read the Outdoor Siren's current sound level (already lowercased)."""
|
||||
try:
|
||||
return str(device.siren.sound_level.name.lower())
|
||||
except AttributeError, ValueError:
|
||||
return None
|
||||
|
||||
|
||||
async def _siren_select_option(device: SHCOutdoorSiren, option: str) -> None:
|
||||
"""Write the Outdoor Siren's sound level."""
|
||||
level = OutdoorSirenService.SoundLevel[option.upper()]
|
||||
await device.siren.async_set_configuration(sound_level=level)
|
||||
|
||||
|
||||
SIREN_SOUND_LEVEL_DESCRIPTION = SHCSelectEntityDescription[SHCOutdoorSiren](
|
||||
key="siren_sound_level",
|
||||
translation_key="siren_sound_level",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
options=["low", "medium", "high"],
|
||||
current_option_fn=_siren_current_option,
|
||||
select_option_fn=_siren_select_option,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: BoschConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the SHC select platform."""
|
||||
session = config_entry.runtime_data
|
||||
shc_info = session.information
|
||||
if TYPE_CHECKING:
|
||||
assert shc_info is not None and shc_info.unique_id is not None
|
||||
|
||||
async_add_entities(
|
||||
SHCSelect(
|
||||
hass=hass,
|
||||
device=siren,
|
||||
parent_id=shc_info.unique_id,
|
||||
entry_id=config_entry.entry_id,
|
||||
description=SIREN_SOUND_LEVEL_DESCRIPTION,
|
||||
)
|
||||
for siren in session.device_helper.outdoor_sirens
|
||||
if siren.siren is not None
|
||||
)
|
||||
|
||||
|
||||
class SHCSelect[_DeviceT: SHCDevice](SHCEntity, SelectEntity):
|
||||
"""Generic SHC select entity, driven by a SHCSelectEntityDescription."""
|
||||
|
||||
entity_description: SHCSelectEntityDescription[_DeviceT]
|
||||
_device: _DeviceT
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
device: _DeviceT,
|
||||
parent_id: str,
|
||||
entry_id: str,
|
||||
description: SHCSelectEntityDescription[_DeviceT],
|
||||
) -> None:
|
||||
"""Initialize the select entity."""
|
||||
self.entity_description = description
|
||||
super().__init__(
|
||||
hass=hass, device=device, parent_id=parent_id, entry_id=entry_id
|
||||
)
|
||||
self._attr_unique_id = f"{device.serial}_{description.key}"
|
||||
|
||||
@property
|
||||
@override
|
||||
def current_option(self) -> str | None:
|
||||
"""Return the current option."""
|
||||
return self.entity_description.current_option_fn(self._device, self.options)
|
||||
|
||||
@override
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Select an option, writing it to the device."""
|
||||
await self.entity_description.select_option_fn(self._device, option)
|
||||
@@ -70,6 +70,16 @@
|
||||
"name": "Pulse length"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"siren_sound_level": {
|
||||
"name": "Siren volume",
|
||||
"state": {
|
||||
"high": "Loud",
|
||||
"low": "Reduced",
|
||||
"medium": "Medium"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"air_quality": {
|
||||
"name": "Air quality"
|
||||
|
||||
@@ -21,6 +21,7 @@ from boschshcpy import (
|
||||
SHCMicromoduleRelay,
|
||||
SHCMotionDetector,
|
||||
SHCMotionDetector2,
|
||||
SHCOutdoorSiren,
|
||||
SHCPresenceSimulationSystem,
|
||||
SHCShutterContact,
|
||||
SHCShutterContact2,
|
||||
@@ -38,6 +39,7 @@ from boschshcpy import (
|
||||
ThermostatService,
|
||||
)
|
||||
from boschshcpy.services_impl import (
|
||||
OutdoorSirenService,
|
||||
PresenceSimulationConfigurationService,
|
||||
ValveTappetService,
|
||||
)
|
||||
@@ -90,6 +92,7 @@ _EMPTY_DEVICE_BUCKETS: dict[str, Any] = {
|
||||
"micromodule_shutter_controls",
|
||||
"motion_detectors",
|
||||
"motion_detectors2",
|
||||
"outdoor_sirens",
|
||||
"roomthermostats",
|
||||
"shutter_contacts",
|
||||
"shutter_contacts2",
|
||||
@@ -208,6 +211,28 @@ def camera_outdoor_gen2_device(
|
||||
return device
|
||||
|
||||
|
||||
def outdoor_siren_device(
|
||||
device_id: str = "hdm:ZigBee:outdoorsiren1",
|
||||
name: str = "Outdoor Siren",
|
||||
sound_level: OutdoorSirenService.SoundLevel = OutdoorSirenService.SoundLevel.MEDIUM,
|
||||
) -> SHCOutdoorSiren:
|
||||
"""Build a minimal device double for the outdoor_sirens bucket."""
|
||||
device = create_autospec(SHCOutdoorSiren, 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 = "OUTDOOR_SIREN"
|
||||
device.device_services = []
|
||||
device.deleted = False
|
||||
device.status = "AVAILABLE"
|
||||
siren_service = create_autospec(OutdoorSirenService, instance=True)
|
||||
siren_service.sound_level = sound_level
|
||||
device.siren = siren_service
|
||||
return device
|
||||
|
||||
|
||||
def shutter_control_device(
|
||||
device_id: str = "hdm:ZigBee:shutter1",
|
||||
name: str = "Shutter",
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
"""Tests for the Bosch SHC select platform."""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from boschshcpy.services_impl import OutdoorSirenService
|
||||
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 tests.common import MockConfigEntry
|
||||
|
||||
SOUND_LEVEL_ENTITY_ID = "select.outdoor_siren_siren_volume"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"device_buckets",
|
||||
[
|
||||
{
|
||||
"outdoor_sirens": [
|
||||
outdoor_siren_device(sound_level=OutdoorSirenService.SoundLevel.MEDIUM)
|
||||
]
|
||||
}
|
||||
],
|
||||
indirect=True,
|
||||
)
|
||||
@pytest.mark.usefixtures("mock_session")
|
||||
async def test_outdoor_siren_sound_level_current_option(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""The Outdoor Siren's current sound level is reported."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
state = hass.states.get(SOUND_LEVEL_ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == "medium"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"device_buckets",
|
||||
[{"outdoor_sirens": [outdoor_siren_device()]}],
|
||||
indirect=True,
|
||||
)
|
||||
@pytest.mark.usefixtures("mock_session")
|
||||
async def test_outdoor_siren_sound_level_select_option(
|
||||
hass: HomeAssistant,
|
||||
mock_session: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Selecting a sound level writes it to the device."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
device = mock_session.device_helper.outdoor_sirens[0]
|
||||
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{ATTR_ENTITY_ID: SOUND_LEVEL_ENTITY_ID, "option": "high"},
|
||||
blocking=True,
|
||||
)
|
||||
device.siren.async_set_configuration.assert_awaited_once_with(
|
||||
sound_level=OutdoorSirenService.SoundLevel.HIGH
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"device_buckets",
|
||||
[{"outdoor_sirens": [outdoor_siren_device()]}],
|
||||
indirect=True,
|
||||
)
|
||||
@pytest.mark.usefixtures("mock_session")
|
||||
async def test_outdoor_siren_no_siren_service(
|
||||
hass: HomeAssistant,
|
||||
mock_session: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""No select entity is created for a siren without the siren service."""
|
||||
mock_session.device_helper.outdoor_sirens[0].siren = None
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert hass.states.get(SOUND_LEVEL_ENTITY_ID) is None
|
||||
Reference in New Issue
Block a user