Skip to content

Commit f50a2de

Browse files
authored
Send all button press events for Sinope SW2500ZB/DM2500ZB (#3438)
1 parent de3643c commit f50a2de

File tree

3 files changed

+183
-59
lines changed

3 files changed

+183
-59
lines changed

tests/test_sinope.py

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,14 @@
1111

1212
from tests.common import ClusterListener
1313
import zhaquirks
14-
from zhaquirks.const import COMMAND_BUTTON_DOUBLE, COMMAND_BUTTON_HOLD
14+
from zhaquirks.const import (
15+
COMMAND_M_INITIAL_PRESS,
16+
COMMAND_M_LONG_RELEASE,
17+
COMMAND_M_MULTI_PRESS_COMPLETE,
18+
COMMAND_M_SHORT_RELEASE,
19+
TURN_OFF,
20+
TURN_ON,
21+
)
1522
from zhaquirks.sinope import SINOPE_MANUFACTURER_CLUSTER_ID
1623
from zhaquirks.sinope.light import (
1724
SinopeTechnologieslight,
@@ -92,20 +99,22 @@ def _get_packet_data(
9299

93100
@pytest.mark.parametrize("quirk", (SinopeTechnologieslight,))
94101
@pytest.mark.parametrize(
95-
"press_type,exp_event",
102+
"press_type,button,exp_event",
96103
(
97-
(ButtonAction.Single_off, None),
98-
(ButtonAction.Single_on, None),
99-
(ButtonAction.Double_on, COMMAND_BUTTON_DOUBLE),
100-
(ButtonAction.Double_off, COMMAND_BUTTON_DOUBLE),
101-
(ButtonAction.Long_on, COMMAND_BUTTON_HOLD),
102-
(ButtonAction.Long_off, COMMAND_BUTTON_HOLD),
104+
(ButtonAction.Pressed_off, TURN_OFF, COMMAND_M_INITIAL_PRESS),
105+
(ButtonAction.Pressed_on, TURN_ON, COMMAND_M_INITIAL_PRESS),
106+
(ButtonAction.Released_off, TURN_OFF, COMMAND_M_SHORT_RELEASE),
107+
(ButtonAction.Released_on, TURN_ON, COMMAND_M_SHORT_RELEASE),
108+
(ButtonAction.Double_on, TURN_ON, COMMAND_M_MULTI_PRESS_COMPLETE),
109+
(ButtonAction.Double_off, TURN_OFF, COMMAND_M_MULTI_PRESS_COMPLETE),
110+
(ButtonAction.Long_on, TURN_ON, COMMAND_M_LONG_RELEASE),
111+
(ButtonAction.Long_off, TURN_OFF, COMMAND_M_LONG_RELEASE),
103112
# Should gracefully handle broken actions.
104-
(t.uint8_t(0x00), None),
113+
(t.uint8_t(0x00), None, None),
105114
),
106115
)
107116
async def test_sinope_light_switch(
108-
zigpy_device_from_quirk, quirk, press_type, exp_event
117+
zigpy_device_from_quirk, quirk, press_type, button, exp_event
109118
):
110119
"""Test that button presses are sent as events."""
111120
device: Device = zigpy_device_from_quirk(quirk)
@@ -126,7 +135,16 @@ class Listener:
126135
),
127136
)
128137
data = _get_packet_data(foundation.GeneralCommand.Report_Attributes, attr)
129-
device.handle_message(260, cluster_id, endpoint_id, endpoint_id, data)
138+
139+
device.packet_received(
140+
t.ZigbeePacket(
141+
profile_id=260,
142+
cluster_id=cluster_id,
143+
src_ep=endpoint_id,
144+
dst_ep=endpoint_id,
145+
data=t.SerializableBytes(data),
146+
)
147+
)
130148

131149
if exp_event is None:
132150
assert cluster_listener.zha_send_event.call_count == 0
@@ -137,6 +155,8 @@ class Listener:
137155
{
138156
"attribute_id": 84,
139157
"attribute_name": "action_report",
158+
"button": button,
159+
"description": press_type.name,
140160
"value": press_type.value,
141161
},
142162
)
@@ -162,7 +182,15 @@ class Listener:
162182

163183
# read attributes general command
164184
data = _get_packet_data(foundation.GeneralCommand.Read_Attributes)
165-
device.handle_message(260, cluster_id, endpoint_id, endpoint_id, data)
185+
device.packet_received(
186+
t.ZigbeePacket(
187+
profile_id=260,
188+
cluster_id=cluster_id,
189+
src_ep=endpoint_id,
190+
dst_ep=endpoint_id,
191+
data=t.SerializableBytes(data),
192+
)
193+
)
166194
# no ZHA events emitted because we only handle Report_Attributes
167195
assert cluster_listener.zha_send_event.call_count == 0
168196

