Skip to content

Commit 0ed342b

Browse files
authored
Use dpcode_wrapper in tuya alarm control panel platform (home-assistant#156306)
1 parent 363c86f commit 0ed342b

File tree

3 files changed

+89
-42
lines changed

3 files changed

+89
-42
lines changed

homeassistant/components/tuya/alarm_control_panel.py

Lines changed: 39 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@
1919
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
2020

2121
from . import TuyaConfigEntry
22-
from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode, DPType
22+
from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode
2323
from .entity import TuyaEntity
24-
from .models import EnumTypeData, find_dpcode
24+
from .models import DPCodeEnumWrapper
2525
from .util import get_dpcode
2626

2727

@@ -85,9 +85,21 @@ def async_discover_device(device_ids: list[str]) -> None:
8585
device = manager.device_map[device_id]
8686
if descriptions := ALARM.get(device.category):
8787
entities.extend(
88-
TuyaAlarmEntity(device, manager, description)
88+
TuyaAlarmEntity(
89+
device,
90+
manager,
91+
description,
92+
action_dpcode_wrapper=action_dpcode_wrapper,
93+
state_dpcode_wrapper=DPCodeEnumWrapper.find_dpcode(
94+
device, description.master_state
95+
),
96+
)
8997
for description in descriptions
90-
if description.key in device.status
98+
if (
99+
action_dpcode_wrapper := DPCodeEnumWrapper.find_dpcode(
100+
device, description.key, prefer_function=True
101+
)
102+
)
91103
)
92104
async_add_entities(entities)
93105

@@ -103,41 +115,31 @@ class TuyaAlarmEntity(TuyaEntity, AlarmControlPanelEntity):
103115

104116
_attr_name = None
105117
_attr_code_arm_required = False
106-
_master_state: EnumTypeData | None = None
107118
_alarm_msg_dpcode: DPCode | None = None
108119

109120
def __init__(
110121
self,
111122
device: CustomerDevice,
112123
device_manager: Manager,
113124
description: TuyaAlarmControlPanelEntityDescription,
125+
*,
126+
action_dpcode_wrapper: DPCodeEnumWrapper,
127+
state_dpcode_wrapper: DPCodeEnumWrapper | None,
114128
) -> None:
115129
"""Init Tuya Alarm."""
116130
super().__init__(device, device_manager)
117131
self.entity_description = description
118132
self._attr_unique_id = f"{super().unique_id}{description.key}"
133+
self._action_dpcode_wrapper = action_dpcode_wrapper
134+
self._state_dpcode_wrapper = state_dpcode_wrapper
119135

120136
# Determine supported modes
121-
if supported_modes := find_dpcode(
122-
self.device, description.key, dptype=DPType.ENUM, prefer_function=True
123-
):
124-
if Mode.HOME in supported_modes.range:
125-
self._attr_supported_features |= AlarmControlPanelEntityFeature.ARM_HOME
126-
127-
if Mode.ARM in supported_modes.range:
128-
self._attr_supported_features |= AlarmControlPanelEntityFeature.ARM_AWAY
129-
130-
if Mode.SOS in supported_modes.range:
131-
self._attr_supported_features |= AlarmControlPanelEntityFeature.TRIGGER
132-
133-
# Determine master state
134-
if enum_type := find_dpcode(
135-
self.device,
136-
description.master_state,
137-
dptype=DPType.ENUM,
138-
prefer_function=True,
139-
):
140-
self._master_state = enum_type
137+
if Mode.HOME in action_dpcode_wrapper.type_information.range:
138+
self._attr_supported_features |= AlarmControlPanelEntityFeature.ARM_HOME
139+
if Mode.ARM in action_dpcode_wrapper.type_information.range:
140+
self._attr_supported_features |= AlarmControlPanelEntityFeature.ARM_AWAY
141+
if Mode.SOS in action_dpcode_wrapper.type_information.range:
142+
self._attr_supported_features |= AlarmControlPanelEntityFeature.TRIGGER
141143

142144
# Determine alarm message
143145
if dp_code := get_dpcode(self.device, description.alarm_msg):
@@ -149,8 +151,8 @@ def alarm_state(self) -> AlarmControlPanelState | None:
149151
# When the alarm is triggered, only its 'state' is changing. From 'normal' to 'alarm'.
150152
# The 'mode' doesn't change, and stays as 'arm' or 'home'.
151153
if (
152-
self._master_state is not None
153-
and self.device.status.get(self._master_state.dpcode) == State.ALARM
154+
self._state_dpcode_wrapper is not None
155+
and self.device.status.get(self._state_dpcode_wrapper.dpcode) == State.ALARM
154156
):
155157
# Only report as triggered if NOT a battery warning
156158
if (
@@ -166,28 +168,26 @@ def alarm_state(self) -> AlarmControlPanelState | None:
166168
def changed_by(self) -> str | None:
167169
"""Last change triggered by."""
168170
if (
169-
self._master_state is not None
171+
self._state_dpcode_wrapper is not None
170172
and self._alarm_msg_dpcode is not None
171-
and self.device.status.get(self._master_state.dpcode) == State.ALARM
173+
and self.device.status.get(self._state_dpcode_wrapper.dpcode) == State.ALARM
172174
and (encoded_msg := self.device.status.get(self._alarm_msg_dpcode))
173175
):
174176
return b64decode(encoded_msg).decode("utf-16be")
175177
return None
176178

177-
def alarm_disarm(self, code: str | None = None) -> None:
179+
async def async_alarm_disarm(self, code: str | None = None) -> None:
178180
"""Send Disarm command."""
179-
self._send_command(
180-
[{"code": self.entity_description.key, "value": Mode.DISARMED}]
181-
)
181+
await self._async_send_dpcode_update(self._action_dpcode_wrapper, Mode.DISARMED)
182182

183-
def alarm_arm_home(self, code: str | None = None) -> None:
183+
async def async_alarm_arm_home(self, code: str | None = None) -> None:
184184
"""Send Home command."""
185-
self._send_command([{"code": self.entity_description.key, "value": Mode.HOME}])
185+
await self._async_send_dpcode_update(self._action_dpcode_wrapper, Mode.HOME)
186186

187-
def alarm_arm_away(self, code: str | None = None) -> None:
187+
async def async_alarm_arm_away(self, code: str | None = None) -> None:
188188
"""Send Arm command."""
189-
self._send_command([{"code": self.entity_description.key, "value": Mode.ARM}])
189+
await self._async_send_dpcode_update(self._action_dpcode_wrapper, Mode.ARM)
190190

191-
def alarm_trigger(self, code: str | None = None) -> None:
191+
async def async_alarm_trigger(self, code: str | None = None) -> None:
192192
"""Send SOS command."""
193-
self._send_command([{"code": self.entity_description.key, "value": Mode.SOS}])
193+
await self._async_send_dpcode_update(self._action_dpcode_wrapper, Mode.SOS)

homeassistant/components/tuya/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ def __init__(self, dpcode: str, type_information: T) -> None:
196196
def find_dpcode(
197197
cls,
198198
device: CustomerDevice,
199-
dpcodes: str | DPCode | tuple[DPCode, ...],
199+
dpcodes: str | DPCode | tuple[DPCode, ...] | None,
200200
*,
201201
prefer_function: bool = False,
202202
) -> Self | None:

tests/components/tuya/test_alarm_control_panel.py

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,22 @@
22

33
from __future__ import annotations
44

5+
from typing import Any
56
from unittest.mock import patch
67

78
import pytest
89
from syrupy.assertion import SnapshotAssertion
910
from tuya_sharing import CustomerDevice, Manager
1011

11-
from homeassistant.components.alarm_control_panel import AlarmControlPanelState
12-
from homeassistant.const import Platform
12+
from homeassistant.components.alarm_control_panel import (
13+
DOMAIN as ALARM_DOMAIN,
14+
SERVICE_ALARM_ARM_AWAY,
15+
SERVICE_ALARM_ARM_HOME,
16+
SERVICE_ALARM_DISARM,
17+
SERVICE_ALARM_TRIGGER,
18+
AlarmControlPanelState,
19+
)
20+
from homeassistant.const import ATTR_ENTITY_ID, Platform
1321
from homeassistant.core import HomeAssistant
1422
from homeassistant.helpers import entity_registry as er
1523

@@ -33,6 +41,45 @@ async def test_platform_setup_and_discovery(
3341
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
3442

3543

44+
@patch("homeassistant.components.tuya.PLATFORMS", [Platform.ALARM_CONTROL_PANEL])
45+
@pytest.mark.parametrize(
46+
"mock_device_code",
47+
["mal_gyitctrjj1kefxp2"],
48+
)
49+
@pytest.mark.parametrize(
50+
("service", "command"),
51+
[
52+
(SERVICE_ALARM_ARM_AWAY, {"code": "master_mode", "value": "arm"}),
53+
(SERVICE_ALARM_ARM_HOME, {"code": "master_mode", "value": "home"}),
54+
(SERVICE_ALARM_DISARM, {"code": "master_mode", "value": "disarmed"}),
55+
(SERVICE_ALARM_TRIGGER, {"code": "master_mode", "value": "sos"}),
56+
],
57+
)
58+
async def test_service(
59+
hass: HomeAssistant,
60+
mock_manager: Manager,
61+
mock_config_entry: MockConfigEntry,
62+
mock_device: CustomerDevice,
63+
service: str,
64+
command: dict[str, Any],
65+
) -> None:
66+
"""Test service."""
67+
entity_id = "alarm_control_panel.multifunction_alarm"
68+
await initialize_entry(hass, mock_manager, mock_config_entry, mock_device)
69+
70+
state = hass.states.get(entity_id)
71+
assert state is not None, f"{entity_id} does not exist"
72+
await hass.services.async_call(
73+
ALARM_DOMAIN,
74+
service,
75+
{
76+
ATTR_ENTITY_ID: entity_id,
77+
},
78+
blocking=True,
79+
)
80+
mock_manager.send_commands.assert_called_once_with(mock_device.id, [command])
81+
82+
3683
@pytest.mark.parametrize(
3784
"mock_device_code",
3885
["mal_gyitctrjj1kefxp2"],

0 commit comments

Comments
 (0)