Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
89ea58d
Add skeleton
axellebot Aug 23, 2022
e9a9e88
Add SE shutter related attributes
axellebot Aug 26, 2022
ca5559d
Edit lift_duration attribute
axellebot Aug 26, 2022
4d9771d
Fix attribute name
axellebot Aug 26, 2022
9ff2ed1
Revert covering lift percentage
axellebot Aug 26, 2022
1afe73e
Remove ZCLAttributeDef use for attribute
axellebot Aug 27, 2022
17dca84
Fix attributes update
axellebot Aug 28, 2022
2a53080
Fix lift_duration attribute ID
axellebot Aug 28, 2022
c6da55b
Add Schneider Electric readme
axellebot Aug 29, 2022
c9ac278
Add SEOnOff cluster
axellebot Aug 30, 2022
f3ba323
Add CH2AX/SWITCH/1 signature
axellebot Aug 30, 2022
3829121
Add found dimmer/thermostat mode
axellebot Aug 30, 2022
e01b590
Add CH2AX/SWITCH/1 scan
axellebot Aug 31, 2022
6b90d19
Refactor modules
axellebot Aug 31, 2022
f3e7a34
Fix tests
axellebot Sep 9, 2022
6725f0e
Merge branch 'dev' into schneider
cspurk Jan 17, 2024
30f59f1
Add quirk for Schneider Electric 1GANG/SHUTTER/1 (MEG5113-0300)
cspurk Jan 21, 2024
1aa7ff4
Merge branch 'dev' into schneider
cspurk Jan 21, 2024
5257939
Correct type hint
cspurk Jan 21, 2024
adf0ad9
Add test asserting that unpatched ZCL commands keep working
cspurk Jan 21, 2024
a4d596c
Remove README by reviewer request
cspurk Jan 27, 2024
5837511
Make structure of packages more consistent with others
cspurk Jan 31, 2024
33f90aa
Remove superfluous cluster ID comments
cspurk Jan 31, 2024
b6c973e
Correct constant value for Schneider Electric manufacturer code
cspurk Feb 4, 2024
18dce89
Merge branch 'dev' into schneider
cspurk Feb 12, 2024
a7ddd34
Define cluster attributes in a more modern way
cspurk Feb 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 134 additions & 0 deletions tests/test_schneiderelectric.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
"""Tests for Schneider Electric devices."""
from unittest import mock

from zigpy.zcl import foundation
from zigpy.zcl.clusters.closures import WindowCovering

import zhaquirks.schneiderelectric.shutters

from tests.common import ClusterListener

zhaquirks.setup()


def test_1gang_shutter_1_signature(assert_signature_matches_quirk):
signature = {
"node_descriptor": (
"NodeDescriptor(logical_type=<LogicalType.Router: 1>, "
"complex_descriptor_available=0, user_descriptor_available=0, reserved=0, "
"aps_flags=0, frequency_band=<FrequencyBand.Freq2400MHz: 8>, "
"mac_capability_flags=<MACCapabilityFlags.FullFunctionDevice|MainsPowered"
"|RxOnWhenIdle|AllocateAddress: 142>, manufacturer_code=4190, "
"maximum_buffer_size=82, maximum_incoming_transfer_size=82, "
"server_mask=10752, maximum_outgoing_transfer_size=82, "
"descriptor_capability_field=<DescriptorCapability.NONE: 0>, "
"*allocate_address=True, *is_alternate_pan_coordinator=False, "
"*is_coordinator=False, *is_end_device=False, "
"*is_full_function_device=True, *is_mains_powered=True, "
"*is_receiver_on_when_idle=True, *is_router=True, "
"*is_security_capable=False)"
),
"endpoints": {
"5": {
"profile_id": 0x0104,
"device_type": "0x0202",
"in_clusters": [
"0x0000",
"0x0003",
"0x0004",
"0x0005",
"0x0102",
"0x0b05",
],
"out_clusters": ["0x0019"],
},
"21": {
"profile_id": 0x0104,
"device_type": "0x0104",
"in_clusters": [
"0x0000",
"0x0003",
"0x0b05",
"0xff17",
],
"out_clusters": [
"0x0003",
"0x0005",
"0x0006",
"0x0008",
"0x0019",
"0x0102",
],
},
},
"manufacturer": "Schneider Electric",
"model": "1GANG/SHUTTER/1",
"class": "zigpy.device.Device",
}
assert_signature_matches_quirk(
zhaquirks.schneiderelectric.shutters.OneGangShutter1, signature
)


