Skip to content

Commit 156aaa6

Browse files
authored
Add support for the Tuya TS0021 2-gang switch (#3247)
1 parent ce63e58 commit 156aaa6

File tree

2 files changed

+208
-0
lines changed

2 files changed

+208
-0
lines changed

tests/test_tuya.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
)
2727
from zhaquirks.tuya import Data, TuyaManufClusterAttributes, TuyaNewManufCluster
2828
import zhaquirks.tuya.sm0202_motion
29+
import zhaquirks.tuya.ts0021
2930
import zhaquirks.tuya.ts0041
3031
import zhaquirks.tuya.ts0042
3132
import zhaquirks.tuya.ts0043
@@ -41,6 +42,12 @@
4142

4243
ZCL_TUYA_SET_TIME_REQUEST = b"\tp\x24\x00\00"
4344

45+
ZCL_TUYA_BUTTON_1_SINGLE_PRESS = b"\tT\x06\x01$\x01\x04\x00\x01\x00"
46+
ZCL_TUYA_BUTTON_1_DOUBLE_PRESS = b"\tU\x06\x01%\x01\x04\x00\x01\x01"
47+
ZCL_TUYA_BUTTON_1_LONG_PRESS = b"\tk\x06\x03\x11\x01\x04\x00\x01\x02"
48+
ZCL_TUYA_BUTTON_2_SINGLE_PRESS = b"\tN\x06\x01\x1f\x02\x04\x00\x01\x00"
49+
ZCL_TUYA_BUTTON_2_DOUBLE_PRESS = b"\tj\x06\x03\x10\x02\x04\x00\x01\x01"
50+
ZCL_TUYA_BUTTON_2_LONG_PRESS = b"\tl\x06\x03\x12\x02\x04\x00\x01\x02"
4451
ZCL_TUYA_MOTION = b"\tL\x01\x00\x05\x03\x04\x00\x01\x02"
4552
ZCL_TUYA_SWITCH_ON = b"\tQ\x02\x006\x01\x01\x00\x01\x01"
4653
ZCL_TUYA_SWITCH_OFF = b"\tQ\x02\x006\x01\x01\x00\x01\x00"
@@ -66,6 +73,7 @@
6673
ZCL_TUYA_VALVE_AUTO_LOCK_ON = b"\t2\x01\x03\x04\x74\x01\x00\x01\x01"
6774
ZCL_TUYA_VALVE_BATTERY_LOW = b"\t2\x01\x03\x04\x6e\x01\x00\x01\x01"
6875

76+
6977
ZCL_TUYA_VALVE_ZONNSMART_TEMPERATURE = (
7078
b"\tp\x01\x00\x02\x18\x02\x00\x04\x00\x00\x00\xd3"
7179
)
@@ -137,6 +145,67 @@ async def test_singleswitch_state_report(zigpy_device_from_quirk, quirk):
137145
assert switch_listener.attribute_updates[1][1] == OFF
138146

139147

