diff --git a/tests/test_candeo.py b/tests/test_candeo.py index 2494d5173a..41e083d51e 100644 --- a/tests/test_candeo.py +++ b/tests/test_candeo.py @@ -1,15 +1,46 @@ """Tests for Candeo.""" +from unittest import mock + import pytest +from zigpy.zcl import foundation from zigpy.zcl.clusters.measurement import IlluminanceMeasurement from tests.common import ClusterListener import zhaquirks from zhaquirks.candeo import CANDEO +from zhaquirks.candeo.scene_switch_remote_5_button_rotary import ( + CandeoSceneSwitchRemoteButtonActionMap, + CandeoSceneSwitchRemoteButtonNumberMap, + CandeoSceneSwitchRemoteCluster, + CandeoSceneSwitchRemoteClusterCommand, + CandeoSceneSwitchRemoteMessageType, + CandeoSceneSwitchRemoteRingActionMap, + CandeoSceneSwitchRemoteRingDirectionMap, +) +from zhaquirks.const import ( + BUTTON, + BUTTON_1, + BUTTON_2, + BUTTON_3, + BUTTON_4, + BUTTON_CENTRE, + COMMAND_CONTINUED_ROTATING, + COMMAND_DOUBLE, + COMMAND_HOLD, + COMMAND_PRESS, + COMMAND_RELEASE, + COMMAND_STARTED_ROTATING, + COMMAND_STOPPED_ROTATING, + LEFT, + RIGHT, + ROTATED, +) zhaquirks.setup() +# candeo motion tests @pytest.mark.parametrize( "lux_in, lux_out", ( @@ -32,3 +63,618 @@ async def test_candeo_motion_illuminance(zigpy_device_from_v2_quirk, lux_in, lux assert len(illuminance_listener.attribute_updates) == 1 assert illuminance_listener.attribute_updates[0][0] == illuminance_attr_id assert illuminance_listener.attribute_updates[0][1] == lux_out + + +# candeo scene switch remote 5 button rotarty tests + + +@pytest.mark.asyncio +async def test_CandeoSceneSwitchRemoteCluster_apply_custom_configuration( + zigpy_device_from_v2_quirk, +): + """Test apply custom configuration is called and calls bind on the cluster.""" + device = zigpy_device_from_v2_quirk(manufacturer=CANDEO, model="C-ZB-SR5BR") + cluster = device.endpoints[1].CandeoSceneSwitchRemoteCluster_Cluster + cluster.bind = mock.AsyncMock() + await cluster.apply_custom_configuration() + cluster.bind.assert_awaited_once() + + +def test_CandeoSceneSwitchRemoteCluster_duplicate_sequence_number( + zigpy_device_from_v2_quirk, +): + """Test duplicate sequence number ignored.""" + device = zigpy_device_from_v2_quirk(manufacturer=CANDEO, model="C-ZB-SR5BR") + + cluster = device.endpoints[1].CandeoSceneSwitchRemoteCluster_Cluster + listener = mock.MagicMock() + cluster.add_listener(listener) + + cluster.send_default_rsp = mock.MagicMock() + + header = foundation.ZCLHeader() + header.command_id = ( + CandeoSceneSwitchRemoteCluster.ServerCommandDefs.candeo_scene_switch_remote.id + ) + header.frame_control = foundation.FrameControl.cluster() + header.tsn = 5 + + args = CandeoSceneSwitchRemoteClusterCommand( + CandeoSceneSwitchRemoteMessageType.button_press, + 0x0, + CandeoSceneSwitchRemoteButtonNumberMap.button_1, + CandeoSceneSwitchRemoteButtonNumberMap.button_1, + ) + + cluster.handle_cluster_request(header, args) + cluster.handle_cluster_request(header, args) + + assert cluster.send_default_rsp.call_count == 2 + assert listener.zha_send_event.call_count == 1 + + +def test_CandeoSceneSwitchRemoteCluster_unknown_command_id(zigpy_device_from_v2_quirk): + """Test unknown command id.""" + device = zigpy_device_from_v2_quirk(manufacturer=CANDEO, model="C-ZB-SR5BR") + + cluster = device.endpoints[1].CandeoSceneSwitchRemoteCluster_Cluster + listener = mock.MagicMock() + cluster.add_listener(listener) + + cluster.send_default_rsp = mock.MagicMock() + + header = foundation.ZCLHeader() + header.command_id = 0x99 + header.frame_control = foundation.FrameControl.cluster() + + cluster.handle_cluster_request(header, []) + + assert listener.zha_send_event.call_count == 0 + + +def test_CandeoSceneSwitchRemoteCluster_missing_schema_fields( + zigpy_device_from_v2_quirk, +): + """Test missing CandeoSceneSwitchRemoteClusterCommand schema fields.""" + device = zigpy_device_from_v2_quirk(manufacturer=CANDEO, model="C-ZB-SR5BR") + + cluster = device.endpoints[1].CandeoSceneSwitchRemoteCluster_Cluster + listener = mock.MagicMock() + cluster.add_listener(listener) + + cluster.send_default_rsp = mock.MagicMock() + + header = foundation.ZCLHeader() + header.command_id = ( + CandeoSceneSwitchRemoteCluster.ServerCommandDefs.candeo_scene_switch_remote.id + ) + header.frame_control = foundation.FrameControl.cluster() + + args = CandeoSceneSwitchRemoteClusterCommand(None, None, None, None) + + cluster.handle_cluster_request(header, args) + + assert listener.zha_send_event.call_count == 0 + + +def test_CandeoSceneSwitchRemoteCluster_unknown_message_type( + zigpy_device_from_v2_quirk, +): + """Test unknown CandeoSceneSwitchRemoteMessageType.""" + device = zigpy_device_from_v2_quirk(manufacturer=CANDEO, model="C-ZB-SR5BR") + + cluster = device.endpoints[1].CandeoSceneSwitchRemoteCluster_Cluster + listener = mock.MagicMock() + cluster.add_listener(listener) + + cluster.send_default_rsp = mock.MagicMock() + + header = foundation.ZCLHeader() + header.command_id = ( + CandeoSceneSwitchRemoteCluster.ServerCommandDefs.candeo_scene_switch_remote.id + ) + header.frame_control = foundation.FrameControl.cluster() + + args = CandeoSceneSwitchRemoteClusterCommand( + 0x99, + 0x0, + CandeoSceneSwitchRemoteButtonNumberMap.button_1, + CandeoSceneSwitchRemoteButtonActionMap.press, + ) + + cluster.handle_cluster_request(header, args) + + assert listener.zha_send_event.call_count == 0 + + +@pytest.mark.parametrize( + "button_number, button_action, expected_button_name, expected_button_action_name", + [ + ( + CandeoSceneSwitchRemoteButtonNumberMap.button_1, + CandeoSceneSwitchRemoteButtonActionMap.press, + BUTTON_1, + COMMAND_PRESS, + ), + ( + CandeoSceneSwitchRemoteButtonNumberMap.button_1, + CandeoSceneSwitchRemoteButtonActionMap.double, + BUTTON_1, + COMMAND_DOUBLE, + ), + ( + CandeoSceneSwitchRemoteButtonNumberMap.button_1, + CandeoSceneSwitchRemoteButtonActionMap.hold, + BUTTON_1, + COMMAND_HOLD, + ), + ( + CandeoSceneSwitchRemoteButtonNumberMap.button_1, + CandeoSceneSwitchRemoteButtonActionMap.release, + BUTTON_1, + COMMAND_RELEASE, + ), + ( + CandeoSceneSwitchRemoteButtonNumberMap.button_2, + CandeoSceneSwitchRemoteButtonActionMap.press, + BUTTON_2, + COMMAND_PRESS, + ), + ( + CandeoSceneSwitchRemoteButtonNumberMap.button_2, + CandeoSceneSwitchRemoteButtonActionMap.double, + BUTTON_2, + COMMAND_DOUBLE, + ), + ( + CandeoSceneSwitchRemoteButtonNumberMap.button_2, + CandeoSceneSwitchRemoteButtonActionMap.hold, + BUTTON_2, + COMMAND_HOLD, + ), + ( + CandeoSceneSwitchRemoteButtonNumberMap.button_2, + CandeoSceneSwitchRemoteButtonActionMap.release, + BUTTON_2, + COMMAND_RELEASE, + ), + ( + CandeoSceneSwitchRemoteButtonNumberMap.button_3, + CandeoSceneSwitchRemoteButtonActionMap.press, + BUTTON_3, + COMMAND_PRESS, + ), + ( + CandeoSceneSwitchRemoteButtonNumberMap.button_3, + CandeoSceneSwitchRemoteButtonActionMap.double, + BUTTON_3, + COMMAND_DOUBLE, + ), + ( + CandeoSceneSwitchRemoteButtonNumberMap.button_3, + CandeoSceneSwitchRemoteButtonActionMap.hold, + BUTTON_3, + COMMAND_HOLD, + ), + ( + CandeoSceneSwitchRemoteButtonNumberMap.button_3, + CandeoSceneSwitchRemoteButtonActionMap.release, + BUTTON_3, + COMMAND_RELEASE, + ), + ( + CandeoSceneSwitchRemoteButtonNumberMap.button_4, + CandeoSceneSwitchRemoteButtonActionMap.press, + BUTTON_4, + COMMAND_PRESS, + ), + ( + CandeoSceneSwitchRemoteButtonNumberMap.button_4, + CandeoSceneSwitchRemoteButtonActionMap.double, + BUTTON_4, + COMMAND_DOUBLE, + ), + ( + CandeoSceneSwitchRemoteButtonNumberMap.button_4, + CandeoSceneSwitchRemoteButtonActionMap.hold, + BUTTON_4, + COMMAND_HOLD, + ), + ( + CandeoSceneSwitchRemoteButtonNumberMap.button_4, + CandeoSceneSwitchRemoteButtonActionMap.release, + BUTTON_4, + COMMAND_RELEASE, + ), + ( + CandeoSceneSwitchRemoteButtonNumberMap.button_centre, + CandeoSceneSwitchRemoteButtonActionMap.press, + BUTTON_CENTRE, + COMMAND_PRESS, + ), + ( + CandeoSceneSwitchRemoteButtonNumberMap.button_centre, + CandeoSceneSwitchRemoteButtonActionMap.double, + BUTTON_CENTRE, + COMMAND_DOUBLE, + ), + ( + CandeoSceneSwitchRemoteButtonNumberMap.button_centre, + CandeoSceneSwitchRemoteButtonActionMap.hold, + BUTTON_CENTRE, + COMMAND_HOLD, + ), + ( + CandeoSceneSwitchRemoteButtonNumberMap.button_centre, + CandeoSceneSwitchRemoteButtonActionMap.release, + BUTTON_CENTRE, + COMMAND_RELEASE, + ), + ], +) +def test_CandeoSceneSwitchRemoteCluster__button_number_and_button_action_combinations( + zigpy_device_from_v2_quirk, + button_number, + button_action, + expected_button_name, + expected_button_action_name, +): + """Test button numbers and button actions generate events correctly.""" + device = zigpy_device_from_v2_quirk(manufacturer=CANDEO, model="C-ZB-SR5BR") + + cluster = device.endpoints[1].CandeoSceneSwitchRemoteCluster_Cluster + listener = mock.MagicMock() + cluster.add_listener(listener) + + cluster.send_default_rsp = mock.MagicMock() + + header = foundation.ZCLHeader() + header.command_id = ( + CandeoSceneSwitchRemoteCluster.ServerCommandDefs.candeo_scene_switch_remote.id + ) + header.frame_control = foundation.FrameControl.cluster() + + args = CandeoSceneSwitchRemoteClusterCommand( + CandeoSceneSwitchRemoteMessageType.button_press, + 0x0, + button_number, + button_action, + ) + + cluster.handle_cluster_request(header, args) + + button_event = listener.zha_send_event.call_args[0] + + assert button_event[0] == expected_button_action_name + + assert button_event[1][BUTTON] == expected_button_name + + +@pytest.mark.parametrize( + "button_number, button_action", + [ + (0x99, CandeoSceneSwitchRemoteButtonActionMap.press), + (CandeoSceneSwitchRemoteButtonNumberMap.button_1, 0x99), + ], +) +def test_CandeoSceneSwitchRemoteCluster_unknown_button_number_or_button_action( + zigpy_device_from_v2_quirk, button_number, button_action +): + """Test unknown button numbers and button actiona are ignored.""" + + device = zigpy_device_from_v2_quirk(manufacturer=CANDEO, model="C-ZB-SR5BR") + + cluster = device.endpoints[1].CandeoSceneSwitchRemoteCluster_Cluster + listener = mock.MagicMock() + cluster.add_listener(listener) + + cluster.send_default_rsp = mock.MagicMock() + + header = foundation.ZCLHeader() + header.command_id = ( + CandeoSceneSwitchRemoteCluster.ServerCommandDefs.candeo_scene_switch_remote.id + ) + header.frame_control = foundation.FrameControl.cluster() + + args = CandeoSceneSwitchRemoteClusterCommand( + CandeoSceneSwitchRemoteMessageType.button_press, + 0x0, + button_number, + button_action, + ) + + cluster.handle_cluster_request(header, args) + + assert listener.zha_send_event.call_count == 0 + + +@pytest.mark.parametrize( + "ring_direction", + [ + (CandeoSceneSwitchRemoteRingDirectionMap.left), + (CandeoSceneSwitchRemoteRingDirectionMap.right), + ], +) +def test_CandeoSceneSwitchRemoteCluster_ring_started_rotating( + zigpy_device_from_v2_quirk, ring_direction +): + """Test ring started rotating actions generate events correctly.""" + device = zigpy_device_from_v2_quirk(manufacturer=CANDEO, model="C-ZB-SR5BR") + + cluster = device.endpoints[1].CandeoSceneSwitchRemoteCluster_Cluster + listener = mock.MagicMock() + cluster.add_listener(listener) + + cluster.send_default_rsp = mock.MagicMock() + + header = foundation.ZCLHeader() + header.command_id = ( + CandeoSceneSwitchRemoteCluster.ServerCommandDefs.candeo_scene_switch_remote.id + ) + header.frame_control = foundation.FrameControl.cluster() + + args = CandeoSceneSwitchRemoteClusterCommand( + CandeoSceneSwitchRemoteMessageType.ring_rotation, + ring_direction, + CandeoSceneSwitchRemoteRingActionMap.started_rotating, + 0x01, + ) + + cluster.handle_cluster_request(header, args) + + ring_event = listener.zha_send_event.call_args[0] + + assert ring_event[0] == COMMAND_STARTED_ROTATING + + expected_ring_direction_name = ( + LEFT + if ring_direction == CandeoSceneSwitchRemoteRingDirectionMap.left + else RIGHT + ) + + assert ring_event[1][ROTATED] == expected_ring_direction_name + + assert listener.zha_send_event.call_count == 1 + + +@pytest.mark.parametrize( + "ring_direction, ring_action", + [ + (0x99, CandeoSceneSwitchRemoteRingActionMap.started_rotating), + (CandeoSceneSwitchRemoteRingDirectionMap.right, 0x99), + ], +) +def test_CandeoSceneSwitchRemoteCluster_unknown_ring_direction_or_ring_action( + zigpy_device_from_v2_quirk, ring_direction, ring_action +): + """Test unknown ring directions and ring actions are ignored.""" + device = zigpy_device_from_v2_quirk(manufacturer=CANDEO, model="C-ZB-SR5BR") + + cluster = device.endpoints[1].CandeoSceneSwitchRemoteCluster_Cluster + listener = mock.MagicMock() + cluster.add_listener(listener) + + cluster.send_default_rsp = mock.MagicMock() + + header = foundation.ZCLHeader() + header.command_id = ( + CandeoSceneSwitchRemoteCluster.ServerCommandDefs.candeo_scene_switch_remote.id + ) + header.frame_control = foundation.FrameControl.cluster() + + args = CandeoSceneSwitchRemoteClusterCommand( + CandeoSceneSwitchRemoteMessageType.ring_rotation, + ring_direction, + ring_action, + 0x01, + ) + + cluster.handle_cluster_request(header, args) + + assert listener.zha_send_event.call_count == 0 + + +@pytest.mark.parametrize( + "ring_direction, ring_clicks", + [ + (CandeoSceneSwitchRemoteRingDirectionMap.left, 0x02), + (CandeoSceneSwitchRemoteRingDirectionMap.right, 0x03), + (CandeoSceneSwitchRemoteRingDirectionMap.left, 0x09), + (CandeoSceneSwitchRemoteRingDirectionMap.right, 0x06), + ], +) +def test_CandeoSceneSwitchRemoteCluster_ring_continued_rotating( + zigpy_device_from_v2_quirk, ring_direction, ring_clicks +): + """Test ring continued rotating actions generate events correctly.""" + device = zigpy_device_from_v2_quirk(manufacturer=CANDEO, model="C-ZB-SR5BR") + + cluster = device.endpoints[1].CandeoSceneSwitchRemoteCluster_Cluster + listener = mock.MagicMock() + cluster.add_listener(listener) + + cluster.send_default_rsp = mock.MagicMock() + + header = foundation.ZCLHeader() + header.command_id = ( + CandeoSceneSwitchRemoteCluster.ServerCommandDefs.candeo_scene_switch_remote.id + ) + header.frame_control = foundation.FrameControl.cluster() + + args = CandeoSceneSwitchRemoteClusterCommand( + CandeoSceneSwitchRemoteMessageType.ring_rotation, + ring_direction, + CandeoSceneSwitchRemoteRingActionMap.started_rotating, + ring_clicks, + ) + + cluster.handle_cluster_request(header, args) + + for x in range(0, ring_clicks, -1): + ring_event = listener.zha_send_event.call_args[x] + + expected_ring_action_name = ( + COMMAND_STARTED_ROTATING if x == 0 else COMMAND_CONTINUED_ROTATING + ) + + assert ring_event[0] == expected_ring_action_name + + expected_ring_direction_name = ( + LEFT + if ring_direction == CandeoSceneSwitchRemoteRingDirectionMap.left + else RIGHT + ) + + assert ring_event[1][ROTATED] == expected_ring_direction_name + + assert listener.zha_send_event.call_count == ring_clicks + + +def test_CandeoSceneSwitchRemoteCluster_ring_direction_and_ring_action_persistence( + zigpy_device_from_v2_quirk, +): + """Test ring continued rotating actions generate events correctly.""" + device = zigpy_device_from_v2_quirk(manufacturer=CANDEO, model="C-ZB-SR5BR") + + cluster = device.endpoints[1].CandeoSceneSwitchRemoteCluster_Cluster + listener = mock.MagicMock() + cluster.add_listener(listener) + + cluster.send_default_rsp = mock.MagicMock() + + header = foundation.ZCLHeader() + header.command_id = ( + CandeoSceneSwitchRemoteCluster.ServerCommandDefs.candeo_scene_switch_remote.id + ) + header.frame_control = foundation.FrameControl.cluster() + header.tsn = 1 + + args = CandeoSceneSwitchRemoteClusterCommand( + CandeoSceneSwitchRemoteMessageType.ring_rotation, + CandeoSceneSwitchRemoteRingDirectionMap.left, + CandeoSceneSwitchRemoteRingActionMap.started_rotating, + 0x01, + ) + + cluster.handle_cluster_request(header, args) + + assert cluster.previous_rotation_direction == LEFT + assert cluster.previous_rotation_event == COMMAND_STARTED_ROTATING + + args = CandeoSceneSwitchRemoteClusterCommand( + CandeoSceneSwitchRemoteMessageType.ring_rotation, + 0x0, + CandeoSceneSwitchRemoteRingActionMap.stopped_rotating, + 0x0, + ) + + header.tsn = 2 + + cluster.handle_cluster_request(header, args) + + assert cluster.previous_rotation_direction == LEFT + assert cluster.previous_rotation_event == COMMAND_STOPPED_ROTATING + + args = CandeoSceneSwitchRemoteClusterCommand( + CandeoSceneSwitchRemoteMessageType.ring_rotation, + CandeoSceneSwitchRemoteRingDirectionMap.right, + CandeoSceneSwitchRemoteRingActionMap.started_rotating, + 0x01, + ) + + header.tsn = 3 + + cluster.handle_cluster_request(header, args) + + assert cluster.previous_rotation_direction == RIGHT + assert cluster.previous_rotation_event == COMMAND_STARTED_ROTATING + + args = CandeoSceneSwitchRemoteClusterCommand( + CandeoSceneSwitchRemoteMessageType.ring_rotation, + 0x0, + CandeoSceneSwitchRemoteRingActionMap.stopped_rotating, + 0x0, + ) + + header.tsn = 4 + + cluster.handle_cluster_request(header, args) + + assert cluster.previous_rotation_direction == RIGHT + assert cluster.previous_rotation_event == COMMAND_STOPPED_ROTATING + + args = CandeoSceneSwitchRemoteClusterCommand( + CandeoSceneSwitchRemoteMessageType.ring_rotation, + CandeoSceneSwitchRemoteRingDirectionMap.left, + CandeoSceneSwitchRemoteRingActionMap.started_rotating, + 0x03, + ) + + header.tsn = 5 + + cluster.handle_cluster_request(header, args) + + assert cluster.previous_rotation_direction == LEFT + assert cluster.previous_rotation_event == COMMAND_CONTINUED_ROTATING + + args = CandeoSceneSwitchRemoteClusterCommand( + CandeoSceneSwitchRemoteMessageType.ring_rotation, + 0x0, + CandeoSceneSwitchRemoteRingActionMap.stopped_rotating, + 0x0, + ) + + header.tsn = 6 + + cluster.handle_cluster_request(header, args) + + assert cluster.previous_rotation_direction == LEFT + assert cluster.previous_rotation_event == COMMAND_STOPPED_ROTATING + + +@pytest.mark.parametrize( + "previous_rotation_direction, previous_rotation_event", + [ + (LEFT, COMMAND_STARTED_ROTATING), + (RIGHT, COMMAND_STARTED_ROTATING), + (LEFT, COMMAND_CONTINUED_ROTATING), + (RIGHT, COMMAND_CONTINUED_ROTATING), + ], +) +def test_CandeoSceneSwitchRemoteCluster_ring_stopped_rotating( + zigpy_device_from_v2_quirk, previous_rotation_direction, previous_rotation_event +): + """Test ring stopped rotating actions generate events correctly.""" + device = zigpy_device_from_v2_quirk(manufacturer=CANDEO, model="C-ZB-SR5BR") + + cluster = device.endpoints[1].CandeoSceneSwitchRemoteCluster_Cluster + listener = mock.MagicMock() + cluster.add_listener(listener) + + cluster.send_default_rsp = mock.MagicMock() + + cluster.previous_rotation_direction = previous_rotation_direction + cluster.previous_rotation_event = previous_rotation_event + + header = foundation.ZCLHeader() + header.command_id = ( + CandeoSceneSwitchRemoteCluster.ServerCommandDefs.candeo_scene_switch_remote.id + ) + header.frame_control = foundation.FrameControl.cluster() + + args = CandeoSceneSwitchRemoteClusterCommand( + CandeoSceneSwitchRemoteMessageType.ring_rotation, + 0x0, + CandeoSceneSwitchRemoteRingActionMap.stopped_rotating, + 0x0, + ) + + cluster.handle_cluster_request(header, args) + + ring_event = listener.zha_send_event.call_args[0] + + assert ring_event[0] == COMMAND_STOPPED_ROTATING + + assert ring_event[1][ROTATED] == previous_rotation_direction + + assert listener.zha_send_event.call_count == 1 diff --git a/zhaquirks/candeo/scene_switch_remote_5_button_rotary.py b/zhaquirks/candeo/scene_switch_remote_5_button_rotary.py new file mode 100644 index 0000000000..9b2f0f7176 --- /dev/null +++ b/zhaquirks/candeo/scene_switch_remote_5_button_rotary.py @@ -0,0 +1,353 @@ +"""Candeo c-zb-sr5br 5-button remote with rotating dial.""" + +from typing import Final, Optional, Union + +from zigpy.quirks import CustomCluster +from zigpy.quirks.v2 import QuirkBuilder +import zigpy.types as t +from zigpy.zcl import foundation +from zigpy.zcl.foundation import BaseCommandDefs, ZCLCommandDef + +from zhaquirks.candeo import CANDEO +from zhaquirks.const import ( + ARGS, + BUTTON, + BUTTON_1, + BUTTON_2, + BUTTON_3, + BUTTON_4, + BUTTON_CENTRE, + COMMAND, + COMMAND_CONTINUED_ROTATING, + COMMAND_DOUBLE, + COMMAND_HOLD, + COMMAND_PRESS, + COMMAND_RELEASE, + COMMAND_STARTED_ROTATING, + COMMAND_STOPPED_ROTATING, + CONTINUED_ROTATING, + DOUBLE_PRESS, + ENDPOINT_ID, + LEFT, + LONG_PRESS, + LONG_RELEASE, + RIGHT, + ROTATED, + SHORT_PRESS, + STARTED_ROTATING, + STOPPED_ROTATING_WITH_DIRECTION, + ZHA_SEND_EVENT, +) + + +class CandeoSceneSwitchRemoteMessageType(t.enum8): + """Candeo Scene Switch Remote Message Type.""" + + button_press = 0x01 + ring_rotation = 0x03 + + +class CandeoSceneSwitchRemoteButtonNumberMap(t.enum8): + """Candeo Scene Switch Remote Button Number Map.""" + + button_1 = 0x01 + button_2 = 0x02 + button_3 = 0x04 + button_4 = 0x08 + button_centre = 0x10 + + +class CandeoSceneSwitchRemoteButtonActionMap(t.enum8): + """Candeo Scene Switch Remote Button Action Map.""" + + press = 0x01 + double = 0x02 + hold = 0x03 + release = 0x04 + + +class CandeoSceneSwitchRemoteRingDirectionMap(t.enum8): + """Candeo Scene Switch Remote Ring Direction Map.""" + + right = 0x01 + left = 0x02 + + +class CandeoSceneSwitchRemoteRingActionMap(t.enum8): + """Candeo Scene Switch Remote Ring Action Map.""" + + started_rotating = 0x01 + stopped_rotating = 0x02 + continued_rotating = 0x03 + + +class CandeoSceneSwitchRemoteClusterCommand(t.Struct): + """CandeoSceneSwitchRemoteClusterCommand.""" + + message_type: CandeoSceneSwitchRemoteMessageType + field_1: t.uint8_t + field_2: t.uint8_t + field_3: t.uint8_t + + +class CandeoSceneSwitchRemoteCluster(CustomCluster): + """CandeoSceneSwitchRemoteCluster: fire events corresponding to button press or ring rotation.""" + + cluster_id: Final[t.uint16_t] = 0xFF03 + name = "CandeoSceneSwitchRemoteCluster_Cluster" + ep_attribute = "CandeoSceneSwitchRemoteCluster_Cluster" + + class ServerCommandDefs(BaseCommandDefs): + """overwrite ServerCommandDefs.""" + + candeo_scene_switch_remote: Final = ZCLCommandDef( + id=0x01, + schema=CandeoSceneSwitchRemoteClusterCommand, + is_manufacturer_specific=True, + ) + + async def apply_custom_configuration(self, *args, **kwargs): + """Apply custom configuration to bind cluster.""" + await self.bind() + + def __init__(self, *args, **kwargs): + """__init___.""" + self.last_tsn = -1 + self.previous_rotation_direction = "unknown" + self.previous_rotation_event = COMMAND_STOPPED_ROTATING + super().__init__(*args, **kwargs) + + def handle_cluster_request( + self, + hdr: foundation.ZCLHeader, + args: tuple[CandeoSceneSwitchRemoteClusterCommand], + *, + dst_addressing: Optional[ + Union[t.Addressing.Group, t.Addressing.IEEE, t.Addressing.NWK] + ] = None, + ): + """Overwrite handle_cluster_request to custom process this cluster.""" + if not hdr.frame_control.disable_default_response: + self.send_default_rsp(hdr, status=foundation.Status.SUCCESS) + if hdr.tsn == self.last_tsn: + return + self.last_tsn = hdr.tsn + if ( + hdr.command_id == self.ServerCommandDefs.candeo_scene_switch_remote.id + and args.message_type is not None + and args.field_1 is not None + and args.field_2 is not None + and args.field_3 is not None + ): + if ( + args.message_type == CandeoSceneSwitchRemoteMessageType.button_press + and args.field_2 + in CandeoSceneSwitchRemoteButtonNumberMap._value2member_map_ + and args.field_3 + in CandeoSceneSwitchRemoteButtonActionMap._value2member_map_ + ): + button_number = CandeoSceneSwitchRemoteButtonNumberMap( + args.field_2 + ).name + button_action = CandeoSceneSwitchRemoteButtonActionMap( + args.field_3 + ).name + self.listener_event( + ZHA_SEND_EVENT, button_action, {BUTTON: button_number} + ) + elif ( + args.message_type == CandeoSceneSwitchRemoteMessageType.ring_rotation + and args.field_2 + in CandeoSceneSwitchRemoteRingActionMap._value2member_map_ + ): + ring_action = CandeoSceneSwitchRemoteRingActionMap(args.field_2).name + if ring_action == COMMAND_STOPPED_ROTATING: + if self.previous_rotation_direction != "unknown": + self.listener_event( + ZHA_SEND_EVENT, + COMMAND_STOPPED_ROTATING, + {ROTATED: self.previous_rotation_direction}, + ) + self.previous_rotation_event = COMMAND_STOPPED_ROTATING + elif ( + args.field_1 + in CandeoSceneSwitchRemoteRingDirectionMap._value2member_map_ + ): + ring_direction = CandeoSceneSwitchRemoteRingDirectionMap( + args.field_1 + ).name + ring_clicks = args.field_3 + if self.previous_rotation_event == COMMAND_STOPPED_ROTATING: + self.listener_event( + ZHA_SEND_EVENT, + COMMAND_STARTED_ROTATING, + {ROTATED: ring_direction}, + ) + self.previous_rotation_event = COMMAND_STARTED_ROTATING + if ring_clicks > 1: + for _x in range(1, ring_clicks): + self.listener_event( + ZHA_SEND_EVENT, + COMMAND_CONTINUED_ROTATING, + {ROTATED: ring_direction}, + ) + self.previous_rotation_event = COMMAND_CONTINUED_ROTATING + elif self.previous_rotation_event in { + COMMAND_STARTED_ROTATING, + COMMAND_CONTINUED_ROTATING, + }: + self.listener_event( + ZHA_SEND_EVENT, + COMMAND_CONTINUED_ROTATING, + {ROTATED: ring_direction}, + ) + if ring_clicks > 1: + for _x in range(1, ring_clicks): + self.listener_event( + ZHA_SEND_EVENT, + COMMAND_CONTINUED_ROTATING, + {ROTATED: ring_direction}, + ) + self.previous_rotation_event = COMMAND_CONTINUED_ROTATING + self.previous_rotation_direction = ring_direction + + +( + QuirkBuilder(CANDEO, "C-ZB-SR5BR") + .replaces(CandeoSceneSwitchRemoteCluster) + .device_automation_triggers( + { + (SHORT_PRESS, BUTTON_1): { + ENDPOINT_ID: 1, + COMMAND: COMMAND_PRESS, + ARGS: {BUTTON: BUTTON_1}, + }, + (DOUBLE_PRESS, BUTTON_1): { + ENDPOINT_ID: 1, + COMMAND: COMMAND_DOUBLE, + ARGS: {BUTTON: BUTTON_1}, + }, + (LONG_PRESS, BUTTON_1): { + ENDPOINT_ID: 1, + COMMAND: COMMAND_HOLD, + ARGS: {BUTTON: BUTTON_1}, + }, + (LONG_RELEASE, BUTTON_1): { + ENDPOINT_ID: 1, + COMMAND: COMMAND_RELEASE, + ARGS: {BUTTON: BUTTON_1}, + }, + (SHORT_PRESS, BUTTON_2): { + ENDPOINT_ID: 1, + COMMAND: COMMAND_PRESS, + ARGS: {BUTTON: BUTTON_2}, + }, + (DOUBLE_PRESS, BUTTON_2): { + ENDPOINT_ID: 1, + COMMAND: COMMAND_DOUBLE, + ARGS: {BUTTON: BUTTON_2}, + }, + (LONG_PRESS, BUTTON_2): { + ENDPOINT_ID: 1, + COMMAND: COMMAND_HOLD, + ARGS: {BUTTON: BUTTON_2}, + }, + (LONG_RELEASE, BUTTON_2): { + ENDPOINT_ID: 1, + COMMAND: COMMAND_RELEASE, + ARGS: {BUTTON: BUTTON_2}, + }, + (SHORT_PRESS, BUTTON_3): { + ENDPOINT_ID: 1, + COMMAND: COMMAND_PRESS, + ARGS: {BUTTON: BUTTON_3}, + }, + (DOUBLE_PRESS, BUTTON_3): { + ENDPOINT_ID: 1, + COMMAND: COMMAND_DOUBLE, + ARGS: {BUTTON: BUTTON_3}, + }, + (LONG_PRESS, BUTTON_3): { + ENDPOINT_ID: 1, + COMMAND: COMMAND_HOLD, + ARGS: {BUTTON: BUTTON_3}, + }, + (LONG_RELEASE, BUTTON_3): { + ENDPOINT_ID: 1, + COMMAND: COMMAND_RELEASE, + ARGS: {BUTTON: BUTTON_3}, + }, + (SHORT_PRESS, BUTTON_4): { + ENDPOINT_ID: 1, + COMMAND: COMMAND_PRESS, + ARGS: {BUTTON: BUTTON_4}, + }, + (DOUBLE_PRESS, BUTTON_4): { + ENDPOINT_ID: 1, + COMMAND: COMMAND_DOUBLE, + ARGS: {BUTTON: BUTTON_4}, + }, + (LONG_PRESS, BUTTON_4): { + ENDPOINT_ID: 1, + COMMAND: COMMAND_HOLD, + ARGS: {BUTTON: BUTTON_4}, + }, + (LONG_RELEASE, BUTTON_4): { + ENDPOINT_ID: 1, + COMMAND: COMMAND_RELEASE, + ARGS: {BUTTON: BUTTON_4}, + }, + (SHORT_PRESS, BUTTON_CENTRE): { + ENDPOINT_ID: 1, + COMMAND: COMMAND_PRESS, + ARGS: {BUTTON: BUTTON_CENTRE}, + }, + (DOUBLE_PRESS, BUTTON_CENTRE): { + ENDPOINT_ID: 1, + COMMAND: COMMAND_DOUBLE, + ARGS: {BUTTON: BUTTON_CENTRE}, + }, + (LONG_PRESS, BUTTON_CENTRE): { + ENDPOINT_ID: 1, + COMMAND: COMMAND_HOLD, + ARGS: {BUTTON: BUTTON_CENTRE}, + }, + (LONG_RELEASE, BUTTON_CENTRE): { + ENDPOINT_ID: 1, + COMMAND: COMMAND_RELEASE, + ARGS: {BUTTON: BUTTON_CENTRE}, + }, + (STARTED_ROTATING, LEFT): { + ENDPOINT_ID: 1, + COMMAND: COMMAND_STARTED_ROTATING, + ARGS: {ROTATED: LEFT}, + }, + (CONTINUED_ROTATING, LEFT): { + ENDPOINT_ID: 1, + COMMAND: COMMAND_CONTINUED_ROTATING, + ARGS: {ROTATED: LEFT}, + }, + (STOPPED_ROTATING_WITH_DIRECTION, LEFT): { + ENDPOINT_ID: 1, + COMMAND: COMMAND_STOPPED_ROTATING, + ARGS: {ROTATED: LEFT}, + }, + (STARTED_ROTATING, RIGHT): { + ENDPOINT_ID: 1, + COMMAND: COMMAND_STARTED_ROTATING, + ARGS: {ROTATED: RIGHT}, + }, + (CONTINUED_ROTATING, RIGHT): { + ENDPOINT_ID: 1, + COMMAND: COMMAND_CONTINUED_ROTATING, + ARGS: {ROTATED: RIGHT}, + }, + (STOPPED_ROTATING_WITH_DIRECTION, RIGHT): { + ENDPOINT_ID: 1, + COMMAND: COMMAND_STOPPED_ROTATING, + ARGS: {ROTATED: RIGHT}, + }, + } + ) + .add_to_registry() +) diff --git a/zhaquirks/const.py b/zhaquirks/const.py index 12bd29a5fc..7faddd2e51 100644 --- a/zhaquirks/const.py +++ b/zhaquirks/const.py @@ -25,6 +25,7 @@ BUTTON_4 = "button_4" BUTTON_5 = "button_5" BUTTON_6 = "button_6" +BUTTON_CENTRE = "button_centre" CLICK_TYPE = "click_type" CLOSE = "close" CLUSTER_COMMAND = "cluster_command" @@ -133,6 +134,7 @@ STARTED_ROTATING = "rotary_knob_started_rotating" CONTINUED_ROTATING = "rotary_knob_continued_rotating" STOPPED_ROTATING = "rotary_knob_stopped_rotating" +STOPPED_ROTATING_WITH_DIRECTION = "rotary_knob_stopped_rotating_with_direction" class BatterySize(t.enum8):