@@ -174,7 +202,15 @@ class Listener:
174202
), # 0x29 = t.int16s
175203
)
176204
data = _get_packet_data(foundation.GeneralCommand.Report_Attributes, attr)
177-
device.handle_message(260, cluster_id, endpoint_id, endpoint_id, data)
205+
device.packet_received(
206+
t.ZigbeePacket(
207+
profile_id=260,
208+
cluster_id=cluster_id,
209+
src_ep=endpoint_id,
210+
dst_ep=endpoint_id,
211+
data=t.SerializableBytes(data),
212+
)
213+
)
178214
# ZHA event emitted because we pass non "action_report"
179215
# reports to the base class handler.
180216
assert cluster_listener.zha_send_event.call_count == 1

zhaquirks/sinope/__init__.py

Lines changed: 85 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
11
"""Module for Sinope quirks implementations."""
22

33
from zigpy.quirks import CustomCluster
4+
import zigpy.types as t
45
from zigpy.zcl.clusters.general import DeviceTemperature
56

67
from zhaquirks.const import (
78
ARGS,
89
ATTRIBUTE_ID,
910
ATTRIBUTE_NAME,
11+
BUTTON,
1012
CLUSTER_ID,
1113
COMMAND,
12-
COMMAND_BUTTON_DOUBLE,
13-
COMMAND_BUTTON_HOLD,
14-
COMMAND_BUTTON_SINGLE,
14+
COMMAND_M_INITIAL_PRESS,
15+
COMMAND_M_LONG_RELEASE,
16+
COMMAND_M_MULTI_PRESS_COMPLETE,
17+
COMMAND_M_SHORT_RELEASE,
1518
DOUBLE_PRESS,
1619
ENDPOINT_ID,
1720
LONG_PRESS,
1821
SHORT_PRESS,
22+
SHORT_RELEASE,
1923
TURN_OFF,
2024
TURN_ON,
2125
VALUE,
@@ -25,42 +29,108 @@
2529
SINOPE_MANUFACTURER_CLUSTER_ID = 0xFF01
2630
ATTRIBUTE_ACTION = "action_report"
2731

32+
33+
class ButtonAction(t.enum8):
34+
"""Action_report values."""
35+
36+
Pressed_on = 0x01
37+
Released_on = 0x02
38+
Long_on = 0x03
39+
Double_on = 0x04
40+
Pressed_off = 0x11
41+
Released_off = 0x12
42+
Long_off = 0x13
43+
Double_off = 0x14
44+
45+
2846
LIGHT_DEVICE_TRIGGERS = {
2947
(SHORT_PRESS, TURN_ON): {
3048
ENDPOINT_ID: 1,
3149
CLUSTER_ID: 65281,
32-
COMMAND: COMMAND_BUTTON_SINGLE,
33-
ARGS: {ATTRIBUTE_ID: 84, ATTRIBUTE_NAME: ATTRIBUTE_ACTION, VALUE: 2},
50+
COMMAND: COMMAND_M_INITIAL_PRESS,
51+
ARGS: {
52+
ATTRIBUTE_ID: 84,
53+
ATTRIBUTE_NAME: ATTRIBUTE_ACTION,
54+
BUTTON: TURN_ON,
55+
VALUE: ButtonAction.Pressed_on,
56+
},
3457
},
3558
(SHORT_PRESS, TURN_OFF): {
3659
ENDPOINT_ID: 1,
3760
CLUSTER_ID: 65281,
38-
COMMAND: COMMAND_BUTTON_SINGLE,
39-
ARGS: {ATTRIBUTE_ID: 84, ATTRIBUTE_NAME: ATTRIBUTE_ACTION, VALUE: 18},
61+
COMMAND: COMMAND_M_INITIAL_PRESS,
62+
ARGS: {
63+
ATTRIBUTE_ID: 84,
64+
ATTRIBUTE_NAME: ATTRIBUTE_ACTION,
65+
BUTTON: TURN_OFF,
66+
VALUE: ButtonAction.Pressed_off,
67+
},
68+
},
69+
(SHORT_RELEASE, TURN_ON): {
70+
ENDPOINT_ID: 1,
71+
CLUSTER_ID: 65281,
72+
COMMAND: COMMAND_M_SHORT_RELEASE,
73+
ARGS: {
74+
ATTRIBUTE_ID: 84,
75+
ATTRIBUTE_NAME: ATTRIBUTE_ACTION,
76+
BUTTON: TURN_ON,
77+
VALUE: ButtonAction.Released_on,
78+
},
79+
},
80+
(SHORT_RELEASE, TURN_OFF): {
81+
ENDPOINT_ID: 1,
82+
CLUSTER_ID: 65281,
83+
COMMAND: COMMAND_M_SHORT_RELEASE,
84+
ARGS: {
85+
ATTRIBUTE_ID: 84,
86+
ATTRIBUTE_NAME: ATTRIBUTE_ACTION,
87+
BUTTON: TURN_OFF,
88+
VALUE: ButtonAction.Released_off,
89+
},
4090
},
4191
(DOUBLE_PRESS, TURN_ON): {
4292
ENDPOINT_ID: 1,
4393
CLUSTER_ID: 65281,
44-
COMMAND: COMMAND_BUTTON_DOUBLE,
45-
ARGS: {ATTRIBUTE_ID: 84, ATTRIBUTE_NAME: ATTRIBUTE_ACTION, VALUE: 4},
94+
COMMAND: COMMAND_M_MULTI_PRESS_COMPLETE,
95+
ARGS: {
96+
ATTRIBUTE_ID: 84,
97+
ATTRIBUTE_NAME: ATTRIBUTE_ACTION,
98+
BUTTON: TURN_ON,
99+
VALUE: ButtonAction.Double_on,
100+
},
46101
},
47102
(DOUBLE_PRESS, TURN_OFF): {
48103
ENDPOINT_ID: 1,
49104
CLUSTER_ID: 65281,
50-
COMMAND: COMMAND_BUTTON_DOUBLE,
51-
ARGS: {ATTRIBUTE_ID: 84, ATTRIBUTE_NAME: ATTRIBUTE_ACTION, VALUE: 20},
105+
COMMAND: COMMAND_M_MULTI_PRESS_COMPLETE,
106+
ARGS: {
107+
ATTRIBUTE_ID: 84,
108+
ATTRIBUTE_NAME: ATTRIBUTE_ACTION,
109+
BUTTON: TURN_OFF,
110+
VALUE: ButtonAction.Double_off,
111+
},
52112
},
53113
(LONG_PRESS, TURN_ON): {
54114
ENDPOINT_ID: 1,
55115
CLUSTER_ID: 65281,
56-
COMMAND: COMMAND_BUTTON_HOLD,
57-
ARGS: {ATTRIBUTE_ID: 84, ATTRIBUTE_NAME: ATTRIBUTE_ACTION, VALUE: 3},
116+
COMMAND: COMMAND_M_LONG_RELEASE,
117+
ARGS: {
118+
ATTRIBUTE_ID: 84,
119+
ATTRIBUTE_NAME: ATTRIBUTE_ACTION,
120+
BUTTON: TURN_ON,
121+
VALUE: ButtonAction.Long_on,
122+
},
58123
},
59124
(LONG_PRESS, TURN_OFF): {
60125
ENDPOINT_ID: 1,
61126
CLUSTER_ID: 65281,
62-
COMMAND: COMMAND_BUTTON_HOLD,
63-
ARGS: {ATTRIBUTE_ID: 84, ATTRIBUTE_NAME: ATTRIBUTE_ACTION, VALUE: 19},
127+
COMMAND: COMMAND_M_LONG_RELEASE,
128+
ARGS: {
129+
ATTRIBUTE_ID: 84,
130+
ATTRIBUTE_NAME: ATTRIBUTE_ACTION,
131+
BUTTON: TURN_OFF,
132+
VALUE: ButtonAction.Long_off,
133+
},
64134
},
65135
}
66136

zhaquirks/sinope/light.py

Lines changed: 49 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,20 @@
2929
from zhaquirks.const import (
3030
ATTRIBUTE_ID,
3131
ATTRIBUTE_NAME,
32-
COMMAND_BUTTON_DOUBLE,
33-
COMMAND_BUTTON_HOLD,
32+
BUTTON,
33+
COMMAND_M_INITIAL_PRESS,
34+
COMMAND_M_LONG_RELEASE,
35+
COMMAND_M_MULTI_PRESS_COMPLETE,
36+
COMMAND_M_SHORT_RELEASE,
37+
DESCRIPTION,
3438
DEVICE_TYPE,
3539
ENDPOINTS,
3640
INPUT_CLUSTERS,
3741
MODELS_INFO,
3842
OUTPUT_CLUSTERS,
3943
PROFILE_ID,
44+
TURN_OFF,
45+
TURN_ON,
4046
VALUE,
4147
ZHA_SEND_EVENT,
4248
)
@@ -45,6 +51,7 @@
4551
LIGHT_DEVICE_TRIGGERS,
4652
SINOPE,
4753
SINOPE_MANUFACTURER_CLUSTER_ID,
54+
ButtonAction,
4855
CustomDeviceTemperatureCluster,
4956
)
5057

