Skip to content

Commit 2bdd076

Browse files
ckm2k1TheJulianJES
andauthored
Convert Sinope light to new style AttributeDefs, add ZHA events (#3313)
Co-authored-by: TheJulianJES <[email protected]>
1 parent e749877 commit 2bdd076

File tree

3 files changed

+322
-57
lines changed

3 files changed

+322
-57
lines changed

tests/test_sinope.py

Lines changed: 149 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,32 @@
11
"""Tests for Sinope."""
22

3+
from unittest import mock
4+
35
import pytest
6+
from zigpy.device import Device
7+
import zigpy.types as t
8+
from zigpy.zcl import foundation
49
from zigpy.zcl.clusters.general import DeviceTemperature
510
from zigpy.zcl.clusters.measurement import FlowMeasurement
611

712
from tests.common import ClusterListener
8-
import zhaquirks.sinope.switch
13+
import zhaquirks
14+
from zhaquirks.const import COMMAND_BUTTON_DOUBLE, COMMAND_BUTTON_HOLD
15+
from zhaquirks.sinope import SINOPE_MANUFACTURER_CLUSTER_ID
16+
from zhaquirks.sinope.light import (
17+
SinopeTechnologieslight,
18+
SinopeTechnologiesManufacturerCluster,
19+
)
20+
from zhaquirks.sinope.switch import SinopeTechnologiesCalypso, SinopeTechnologiesValveG2
921

1022
zhaquirks.setup()
1123

24+
ButtonAction = SinopeTechnologiesManufacturerCluster.Action
25+
26+
SINOPE_MANUFACTURER_ID = 4508 # 0x119C
1227

13-
@pytest.mark.parametrize("quirk", (zhaquirks.sinope.switch.SinopeTechnologiesCalypso,))
28+
29+
@pytest.mark.parametrize("quirk", (SinopeTechnologiesCalypso,))
1430
async def test_sinope_device_temp(zigpy_device_from_quirk, quirk):
1531
"""Test that device temperature is multiplied."""
1632
device = zigpy_device_from_quirk(quirk)
@@ -33,7 +49,7 @@ async def test_sinope_device_temp(zigpy_device_from_quirk, quirk):
3349
assert dev_temp_listener.attribute_updates[1][1] == 25 # not modified
3450

3551

36-
@pytest.mark.parametrize("quirk", (zhaquirks.sinope.switch.SinopeTechnologiesValveG2,))
52+
@pytest.mark.parametrize("quirk", (SinopeTechnologiesValveG2,))
3753
async def test_sinope_flow_measurement(zigpy_device_from_quirk, quirk):
3854
"""Test that flow measurement measured value is divided."""
3955
device = zigpy_device_from_quirk(quirk)
@@ -57,3 +73,133 @@ async def test_sinope_flow_measurement(zigpy_device_from_quirk, quirk):
5773
== flow_measurement_other_attr_id
5874
)
5975
assert flow_measurement_listener.attribute_updates[1][1] == 25 # not modified
76+
77+
78+
def _get_packet_data(
79+
command: foundation.GeneralCommand,
80+
attr: foundation.Attribute | None = None,
81+
dirc: foundation.Direction = foundation.Direction.Server_to_Client,
82+
) -> bytes:
83+
hdr = foundation.ZCLHeader.general(
84+
1, command, SINOPE_MANUFACTURER_ID, dirc
85+
).serialize()
86+
if attr is not None:
87+
cmd = foundation.GENERAL_COMMANDS[command].schema([attr]).serialize()
88+
else:
89+
cmd = b""
90+
return t.SerializableBytes(hdr + cmd).serialize()
91+
92+
93+
@pytest.mark.parametrize("quirk", (SinopeTechnologieslight,))
94+
@pytest.mark.parametrize(
95+
"press_type,exp_event",
96+
(
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),
103+
# Should gracefully handle broken actions.
104+
(t.uint8_t(0x00), None),
105+
),
106+
)
107+
async def test_sinope_light_switch(
108+
zigpy_device_from_quirk, quirk, press_type, exp_event
109+
):
110+
"""Test that button presses are sent as events."""
111+
device: Device = zigpy_device_from_quirk(quirk)
112+
cluster_id = SINOPE_MANUFACTURER_CLUSTER_ID
113+
endpoint_id = 1
114+
115+
class Listener:
116+
zha_send_event = mock.MagicMock()
117+
118+
cluster_listener = Listener()
119+
device.endpoints[endpoint_id].in_clusters[cluster_id].add_listener(cluster_listener)
120+
121+
attr = foundation.Attribute(
122+
attrid=0x54, # "action_report" attribute
123+
value=foundation.TypeValue(
124+
type=t.enum8(0x30),
125+
value=press_type,
126+
),
127+
)
128+
data = _get_packet_data(foundation.GeneralCommand.Report_Attributes, attr)
129+
device.handle_message(260, cluster_id, endpoint_id, endpoint_id, data)
130+
131+
if exp_event is None:
132+
assert cluster_listener.zha_send_event.call_count == 0
133+
else:
134+
assert cluster_listener.zha_send_event.call_count == 1
135+
assert cluster_listener.zha_send_event.call_args == mock.call(
136+
exp_event,
137+
{
138+
"attribute_id": 84,
139+
"attribute_name": "action_report",
140+
"value": press_type.value,
141+
},
142+
)
143+
144+
145+
@pytest.mark.parametrize("quirk", (SinopeTechnologieslight,))
146+
async def test_sinope_light_switch_non_action_report(zigpy_device_from_quirk, quirk):
147+
"""Test commands not handled by custom handler.
148+
149+
Make sure that non attribute report commands and attribute reports that don't
150+
concern action_report are passed through to base class.
151+
"""
152+
153+
device: Device = zigpy_device_from_quirk(quirk)
154+
cluster_id = SINOPE_MANUFACTURER_CLUSTER_ID
155+
endpoint_id = 1
156+
157+
class Listener:
158+
zha_send_event = mock.MagicMock()
159+
160+
cluster_listener = Listener()
161+
device.endpoints[endpoint_id].in_clusters[cluster_id].add_listener(cluster_listener)
162+
163+
# read attributes general command
164+
data = _get_packet_data(foundation.GeneralCommand.Read_Attributes)
165+
device.handle_message(260, cluster_id, endpoint_id, endpoint_id, data)
166+
# no ZHA events emitted because we only handle Report_Attributes
167+
assert cluster_listener.zha_send_event.call_count == 0
168+
169+
# report attributes command, but not "action_report"
170+
attr = foundation.Attribute(
171+
attrid=0x10, # "on_intensity" attribute
172+
value=foundation.TypeValue(
173+
type=t.int16s(0x29), value=t.int16s(50)
174+
), # 0x29 = t.int16s
175+
)
176+
data = _get_packet_data(foundation.GeneralCommand.Report_Attributes, attr)
177+
device.handle_message(260, cluster_id, endpoint_id, endpoint_id, data)
178+
# ZHA event emitted because we pass non "action_report"
179+
# reports to the base class handler.
180+
assert cluster_listener.zha_send_event.call_count == 1
181+
182+
183+
@pytest.mark.parametrize("quirk", (SinopeTechnologieslight,))
184+
async def test_sinope_light_switch_reporting(zigpy_device_from_quirk, quirk):
185+
"""Test that configuring reporting for action_report works."""
186+
device: Device = zigpy_device_from_quirk(quirk)
187+
188+
manu_cluster = device.endpoints[1].in_clusters[SINOPE_MANUFACTURER_CLUSTER_ID]
189+
190+
request_patch = mock.patch("zigpy.zcl.Cluster.request", mock.AsyncMock())
191+
bind_patch = mock.patch("zigpy.zcl.Cluster.bind", mock.AsyncMock())
192+
193+
with request_patch as request_mock, bind_patch as bind_mock:
194+
request_mock.return_value = (foundation.Status.SUCCESS, "done")
195+
196+
await manu_cluster.bind()
197+
await manu_cluster.configure_reporting(
198+
SinopeTechnologiesManufacturerCluster.AttributeDefs.action_report.id,
199+
3600,
200+
10800,
201+
1,
202+
)
203+
204+
assert len(request_mock.mock_calls) == 1
205+
assert len(bind_mock.mock_calls) == 1

zhaquirks/sinope/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424
SINOPE = "Sinope Technologies"
2525
SINOPE_MANUFACTURER_CLUSTER_ID = 0xFF01
26-
ATTRIBUTE_ACTION = "actionReport"
26+
ATTRIBUTE_ACTION = "action_report"
2727

2828
LIGHT_DEVICE_TRIGGERS = {
2929
(SHORT_PRESS, TURN_ON): {

0 commit comments

Comments
 (0)