Skip to content

Commit a4c15ef

Browse files
cspurkaxellebot
andauthored
Add quirk for Schneider Electric 1GANG/SHUTTER/1 (MEG5113-0300) (#2928)
* Add quirk for Schneider Electric 1GANG/SHUTTER/1 (MEG5113-0300) This commit is based on previous work by Axel Le Bot which unfortunately is incomplete and is unknown whether it works. I have hence removed the code that is not needed for the new quirk. * Add test asserting that unpatched ZCL commands keep working --------- Co-authored-by: Axel LE BOT <[email protected]>
1 parent 0df6f2b commit a4c15ef

File tree

3 files changed

+439
-0
lines changed

3 files changed

+439
-0
lines changed

tests/test_schneiderelectric.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
"""Tests for Schneider Electric devices."""
2+
from unittest import mock
3+
4+
from zigpy.zcl import foundation
5+
from zigpy.zcl.clusters.closures import WindowCovering
6+
7+
import zhaquirks.schneiderelectric.shutters
8+
9+
from tests.common import ClusterListener
10+
11+
zhaquirks.setup()
12+
13+
14+
def test_1gang_shutter_1_signature(assert_signature_matches_quirk):
15+
signature = {
16+
"node_descriptor": (
17+
"NodeDescriptor(logical_type=<LogicalType.Router: 1>, "
18+
"complex_descriptor_available=0, user_descriptor_available=0, reserved=0, "
19+
"aps_flags=0, frequency_band=<FrequencyBand.Freq2400MHz: 8>, "
20+
"mac_capability_flags=<MACCapabilityFlags.FullFunctionDevice|MainsPowered"
21+
"|RxOnWhenIdle|AllocateAddress: 142>, manufacturer_code=4190, "
22+
"maximum_buffer_size=82, maximum_incoming_transfer_size=82, "
23+
"server_mask=10752, maximum_outgoing_transfer_size=82, "
24+
"descriptor_capability_field=<DescriptorCapability.NONE: 0>, "
25+
"*allocate_address=True, *is_alternate_pan_coordinator=False, "
26+
"*is_coordinator=False, *is_end_device=False, "
27+
"*is_full_function_device=True, *is_mains_powered=True, "
28+
"*is_receiver_on_when_idle=True, *is_router=True, "
29+
"*is_security_capable=False)"
30+
),
31+
"endpoints": {
32+
"5": {
33+
"profile_id": 0x0104,
34+
"device_type": "0x0202",
35+
"in_clusters": [
36+
"0x0000",
37+
"0x0003",
38+
"0x0004",
39+
"0x0005",
40+
"0x0102",
41+
"0x0b05",
42+
],
43+
"out_clusters": ["0x0019"],
44+
},
45+
"21": {
46+
"profile_id": 0x0104,
47+
"device_type": "0x0104",
48+
"in_clusters": [
49+
"0x0000",
50+
"0x0003",
51+
"0x0b05",
52+
"0xff17",
53+
],
54+
"out_clusters": [
55+
"0x0003",
56+
"0x0005",
57+
"0x0006",
58+
"0x0008",
59+
"0x0019",
60+
"0x0102",
61+
],
62+
},
63+
},
64+
"manufacturer": "Schneider Electric",
65+
"model": "1GANG/SHUTTER/1",
66+
"class": "zigpy.device.Device",
67+
}
68+
assert_signature_matches_quirk(
69+
zhaquirks.schneiderelectric.shutters.OneGangShutter1, signature
70+
)
71+
72+
73+
async def test_1gang_shutter_1_go_to_lift_percentage_cmd(zigpy_device_from_quirk):
74+
"""Asserts that the go_to_lift_percentage command inverts the percentage value."""
75+
76+
device = zigpy_device_from_quirk(
77+
zhaquirks.schneiderelectric.shutters.OneGangShutter1
78+
)
79+
window_covering_cluster = device.endpoints[5].window_covering
80+
81+
p = mock.patch.object(window_covering_cluster, "request", mock.AsyncMock())
82+
with p as request_mock:
83+
request_mock.return_value = (foundation.Status.SUCCESS, "done")
84+
85+
await window_covering_cluster.go_to_lift_percentage(58)
86+
87+
assert request_mock.call_count == 1
88+
assert request_mock.call_args[0][1] == (
89+
WindowCovering.ServerCommandDefs.go_to_lift_percentage.id
90+
)
91+
assert request_mock.call_args[0][3] == 42 # 100 - 58
92+
93+
94+
async def test_1gang_shutter_1_unpatched_cmd(zigpy_device_from_quirk):
95+
"""Asserts that unpatched ZCL commands keep working."""
96+
97+
device = zigpy_device_from_quirk(
98+
zhaquirks.schneiderelectric.shutters.OneGangShutter1
99+
)
100+
window_covering_cluster = device.endpoints[5].window_covering
101+
102+
p = mock.patch.object(window_covering_cluster, "request", mock.AsyncMock())
103+
with p as request_mock:
104+
request_mock.return_value = (foundation.Status.SUCCESS, "done")
105+
106+
await window_covering_cluster.up_open()
107+
108+
assert request_mock.call_count == 1
109+
assert request_mock.call_args[0][1] == (
110+
WindowCovering.ServerCommandDefs.up_open.id
111+
)
112+
113+
114+
async def test_1gang_shutter_1_lift_percentage_updates(zigpy_device_from_quirk):
115+
"""Asserts that updates to the ``current_position_lift_percentage`` attribute
116+
(e.g., by the device) invert the reported percentage value."""
117+
118+
device = zigpy_device_from_quirk(
119+
zhaquirks.schneiderelectric.shutters.OneGangShutter1
120+
)
121+
window_covering_cluster = device.endpoints[5].window_covering
122+
cluster_listener = ClusterListener(window_covering_cluster)
123+
124+
window_covering_cluster.update_attribute(
125+
WindowCovering.AttributeDefs.current_position_lift_percentage.id,
126+
77,
127+
)
128+
129+
assert len(cluster_listener.attribute_updates) == 1
130+
assert cluster_listener.attribute_updates[0] == (
131+
WindowCovering.AttributeDefs.current_position_lift_percentage.id,
132+
23, # 100 - 77
133+
)
134+
assert len(cluster_listener.cluster_commands) == 0
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
"""Quirks implementations for Schneider Electric devices."""
2+
from typing import Any, Coroutine, Final, Union
3+
4+
from zigpy import types as t
5+
from zigpy.quirks import CustomCluster
6+
from zigpy.zcl import foundation
7+
from zigpy.zcl.clusters.closures import WindowCovering
8+
from zigpy.zcl.clusters.general import Basic
9+
from zigpy.zcl.foundation import BaseAttributeDefs, ZCLAttributeDef
10+
11+
SE_MANUF_NAME = "Schneider Electric"
12+
SE_MANUF_ID = 4190
13+
14+
15+
class SEBasic(CustomCluster, Basic):
16+
"""Schneider Electric manufacturer specific Basic cluster."""
17+
18+
class AttributeDefs(Basic.AttributeDefs):
19+
se_sw_build_id: Final = ZCLAttributeDef(
20+
id=0xE001,
21+
type=t.CharacterString,
22+
is_manufacturer_specific=True,
23+
) # value: "002.004.016 R"
24+
unknown_attribute_57346: Final = ZCLAttributeDef(
25+
id=0xE002,
26+
type=t.CharacterString,
27+
is_manufacturer_specific=True,
28+
) # value: "001.000.000"
29+
unknown_attribute_57348: Final = ZCLAttributeDef(
30+
id=0xE004,
31+
type=t.CharacterString,
32+
is_manufacturer_specific=True,
33+
) # value: "213249FEFF5ECFD"
34+
unknown_attribute_57351: Final = ZCLAttributeDef(
35+
id=0xE007,
36+
type=t.enum16,
37+
is_manufacturer_specific=True,
38+
)
39+
se_device_type: Final = ZCLAttributeDef(
40+
id=0xE008,
41+
type=t.CharacterString,
42+
is_manufacturer_specific=True,
43+
) # value: "Wiser Light"
44+
se_model: Final = ZCLAttributeDef(
45+
id=0xE009,
46+
type=t.CharacterString,
47+
is_manufacturer_specific=True,
48+
) # value: "NHPB/SHUTTER/1"
49+
se_realm: Final = ZCLAttributeDef(
50+
id=0xE00A,
51+
type=t.CharacterString,
52+
is_manufacturer_specific=True,
53+
) # value: "Wiser Home"
54+
unknown_attribute_57355: Final = ZCLAttributeDef(
55+
id=0xE00B,
56+
type=t.CharacterString,
57+
is_manufacturer_specific=True,
58+
) # value: "http://www.schneider-electric.com"
59+
60+
61+
class SEWindowCovering(CustomCluster, WindowCovering):
62+
"""Schneider Electric manufacturer specific Window Covering cluster."""
63+
64+
class AttributeDefs(WindowCovering.AttributeDefs):
65+
unknown_attribute_65533: Final = ZCLAttributeDef(
66+
id=0xFFFD,
67+
type=t.uint16_t,
68+
is_manufacturer_specific=True,
69+
)
70+
lift_duration: Final = ZCLAttributeDef(
71+
id=0xE000,
72+
type=t.uint16_t,
73+
is_manufacturer_specific=True,
74+
)
75+
unknown_attribute_57360: Final = ZCLAttributeDef(
76+
id=0xE010,
77+
type=t.bitmap8,
78+
is_manufacturer_specific=True,
79+
)
80+
unknown_attribute_57362: Final = ZCLAttributeDef(
81+
id=0xE012,
82+
type=t.uint16_t,
83+
is_manufacturer_specific=True,
84+
)
85+
unknown_attribute_57363: Final = ZCLAttributeDef(
86+
id=0xE013,
87+
type=t.bitmap8,
88+
is_manufacturer_specific=True,
89+
)
90+
unknown_attribute_57364: Final = ZCLAttributeDef(
91+
id=0xE014,
92+
type=t.uint16_t,
93+
is_manufacturer_specific=True,
94+
)
95+
unknown_attribute_57365: Final = ZCLAttributeDef(
96+
id=0xE015,
97+
type=t.uint16_t,
98+
is_manufacturer_specific=True,
99+
)
100+
unknown_attribute_57366: Final = ZCLAttributeDef(
101+
id=0xE016,
102+
type=t.uint16_t,
103+
is_manufacturer_specific=True,
104+
)
105+
unknown_attribute_57367: Final = ZCLAttributeDef(
106+
id=0xE017,
107+
type=t.uint8_t,
108+
is_manufacturer_specific=True,
109+
)
110+
111+
def _update_attribute(self, attrid: Union[int, t.uint16_t], value: Any):
112+
if attrid == WindowCovering.AttributeDefs.current_position_lift_percentage.id:
113+
# Invert the percentage value
114+
value = 100 - value
115+
super()._update_attribute(attrid, value)
116+
117+
async def command(
118+
self,
119+
command_id: Union[foundation.GeneralCommand, int, t.uint8_t],
120+
*args: Any,
121+
**kwargs: Any,
122+
) -> Coroutine:
123+
command = self.server_commands[command_id]
124+
125+
# Override default command to invert percent lift value.
126+
if command.id == WindowCovering.ServerCommandDefs.go_to_lift_percentage.id:
127+
percent = args[0]
128+
percent = 100 - percent
129+
return await super().command(command_id, percent, **kwargs)
130+
131+
return await super().command(command_id, *args, **kwargs)
132+
133+
134+
class SELedIndicatorSignals(t.enum8):
135+
"""Available LED indicator signal combinations.
136+
137+
Shutter movement can be indicated with a red LED signal. A green LED
138+
light permanently provides orientation, if desired.
139+
"""
140+
141+
MOVEMENT_ONLY = 0x00
142+
MOVEMENT_AND_ORIENTATION = 0x01
143+
ORIENTATION_ONLY = 0x02
144+
NONE = 0x03
145+
146+
147+
class SESpecific(CustomCluster):
148+
"""Schneider Electric manufacturer specific cluster."""
149+
150+
name = "Schneider Electric Manufacturer Specific"
151+
ep_attribute = "schneider_electric_manufacturer"
152+
cluster_id = 0xFF17
153+
154+
class AttributeDefs(BaseAttributeDefs):
155+
led_indicator_signals: Final = ZCLAttributeDef(
156+
id=0x0000,
157+
type=SELedIndicatorSignals,
158+
is_manufacturer_specific=True,
159+
)
160+
unknown_attribute_1: Final = ZCLAttributeDef(
161+
id=0x0001,
162+
type=t.enum8,
163+
is_manufacturer_specific=True,
164+
)
165+
unknown_attribute_16: Final = ZCLAttributeDef(
166+
id=0x0010,
167+
type=t.uint8_t,
168+
is_manufacturer_specific=True,
169+
)
170+
unknown_attribute_17: Final = ZCLAttributeDef(
171+
id=0x0011,
172+
type=t.uint16_t,
173+
is_manufacturer_specific=True,
174+
)
175+
unknown_attribute_32: Final = ZCLAttributeDef(
176+
id=0x0020,
177+
type=t.uint8_t,
178+
is_manufacturer_specific=True,
179+
)
180+
unknown_attribute_33: Final = ZCLAttributeDef(
181+
id=0x0021,
182+
type=t.uint16_t,
183+
is_manufacturer_specific=True,
184+
)
185+
unknown_attribute_65533: Final = ZCLAttributeDef(
186+
id=0xFFFD,
187+
type=t.uint16_t,
188+
is_manufacturer_specific=True,
189+
)

0 commit comments

Comments
 (0)