async def test_1gang_shutter_1_go_to_lift_percentage_cmd(zigpy_device_from_quirk):
"""Asserts that the go_to_lift_percentage command inverts the percentage value."""

device = zigpy_device_from_quirk(
zhaquirks.schneiderelectric.shutters.OneGangShutter1
)
window_covering_cluster = device.endpoints[5].window_covering

p = mock.patch.object(window_covering_cluster, "request", mock.AsyncMock())
with p as request_mock:
request_mock.return_value = (foundation.Status.SUCCESS, "done")

await window_covering_cluster.go_to_lift_percentage(58)

assert request_mock.call_count == 1
assert request_mock.call_args[0][1] == (
WindowCovering.ServerCommandDefs.go_to_lift_percentage.id
)
assert request_mock.call_args[0][3] == 42 # 100 - 58


async def test_1gang_shutter_1_unpatched_cmd(zigpy_device_from_quirk):
"""Asserts that unpatched ZCL commands keep working."""

device = zigpy_device_from_quirk(
zhaquirks.schneiderelectric.shutters.OneGangShutter1
)
window_covering_cluster = device.endpoints[5].window_covering

p = mock.patch.object(window_covering_cluster, "request", mock.AsyncMock())
with p as request_mock:
request_mock.return_value = (foundation.Status.SUCCESS, "done")

await window_covering_cluster.up_open()

assert request_mock.call_count == 1
assert request_mock.call_args[0][1] == (
WindowCovering.ServerCommandDefs.up_open.id
)


async def test_1gang_shutter_1_lift_percentage_updates(zigpy_device_from_quirk):
"""Asserts that updates to the ``current_position_lift_percentage`` attribute
(e.g., by the device) invert the reported percentage value."""

device = zigpy_device_from_quirk(
zhaquirks.schneiderelectric.shutters.OneGangShutter1
)
window_covering_cluster = device.endpoints[5].window_covering
cluster_listener = ClusterListener(window_covering_cluster)

window_covering_cluster.update_attribute(
WindowCovering.AttributeDefs.current_position_lift_percentage.id,
77,
)

assert len(cluster_listener.attribute_updates) == 1
assert cluster_listener.attribute_updates[0] == (
WindowCovering.AttributeDefs.current_position_lift_percentage.id,
23, # 100 - 77
)
assert len(cluster_listener.cluster_commands) == 0
189 changes: 189 additions & 0 deletions zhaquirks/schneiderelectric/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
"""Quirks implementations for Schneider Electric devices."""
from typing import Any, Coroutine, Final, Union

from zigpy import types as t
from zigpy.quirks import CustomCluster
from zigpy.zcl import foundation
from zigpy.zcl.clusters.closures import WindowCovering
from zigpy.zcl.clusters.general import Basic
from zigpy.zcl.foundation import BaseAttributeDefs, ZCLAttributeDef

SE_MANUF_NAME = "Schneider Electric"
SE_MANUF_ID = 4190


class SEBasic(CustomCluster, Basic):
"""Schneider Electric manufacturer specific Basic cluster."""

class AttributeDefs(Basic.AttributeDefs):
se_sw_build_id: Final = ZCLAttributeDef(
id=0xE001,
type=t.CharacterString,
is_manufacturer_specific=True,
) # value: "002.004.016 R"
unknown_attribute_57346: Final = ZCLAttributeDef(
id=0xE002,
type=t.CharacterString,
is_manufacturer_specific=True,
) # value: "001.000.000"
unknown_attribute_57348: Final = ZCLAttributeDef(
id=0xE004,
type=t.CharacterString,
is_manufacturer_specific=True,
) # value: "213249FEFF5ECFD"
unknown_attribute_57351: Final = ZCLAttributeDef(
id=0xE007,
type=t.enum16,
is_manufacturer_specific=True,
)
se_device_type: Final = ZCLAttributeDef(
id=0xE008,
type=t.CharacterString,
is_manufacturer_specific=True,
) # value: "Wiser Light"
se_model: Final = ZCLAttributeDef(
id=0xE009,
type=t.CharacterString,
is_manufacturer_specific=True,
) # value: "NHPB/SHUTTER/1"
se_realm: Final = ZCLAttributeDef(
id=0xE00A,
type=t.CharacterString,
is_manufacturer_specific=True,
) # value: "Wiser Home"
unknown_attribute_57355: Final = ZCLAttributeDef(
id=0xE00B,
type=t.CharacterString,
is_manufacturer_specific=True,
) # value: "http://www.schneider-electric.com"