148+
@pytest.mark.parametrize(
149+
"quirk,raw_event,expected_attr_name,expected_attr_value",
150+
(
151+
(
152+
zhaquirks.tuya.ts0021.TS0021,
153+
ZCL_TUYA_BUTTON_1_SINGLE_PRESS,
154+
"btn_1_pressed",
155+
0x00,
156+
),
157+
(
158+
zhaquirks.tuya.ts0021.TS0021,
159+
ZCL_TUYA_BUTTON_1_DOUBLE_PRESS,
160+
"btn_1_pressed",
161+
0x01,
162+
),
163+
(
164+
zhaquirks.tuya.ts0021.TS0021,
165+
ZCL_TUYA_BUTTON_1_LONG_PRESS,
166+
"btn_1_pressed",
167+
0x02,
168+
),
169+
(
170+
zhaquirks.tuya.ts0021.TS0021,
171+
ZCL_TUYA_BUTTON_2_SINGLE_PRESS,
172+
"btn_2_pressed",
173+
0x00,
174+
),
175+
(
176+
zhaquirks.tuya.ts0021.TS0021,
177+
ZCL_TUYA_BUTTON_2_DOUBLE_PRESS,
178+
"btn_2_pressed",
179+
0x01,
180+
),
181+
(
182+
zhaquirks.tuya.ts0021.TS0021,
183+
ZCL_TUYA_BUTTON_2_LONG_PRESS,
184+
"btn_2_pressed",
185+
0x02,
186+
),
187+
),
188+
)
189+
async def test_ts0021_switch(
190+
zigpy_device_from_quirk, quirk, raw_event, expected_attr_name, expected_attr_value
191+
):
192+
"""Test tuya TS0021 2-gang switch."""
193+
194+
device = zigpy_device_from_quirk(quirk)
195+
196+
tuya_cluster = device.endpoints[1].tuya_manufacturer
197+
switch_listener = ClusterListener(tuya_cluster)
198+
199+
hdr, args = tuya_cluster.deserialize(raw_event)
200+
tuya_cluster.handle_message(hdr, args)
201+
202+
assert len(switch_listener.cluster_commands) == 1
203+
assert len(switch_listener.attribute_updates) == 1
204+
205+
assert switch_listener.attribute_updates[0][0] == expected_attr_name
206+
assert switch_listener.attribute_updates[0][1] == expected_attr_value
207+
208+
140209
@pytest.mark.parametrize("quirk", (zhaquirks.tuya.ts0601_switch.TuyaDoubleSwitchTO,))
141210
async def test_doubleswitch_state_report(zigpy_device_from_quirk, quirk):
142211
"""Test tuya double switch."""

