diff --git a/homeassistant/components/tuya/alarm_control_panel.py b/homeassistant/components/tuya/alarm_control_panel.py index 96f7d3a1e1c..4972fe88339 100644 --- a/homeassistant/components/tuya/alarm_control_panel.py +++ b/homeassistant/components/tuya/alarm_control_panel.py @@ -2,6 +2,8 @@ from __future__ import annotations +from base64 import b64decode +from dataclasses import dataclass from enum import StrEnum from tuya_sharing import CustomerDevice, Manager @@ -18,7 +20,15 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from . import TuyaConfigEntry from .const import TUYA_DISCOVERY_NEW, DPCode, DPType -from .entity import TuyaEntity +from .entity import EnumTypeData, TuyaEntity + + +@dataclass(frozen=True) +class TuyaAlarmControlPanelEntityDescription(AlarmControlPanelEntityDescription): + """Describe a Tuya Alarm Control Panel entity.""" + + master_state: DPCode | None = None + alarm_msg: DPCode | None = None class Mode(StrEnum): @@ -30,6 +40,13 @@ class Mode(StrEnum): SOS = "sos" +class State(StrEnum): + """Alarm states.""" + + NORMAL = "normal" + ALARM = "alarm" + + STATE_MAPPING: dict[str, AlarmControlPanelState] = { Mode.DISARMED: AlarmControlPanelState.DISARMED, Mode.ARM: AlarmControlPanelState.ARMED_AWAY, @@ -40,12 +57,14 @@ STATE_MAPPING: dict[str, AlarmControlPanelState] = { # All descriptions can be found here: # https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq -ALARM: dict[str, tuple[AlarmControlPanelEntityDescription, ...]] = { +ALARM: dict[str, tuple[TuyaAlarmControlPanelEntityDescription, ...]] = { # Alarm Host # https://developer.tuya.com/en/docs/iot/categorymal?id=Kaiuz33clqxaf "mal": ( - AlarmControlPanelEntityDescription( + TuyaAlarmControlPanelEntityDescription( key=DPCode.MASTER_MODE, + master_state=DPCode.MASTER_STATE, + alarm_msg=DPCode.ALARM_MSG, name="Alarm", ), ) @@ -86,12 +105,14 @@ class TuyaAlarmEntity(TuyaEntity, AlarmControlPanelEntity): _attr_name = None _attr_code_arm_required = False + _master_state: EnumTypeData | None = None + _alarm_msg_dpcode: DPCode | None = None def __init__( self, device: CustomerDevice, device_manager: Manager, - description: AlarmControlPanelEntityDescription, + description: TuyaAlarmControlPanelEntityDescription, ) -> None: """Init Tuya Alarm.""" super().__init__(device, device_manager) @@ -111,13 +132,39 @@ class TuyaAlarmEntity(TuyaEntity, AlarmControlPanelEntity): if Mode.SOS in supported_modes.range: self._attr_supported_features |= AlarmControlPanelEntityFeature.TRIGGER + # Determine master state + if enum_type := self.find_dpcode( + description.master_state, dptype=DPType.ENUM, prefer_function=True + ): + self._master_state = enum_type + + # Determine alarm message + if dp_code := self.find_dpcode(description.alarm_msg, prefer_function=True): + self._alarm_msg_dpcode = dp_code + @property def alarm_state(self) -> AlarmControlPanelState | None: """Return the state of the device.""" + # When the alarm is triggered, only its 'state' is changing. From 'normal' to 'alarm'. + # The 'mode' doesn't change, and stays as 'arm' or 'home'. + if self._master_state is not None: + if self.device.status.get(self._master_state.dpcode) == State.ALARM: + return AlarmControlPanelState.TRIGGERED + if not (status := self.device.status.get(self.entity_description.key)): return None return STATE_MAPPING.get(status) + @property + def changed_by(self) -> str | None: + """Last change triggered by.""" + if self._master_state is not None and self._alarm_msg_dpcode is not None: + if self.device.status.get(self._master_state.dpcode) == State.ALARM: + encoded_msg = self.device.status.get(self._alarm_msg_dpcode) + if encoded_msg: + return b64decode(encoded_msg).decode("utf-16be") + return None + def alarm_disarm(self, code: str | None = None) -> None: """Send Disarm command.""" self._send_command( diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 508c47443ca..a40468fdc8f 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -102,6 +102,7 @@ class DPCode(StrEnum): ALARM_TIME = "alarm_time" # Alarm time ALARM_VOLUME = "alarm_volume" # Alarm volume ALARM_MESSAGE = "alarm_message" + ALARM_MSG = "alarm_msg" ANGLE_HORIZONTAL = "angle_horizontal" ANGLE_VERTICAL = "angle_vertical" ANION = "anion" # Ionizer unit @@ -226,6 +227,7 @@ class DPCode(StrEnum): LIGHT_MODE = "light_mode" LOCK = "lock" # Lock / Child lock MASTER_MODE = "master_mode" # alarm mode + MASTER_STATE = "master_state" # alarm state MACH_OPERATE = "mach_operate" MANUAL_FEED = "manual_feed" MATERIAL = "material" # Material