class SEWindowCovering(CustomCluster, WindowCovering):
"""Schneider Electric manufacturer specific Window Covering cluster."""

class AttributeDefs(WindowCovering.AttributeDefs):
unknown_attribute_65533: Final = ZCLAttributeDef(
id=0xFFFD,
type=t.uint16_t,
is_manufacturer_specific=True,
)
lift_duration: Final = ZCLAttributeDef(
id=0xE000,
type=t.uint16_t,
is_manufacturer_specific=True,
)
unknown_attribute_57360: Final = ZCLAttributeDef(
id=0xE010,
type=t.bitmap8,
is_manufacturer_specific=True,
)
unknown_attribute_57362: Final = ZCLAttributeDef(
id=0xE012,
type=t.uint16_t,
is_manufacturer_specific=True,
)
unknown_attribute_57363: Final = ZCLAttributeDef(
id=0xE013,
type=t.bitmap8,
is_manufacturer_specific=True,
)
unknown_attribute_57364: Final = ZCLAttributeDef(
id=0xE014,
type=t.uint16_t,
is_manufacturer_specific=True,
)
unknown_attribute_57365: Final = ZCLAttributeDef(
id=0xE015,
type=t.uint16_t,
is_manufacturer_specific=True,
)
unknown_attribute_57366: Final = ZCLAttributeDef(
id=0xE016,
type=t.uint16_t,
is_manufacturer_specific=True,
)
unknown_attribute_57367: Final = ZCLAttributeDef(
id=0xE017,
type=t.uint8_t,
is_manufacturer_specific=True,
)

def _update_attribute(self, attrid: Union[int, t.uint16_t], value: Any):
if attrid == WindowCovering.AttributeDefs.current_position_lift_percentage.id:
# Invert the percentage value
value = 100 - value
super()._update_attribute(attrid, value)

async def command(
self,
command_id: Union[foundation.GeneralCommand, int, t.uint8_t],
*args: Any,
**kwargs: Any,
) -> Coroutine:
command = self.server_commands[command_id]

# Override default command to invert percent lift value.
if command.id == WindowCovering.ServerCommandDefs.go_to_lift_percentage.id:
percent = args[0]
percent = 100 - percent
return await super().command(command_id, percent, **kwargs)

return await super().command(command_id, *args, **kwargs)


class SELedIndicatorSignals(t.enum8):
"""Available LED indicator signal combinations.

Shutter movement can be indicated with a red LED signal. A green LED
light permanently provides orientation, if desired.
"""

MOVEMENT_ONLY = 0x00
MOVEMENT_AND_ORIENTATION = 0x01
ORIENTATION_ONLY = 0x02
NONE = 0x03


class SESpecific(CustomCluster):
"""Schneider Electric manufacturer specific cluster."""

name = "Schneider Electric Manufacturer Specific"
ep_attribute = "schneider_electric_manufacturer"
cluster_id = 0xFF17

class AttributeDefs(BaseAttributeDefs):
led_indicator_signals: Final = ZCLAttributeDef(
id=0x0000,
type=SELedIndicatorSignals,
is_manufacturer_specific=True,
)
unknown_attribute_1: Final = ZCLAttributeDef(
id=0x0001,
type=t.enum8,
is_manufacturer_specific=True,
)
unknown_attribute_16: Final = ZCLAttributeDef(
id=0x0010,
type=t.uint8_t,
is_manufacturer_specific=True,
)
unknown_attribute_17: Final = ZCLAttributeDef(
id=0x0011,
type=t.uint16_t,
is_manufacturer_specific=True,
)
unknown_attribute_32: Final = ZCLAttributeDef(
id=0x0020,
type=t.uint8_t,
is_manufacturer_specific=True,
)
unknown_attribute_33: Final = ZCLAttributeDef(
id=0x0021,
type=t.uint16_t,
is_manufacturer_specific=True,
)
unknown_attribute_65533: Final = ZCLAttributeDef(
id=0xFFFD,
type=t.uint16_t,
is_manufacturer_specific=True,
)
Loading