zhaquirks/tuya/ts0021.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
"""Device handler the TS0021."""
2+
3+
from zigpy.profiles import zha
4+
from zigpy.quirks import CustomDevice
5+
from zigpy.zcl.clusters.general import Basic, Ota, PowerConfiguration, Time
6+
from zigpy.zcl.clusters.security import IasZone
7+
8+
from zhaquirks import EventableCluster
9+
from zhaquirks.const import (
10+
ARGS,
11+
ATTRIBUTE_ID,
12+
CLUSTER_ID,
13+
COMMAND,
14+
COMMAND_ATTRIBUTE_UPDATED,
15+
DEVICE_TYPE,
16+
DOUBLE_PRESS,
17+
ENDPOINT_ID,
18+
ENDPOINTS,
19+
INPUT_CLUSTERS,
20+
LONG_PRESS,
21+
MODELS_INFO,
22+
OUTPUT_CLUSTERS,
23+
PROFILE_ID,
24+
SHORT_PRESS,
25+
VALUE,
26+
)
27+
from zhaquirks.tuya import TUYA_CLUSTER_ID, DPToAttributeMapping, TuyaNewManufCluster
28+
29+
BTN_1 = "Button 1"
30+
BTN_2 = "Button 2"
31+
32+
ATTR_BTN_1_PRESSED = "btn_1_pressed"
33+
ATTR_BTN_2_PRESSED = "btn_2_pressed"
34+
35+
36+
class TuyaCustomCluster(TuyaNewManufCluster, EventableCluster):
37+
"""Tuya Custom Cluster for mapping data points to attributes."""
38+
39+
dp_to_attribute: dict[int, DPToAttributeMapping] = {
40+
1: DPToAttributeMapping(
41+
TuyaNewManufCluster.ep_attribute,
42+
ATTR_BTN_1_PRESSED,
43+
),
44+
2: DPToAttributeMapping(
45+
TuyaNewManufCluster.ep_attribute,
46+
ATTR_BTN_2_PRESSED,
47+
),
48+
}
49+
50+
data_point_handlers = {
51+
1: "_dp_2_attr_update",
52+
2: "_dp_2_attr_update",
53+
}
54+
55+
56+
class TS0021(CustomDevice):
57+
"""Tuya TS0021 2-button switch device."""
58+
59+
signature = {
60+
# SizePrefixedSimpleDescriptor(endpoint=1, profile=260, device_type=1026,
61+
# device_version=1,
62+
# input_clusters=[1, 1280, 61184, 0],
63+
# output_clusters=[25, 10])
64+
MODELS_INFO: [("_TZ3210_3ulg9kpo", "TS0021")],
65+
ENDPOINTS: {
66+
1: {
67+
PROFILE_ID: zha.PROFILE_ID,
68+
DEVICE_TYPE: zha.DeviceType.IAS_ZONE,
69+
INPUT_CLUSTERS: [
70+
Basic.cluster_id,
71+
PowerConfiguration.cluster_id,
72+
IasZone.cluster_id,
73+
TuyaNewManufCluster.cluster_id,
74+
],
75+
OUTPUT_CLUSTERS: [
76+
Time.cluster_id,
77+
Ota.cluster_id,
78+
],
79+
},
80+
},
81+
}
82+
83+
replacement = {
84+
ENDPOINTS: {
85+
1: {
86+
PROFILE_ID: zha.PROFILE_ID,
87+
DEVICE_TYPE: zha.DeviceType.IAS_ZONE,
88+
INPUT_CLUSTERS: [
89+
Basic.cluster_id,
90+
PowerConfiguration.cluster_id,
91+
IasZone.cluster_id,
92+
TuyaCustomCluster,
93+
],
94+
OUTPUT_CLUSTERS: [
95+
Time.cluster_id,
96+
Ota.cluster_id,
97+
],
98+
},
99+
},
100+
}
101+
102+
device_automation_triggers = {
103+
(SHORT_PRESS, BTN_1): {
104+
COMMAND: COMMAND_ATTRIBUTE_UPDATED,
105+
CLUSTER_ID: TUYA_CLUSTER_ID,
106+
ENDPOINT_ID: 1,
107+
ARGS: {ATTRIBUTE_ID: ATTR_BTN_1_PRESSED, VALUE: 0},
108+
},
109+
(DOUBLE_PRESS, BTN_1): {
110+
COMMAND: COMMAND_ATTRIBUTE_UPDATED,
111+
CLUSTER_ID: TUYA_CLUSTER_ID,
112+
ENDPOINT_ID: 1,
113+
ARGS: {ATTRIBUTE_ID: ATTR_BTN_1_PRESSED, VALUE: 1},
114+
},
115+
(LONG_PRESS, BTN_1): {
116+
COMMAND: COMMAND_ATTRIBUTE_UPDATED,
117+
CLUSTER_ID: TUYA_CLUSTER_ID,
118+
ENDPOINT_ID: 1,
119+
ARGS: {ATTRIBUTE_ID: ATTR_BTN_1_PRESSED, VALUE: 2},
120+
},
121+
(SHORT_PRESS, BTN_2): {
122+
COMMAND: COMMAND_ATTRIBUTE_UPDATED,
123+
CLUSTER_ID: TUYA_CLUSTER_ID,
124+
ENDPOINT_ID: 1,
125+
ARGS: {ATTRIBUTE_ID: ATTR_BTN_2_PRESSED, VALUE: 0},
126+
},
127+
(DOUBLE_PRESS, BTN_2): {
128+
COMMAND: COMMAND_ATTRIBUTE_UPDATED,
129+
CLUSTER_ID: TUYA_CLUSTER_ID,
130+
ENDPOINT_ID: 1,
131+
ARGS: {ATTRIBUTE_ID: ATTR_BTN_2_PRESSED, VALUE: 1},
132+
},
133+
(LONG_PRESS, BTN_2): {
134+
COMMAND: COMMAND_ATTRIBUTE_UPDATED,
135+
CLUSTER_ID: TUYA_CLUSTER_ID,
136+
ENDPOINT_ID: 1,
137+
ARGS: {ATTRIBUTE_ID: ATTR_BTN_2_PRESSED, VALUE: 2},
138+
},
139+
}

0 commit comments

Comments
 (0)