Skip to content

Commit 350d79c

Browse files
Add Zunzunbee Slate Switch multi button remote (#4180)
Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>
1 parent bf73aa9 commit 350d79c

File tree

4 files changed

+271
-0
lines changed

4 files changed

+271
-0
lines changed
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
"""Tests for ZunZunBee quirks."""
2+
3+
from unittest import mock
4+
5+
import pytest
6+
import zigpy.types as t
7+
from zigpy.zcl.clusters.security import IasZone
8+
9+
import zhaquirks
10+
11+
zhaquirks.setup()
12+
13+
14+
async def test_button_ias(zigpy_device_from_v2_quirk):
15+
"""Test ZunZunBee button remotes."""
16+
device = zigpy_device_from_v2_quirk("zunzunbee", "SSWZ8T")
17+
ias_zone_status_attr_id = IasZone.AttributeDefs.zone_status.id
18+
cluster = device.endpoints[1].ias_zone
19+
listener = mock.MagicMock()
20+
cluster.add_listener(listener)
21+
22+
# Define button press values (hex + long press variants)
23+
button_values = [
24+
0x2,
25+
0x3,
26+
0x4,
27+
0x5,
28+
0x8,
29+
0x9,
30+
0x10,
31+
0x11,
32+
0x20,
33+
0x21,
34+
0x40,
35+
0x41,
36+
0x80,
37+
0x81,
38+
0x100,
39+
0x101,
40+
]
41+
42+
for value in button_values:
43+
cluster.update_attribute(ias_zone_status_attr_id, value)
44+
assert listener.attribute_updated.call_args[0][0] == ias_zone_status_attr_id
45+
assert listener.attribute_updated.call_args[0][1] == value
46+
47+
assert listener.attribute_updated.call_count == len(button_values)
48+
assert listener.zha_send_event.call_count == len(button_values)
49+
50+
51+
@pytest.mark.parametrize(
52+
"message, button, press_type",
53+
[
54+
(
55+
b"\x18\n\n\x02\x00\x19\x02\x00\xfe\xff0\x01",
56+
"button_1",
57+
"remote_button_short_press",
58+
),
59+
(
60+
b"\x18\n\n\x02\x00\x19\x03\x00\xfe\xff0\x01",
61+
"button_1",
62+
"remote_button_long_press",
63+
),
64+
(
65+
b"\x18\n\n\x02\x00\x19\x04\x00\xfe\xff0\x01",
66+
"button_2",
67+
"remote_button_short_press",
68+
),
69+
(
70+
b"\x18\n\n\x02\x00\x19\x05\x00\xfe\xff0\x01",
71+
"button_2",
72+
"remote_button_long_press",
73+
),
74+
(
75+
b"\x18\n\n\x02\x00\x19\x08\x00\xfe\xff0\x01",
76+
"button_3",
77+
"remote_button_short_press",
78+
),
79+
(
80+
b"\x18\n\n\x02\x00\x19\x09\x00\xfe\xff0\x01",
81+
"button_3",
82+
"remote_button_long_press",
83+
),
84+
(
85+
b"\x18\n\n\x02\x00\x19\x10\x00\xfe\xff0\x01",
86+
"button_4",
87+
"remote_button_short_press",
88+
),
89+
(
90+
b"\x18\n\n\x02\x00\x19\x11\x00\xfe\xff0\x01",
91+
"button_4",
92+
"remote_button_long_press",
93+
),
94+
(
95+
b"\x18\n\n\x02\x00\x19\x20\x00\xfe\xff0\x01",
96+
"button_5",
97+
"remote_button_short_press",
98+
),
99+
(
100+
b"\x18\n\n\x02\x00\x19\x21\x00\xfe\xff0\x01",
101+
"button_5",
102+
"remote_button_long_press",
103+
),
104+
(
105+
b"\x18\n\n\x02\x00\x19\x40\x00\xfe\xff0\x01",
106+
"button_6",
107+
"remote_button_short_press",
108+
),
109+
(
110+
b"\x18\n\n\x02\x00\x19\x41\x00\xfe\xff0\x01",
111+
"button_6",
112+
"remote_button_long_press",
113+
),
114+
(
115+
b"\x18\n\n\x02\x00\x19\x80\x00\xfe\xff0\x01",
116+
"button_7",
117+
"remote_button_short_press",
118+
),
119+
(
120+
b"\x18\n\n\x02\x00\x19\x81\x00\xfe\xff0\x01",
121+
"button_7",
122+
"remote_button_long_press",
123+
),
124+
(
125+
b"\x18\n\n\x02\x00\x19\x00\x01\xfe\xff0\x01",
126+
"button_8",
127+
"remote_button_short_press",
128+
),
129+
(
130+
b"\x18\n\n\x02\x00\x19\x01\x01\xfe\xff0\x01",
131+
"button_8",
132+
"remote_button_long_press",
133+
),
134+
],
135+
)
136+
async def test_button_triggers(zigpy_device_from_v2_quirk, message, button, press_type):
137+
"""Test ZHA_SEND_EVENT case."""
138+
device = zigpy_device_from_v2_quirk("zunzunbee", "SSWZ8T")
139+
cluster = device.endpoints[1].ias_zone
140+
listener = mock.MagicMock()
141+
cluster.add_listener(listener)
142+
143+
device.packet_received(
144+
t.ZigbeePacket(
145+
profile_id=260,
146+
cluster_id=cluster.cluster_id,
147+
src_ep=1,
148+
dst_ep=1,
149+
data=t.SerializableBytes(message),
150+
)
151+
)
152+
153+
assert listener.zha_send_event.call_count == 1
154+
assert listener.zha_send_event.call_args == mock.call(
155+
f"{button}_{press_type}",
156+
{"button": button, "press_type": press_type},
157+
)
158+
159+
160+
async def test_discard_invalid_value(zigpy_device_from_v2_quirk):
161+
"""Test that invalid values are discarded without triggering events."""
162+
device = zigpy_device_from_v2_quirk("zunzunbee", "SSWZ8T")
163+
cluster = device.endpoints[1].ias_zone
164+
listener = mock.MagicMock()
165+
cluster.add_listener(listener)
166+
167+
ias_zone_status_attr_id = IasZone.AttributeDefs.zone_status.id
168+
invalid_value = 6 # example invalid value
169+
cluster.update_attribute(ias_zone_status_attr_id, invalid_value)
170+
171+
assert listener.attribute_updated.call_args[0][0] == ias_zone_status_attr_id
172+
assert listener.attribute_updated.call_args[0][1] == invalid_value
173+
174+
listener.zha_send_event.assert_not_called()

zhaquirks/const.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
BUTTON_4 = "button_4"
2626
BUTTON_5 = "button_5"
2727
BUTTON_6 = "button_6"
28+
BUTTON_7 = "button_7"
29+
BUTTON_8 = "button_8"
2830
CLICK_TYPE = "click_type"
2931
CLOSE = "close"
3032
CLUSTER_COMMAND = "cluster_command"

zhaquirks/zunzunbee/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""zunzunbee devices."""
2+
3+
ZUNZUNBEE = "zunzunbee"

zhaquirks/zunzunbee/slateswitch.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
"""ZunZunBee button device."""
2+
3+
from zigpy.quirks import CustomCluster
4+
from zigpy.quirks.v2 import QuirkBuilder
5+
from zigpy.zcl.clusters.security import IasZone
6+
7+
from zhaquirks.const import (
8+
BUTTON,
9+
BUTTON_1,
10+
BUTTON_2,
11+
BUTTON_3,
12+
BUTTON_4,
13+
BUTTON_5,
14+
BUTTON_6,
15+
BUTTON_7,
16+
BUTTON_8,
17+
CLUSTER_ID,
18+
COMMAND,
19+
LONG_PRESS,
20+
PRESS_TYPE,
21+
SHORT_PRESS,
22+
ZHA_SEND_EVENT,
23+
)
24+
from zhaquirks.zunzunbee import ZUNZUNBEE
25+
26+
PRESS_TYPES = {
27+
1: SHORT_PRESS,
28+
2: LONG_PRESS,
29+
}
30+
31+
BUTTON_MAPPING = {
32+
2: BUTTON_1,
33+
4: BUTTON_2,
34+
8: BUTTON_3,
35+
16: BUTTON_4,
36+
32: BUTTON_5,
37+
64: BUTTON_6,
38+
128: BUTTON_7,
39+
256: BUTTON_8,
40+
}
41+
42+
43+
class ZunZunBeeIASCluster(CustomCluster, IasZone):
44+
"""IAS cluster used for ZunZunBee button."""
45+
46+
def _update_attribute(self, attrid, value):
47+
super()._update_attribute(attrid, value)
48+
if attrid == self.AttributeDefs.zone_status.id:
49+
# Ignore first bit for determining button id
50+
button_id = value & 0x01FE
51+
52+
# Map to button presses, ignore invalid buttons
53+
button = BUTTON_MAPPING.get(button_id)
54+
if button is None:
55+
return
56+
57+
# Only check first bit for press type
58+
press_id = (value & 1) + 1
59+
press_type = PRESS_TYPES[press_id]
60+
61+
action = f"{button}_{press_type}"
62+
event_args = {
63+
BUTTON: button,
64+
PRESS_TYPE: press_type,
65+
}
66+
self.listener_event(ZHA_SEND_EVENT, action, event_args)
67+
68+
69+
(
70+
QuirkBuilder(ZUNZUNBEE, "SSWZ8T")
71+
.replaces(ZunZunBeeIASCluster)
72+
.device_automation_triggers(
73+
{
74+
(press_type, button): {
75+
COMMAND: f"{button}_{press_type}",
76+
CLUSTER_ID: IasZone.cluster_id,
77+
}
78+
for press_type in (SHORT_PRESS, LONG_PRESS)
79+
for button in (
80+
BUTTON_1,
81+
BUTTON_2,
82+
BUTTON_3,
83+
BUTTON_4,
84+
BUTTON_5,
85+
BUTTON_6,
86+
BUTTON_7,
87+
BUTTON_8,
88+
)
89+
}
90+
)
91+
.add_to_registry()
92+
)

0 commit comments

Comments
 (0)