@@ -73,19 +80,6 @@ class DoubleFull(t.enum8):
7380
On = 0x01
7481

7582

76-
class ButtonAction(t.enum8):
77-
"""Action_report values."""
78-
79-
Single_on = 0x01
80-
Single_release_on = 0x02
81-
Long_on = 0x03
82-
Double_on = 0x04
83-
Single_off = 0x11
84-
Single_release_off = 0x12
85-
Long_off = 0x13
86-
Double_off = 0x14
87-
88-
8983
class SinopeTechnologiesManufacturerCluster(CustomCluster):
9084
"""SinopeTechnologiesManufacturerCluster manufacturer cluster."""
9185

@@ -191,29 +185,53 @@ def handle_cluster_general_request(
191185
hdr, args, dst_addressing=dst_addressing
192186
)
193187

194-
value = attr.value.value
188+
action = self.Action(attr.value.value)
189+
190+
command, button = self._get_command_from_action(action)
191+
if not command or not button:
192+
return
193+
195194
event_args = {
196195
ATTRIBUTE_ID: 84,
197196
ATTRIBUTE_NAME: ATTRIBUTE_ACTION,
198-
VALUE: value.value,
197+
BUTTON: button,
198+
DESCRIPTION: action.name,
199+
VALUE: action.value,
199200
}
200-
action = self._get_command_from_action(self.Action(value))
201-
if not action:
202-
return
203-
self.listener_event(ZHA_SEND_EVENT, action, event_args)
204201

