"""Implementation of alarm module.""" from __future__ import annotations from typing import TYPE_CHECKING, Annotated, Literal, TypeAlias from ...feature import Feature from ...module import FeatureAttribute from ..smartmodule import SmartModule DURATION_MAX = 10 * 60 VOLUME_INT_TO_STR = { 0: "mute", 1: "low", 2: "normal", 3: "high", } VOLUME_STR_LIST = [v for v in VOLUME_INT_TO_STR.values()] VOLUME_INT_RANGE = (min(VOLUME_INT_TO_STR.keys()), max(VOLUME_INT_TO_STR.keys())) VOLUME_STR_TO_INT = {v: k for k, v in VOLUME_INT_TO_STR.items()} AlarmVolume: TypeAlias = Literal["mute", "low", "normal", "high"] class Alarm(SmartModule): """Implementation of alarm module.""" REQUIRED_COMPONENT = "alarm" def query(self) -> dict: """Query to execute during the update cycle.""" return { "get_alarm_configure": None, "get_support_alarm_type_list": None, # This should be needed only once } def _initialize_features(self) -> None: """Initialize features.""" device = self._device self._add_feature( Feature( device, id="alarm", name="Alarm", container=self, attribute_getter="active", icon="mdi:bell", type=Feature.Type.BinarySensor, ) ) self._add_feature( Feature( device, id="alarm_source", name="Alarm source", container=self, attribute_getter="source", icon="mdi:bell", type=Feature.Type.Sensor, ) ) self._add_feature( Feature( device, id="alarm_sound", name="Alarm sound", container=self, attribute_getter="alarm_sound", attribute_setter="set_alarm_sound", category=Feature.Category.Config, type=Feature.Type.Choice, choices_getter="alarm_sounds", ) ) self._add_feature( Feature( device, id="alarm_volume", name="Alarm volume", container=self, attribute_getter="_alarm_volume_str", attribute_setter="set_alarm_volume", category=Feature.Category.Config, type=Feature.Type.Choice, choices_getter=lambda: VOLUME_STR_LIST, ) ) self._add_feature( Feature( device, id="alarm_volume_level", name="Alarm volume", container=self, attribute_getter="alarm_volume", attribute_setter="set_alarm_volume", category=Feature.Category.Config, type=Feature.Type.Number, range_getter=lambda: VOLUME_INT_RANGE, ) ) self._add_feature( Feature( device, id="alarm_duration", name="Alarm duration", container=self, attribute_getter="alarm_duration", attribute_setter="set_alarm_duration", category=Feature.Category.Config, type=Feature.Type.Number, range_getter=lambda: (1, DURATION_MAX), ) ) self._add_feature( Feature( device, id="test_alarm", name="Test alarm", container=self, attribute_setter="play", type=Feature.Type.Action, ) ) self._add_feature( Feature( device, id="stop_alarm", name="Stop alarm", container=self, attribute_setter="stop", type=Feature.Type.Action, ) ) @property def alarm_sound(self) -> Annotated[str, FeatureAttribute()]: """Return current alarm sound.""" return self.data["get_alarm_configure"]["type"] async def set_alarm_sound(self, sound: str) -> Annotated[dict, FeatureAttribute()]: """Set alarm sound. See *alarm_sounds* for list of available sounds. """ self._check_sound(sound) payload = self.data["get_alarm_configure"].copy() payload["type"] = sound return await self.call("set_alarm_configure", payload) @property def alarm_sounds(self) -> list[str]: """Return list of available alarm sounds.""" return self.data["get_support_alarm_type_list"]["alarm_type_list"] @property def alarm_volume(self) -> Annotated[int, FeatureAttribute("alarm_volume_level")]: """Return alarm volume.""" return VOLUME_STR_TO_INT[self._alarm_volume_str] @property def _alarm_volume_str( self, ) -> Annotated[AlarmVolume, FeatureAttribute("alarm_volume")]: """Return alarm volume.""" return self.data["get_alarm_configure"]["volume"] async def set_alarm_volume( self, volume: AlarmVolume | int ) -> Annotated[dict, FeatureAttribute()]: """Set alarm volume.""" self._check_and_convert_volume(volume) payload = self.data["get_alarm_configure"].copy() payload["volume"] = volume return await self.call("set_alarm_configure", payload) @property def alarm_duration(self) -> Annotated[int, FeatureAttribute()]: """Return alarm duration.""" return self.data["get_alarm_configure"]["duration"] async def set_alarm_duration( self, duration: int ) -> Annotated[dict, FeatureAttribute()]: """Set alarm duration.""" self._check_duration(duration) payload = self.data["get_alarm_configure"].copy() payload["duration"] = duration return await self.call("set_alarm_configure", payload) @property def active(self) -> bool: """Return true if alarm is active.""" return self._device.sys_info["in_alarm"] @property def source(self) -> str | None: """Return the alarm cause.""" src = self._device.sys_info["in_alarm_source"] return src if src else None async def play( self, *, duration: int | None = None, volume: int | AlarmVolume | None = None, sound: str | None = None, ) -> dict: """Play alarm. The optional *duration*, *volume*, and *sound* to override the device settings. *volume* can be set to 'mute', 'low', 'normal', or 'high'. *duration* is in seconds. See *alarm_sounds* for the list of sounds available for the device. """ params: dict[str, str | int] = {} if duration is not None: self._check_duration(duration) params["alarm_duration"] = duration if volume is not None: target_volume = self._check_and_convert_volume(volume) params["alarm_volume"] = target_volume if sound is not None: self._check_sound(sound) params["alarm_type"] = sound return await self.call("play_alarm", params) async def stop(self) -> dict: """Stop alarm.""" return await self.call("stop_alarm") def _check_and_convert_volume(self, volume: str | int) -> str: """Raise an exception on invalid volume.""" if isinstance(volume, int): volume = VOLUME_INT_TO_STR.get(volume, "invalid") if TYPE_CHECKING: assert isinstance(volume, str) if volume not in VOLUME_INT_TO_STR.values(): raise ValueError( f"Invalid volume {volume} " f"available: {VOLUME_INT_TO_STR.keys()}, {VOLUME_INT_TO_STR.values()}" ) return volume def _check_duration(self, duration: int) -> None: """Raise an exception on invalid duration.""" if duration < 1 or duration > DURATION_MAX: raise ValueError(f"Invalid duration {duration} available: 1-600") def _check_sound(self, sound: str) -> None: """Raise an exception on invalid sound.""" if sound not in self.alarm_sounds: raise ValueError(f"Invalid sound {sound} available: {self.alarm_sounds}")