205-
def _get_command_from_action(self, action: ButtonAction) -> str | None:
206-
# const lookup = {2: 'up_single', 3: 'up_hold', 4: 'up_double',
207-
# 18: 'down_single', 19: 'down_hold', 20: 'down_double'};
202+
self.debug(
203+
"SINOPE ZHA_SEND_EVENT command: '%s' event_args: %s",
204+
command,
205+
event_args,
206+
)
207+
208+
self.listener_event(ZHA_SEND_EVENT, command, event_args)
209+
210+
def _get_command_from_action(
211+
self, action: ButtonAction
212+
) -> tuple[str | None, str | None]:
213+
# const lookup = {1: 'up_single', 2: 'up_single_released', 3: 'up_hold', 4: 'up_double',
214+
# 17: 'down_single, 18: 'down_single_released', 19: 'down_hold', 20: 'down_double'};
208215
match action:
209-
case self.Action.Single_off | self.Action.Single_on:
210-
return None
211-
case self.Action.Double_off | self.Action.Double_on:
212-
return COMMAND_BUTTON_DOUBLE
213-
case self.Action.Long_off | self.Action.Long_on:
214-
return COMMAND_BUTTON_HOLD
216+
case self.Action.Pressed_off:
217+
return COMMAND_M_INITIAL_PRESS, TURN_OFF
218+
case self.Action.Pressed_on:
219+
return COMMAND_M_INITIAL_PRESS, TURN_ON
220+
case self.Action.Released_off:
221+
return COMMAND_M_SHORT_RELEASE, TURN_OFF
222+
case self.Action.Released_on:
223+
return COMMAND_M_SHORT_RELEASE, TURN_ON
224+
case self.Action.Double_off:
225+
return COMMAND_M_MULTI_PRESS_COMPLETE, TURN_OFF
226+
case self.Action.Double_on:
227+
return COMMAND_M_MULTI_PRESS_COMPLETE, TURN_ON
228+
case self.Action.Long_off:
229+
return COMMAND_M_LONG_RELEASE, TURN_OFF
230+
case self.Action.Long_on:
231+
return COMMAND_M_LONG_RELEASE, TURN_ON
215232
case _:
216-
return None
233+
self.debug("SINOPE unhandled action: %s", action)
234+
return None, None
217235

218236

219237
class LightManufacturerCluster(EventableCluster, SinopeTechnologiesManufacturerCluster):

0 commit comments

Comments
 (0)