diff --git a/tests/test_sinope.py b/tests/test_sinope.py index 0680d45163..3f377a858d 100644 --- a/tests/test_sinope.py +++ b/tests/test_sinope.py @@ -11,7 +11,14 @@ from tests.common import ClusterListener import zhaquirks -from zhaquirks.const import COMMAND_BUTTON_DOUBLE, COMMAND_BUTTON_HOLD +from zhaquirks.const import ( + COMMAND_M_INITIAL_PRESS, + COMMAND_M_LONG_RELEASE, + COMMAND_M_MULTI_PRESS_COMPLETE, + COMMAND_M_SHORT_RELEASE, + TURN_OFF, + TURN_ON, +) from zhaquirks.sinope import SINOPE_MANUFACTURER_CLUSTER_ID from zhaquirks.sinope.light import ( SinopeTechnologieslight, @@ -92,20 +99,22 @@ def _get_packet_data( @pytest.mark.parametrize("quirk", (SinopeTechnologieslight,)) @pytest.mark.parametrize( - "press_type,exp_event", + "press_type,button,exp_event", ( - (ButtonAction.Single_off, None), - (ButtonAction.Single_on, None), - (ButtonAction.Double_on, COMMAND_BUTTON_DOUBLE), - (ButtonAction.Double_off, COMMAND_BUTTON_DOUBLE), - (ButtonAction.Long_on, COMMAND_BUTTON_HOLD), - (ButtonAction.Long_off, COMMAND_BUTTON_HOLD), + (ButtonAction.Pressed_off, TURN_OFF, COMMAND_M_INITIAL_PRESS), + (ButtonAction.Pressed_on, TURN_ON, COMMAND_M_INITIAL_PRESS), + (ButtonAction.Released_off, TURN_OFF, COMMAND_M_SHORT_RELEASE), + (ButtonAction.Released_on, TURN_ON, COMMAND_M_SHORT_RELEASE), + (ButtonAction.Double_on, TURN_ON, COMMAND_M_MULTI_PRESS_COMPLETE), + (ButtonAction.Double_off, TURN_OFF, COMMAND_M_MULTI_PRESS_COMPLETE), + (ButtonAction.Long_on, TURN_ON, COMMAND_M_LONG_RELEASE), + (ButtonAction.Long_off, TURN_OFF, COMMAND_M_LONG_RELEASE), # Should gracefully handle broken actions. - (t.uint8_t(0x00), None), + (t.uint8_t(0x00), None, None), ), ) async def test_sinope_light_switch( - zigpy_device_from_quirk, quirk, press_type, exp_event + zigpy_device_from_quirk, quirk, press_type, button, exp_event ): """Test that button presses are sent as events.""" device: Device = zigpy_device_from_quirk(quirk) @@ -126,7 +135,16 @@ class Listener: ), ) data = _get_packet_data(foundation.GeneralCommand.Report_Attributes, attr) - device.handle_message(260, cluster_id, endpoint_id, endpoint_id, data) + + device.packet_received( + t.ZigbeePacket( + profile_id=260, + cluster_id=cluster_id, + src_ep=endpoint_id, + dst_ep=endpoint_id, + data=t.SerializableBytes(data), + ) + ) if exp_event is None: assert cluster_listener.zha_send_event.call_count == 0 @@ -137,6 +155,8 @@ class Listener: { "attribute_id": 84, "attribute_name": "action_report", + "button": button, + "description": press_type.name, "value": press_type.value, }, ) @@ -162,7 +182,15 @@ class Listener: # read attributes general command data = _get_packet_data(foundation.GeneralCommand.Read_Attributes) - device.handle_message(260, cluster_id, endpoint_id, endpoint_id, data) + device.packet_received( + t.ZigbeePacket( + profile_id=260, + cluster_id=cluster_id, + src_ep=endpoint_id, + dst_ep=endpoint_id, + data=t.SerializableBytes(data), + ) + ) # no ZHA events emitted because we only handle Report_Attributes assert cluster_listener.zha_send_event.call_count == 0 @@ -174,7 +202,15 @@ class Listener: ), # 0x29 = t.int16s ) data = _get_packet_data(foundation.GeneralCommand.Report_Attributes, attr) - device.handle_message(260, cluster_id, endpoint_id, endpoint_id, data) + device.packet_received( + t.ZigbeePacket( + profile_id=260, + cluster_id=cluster_id, + src_ep=endpoint_id, + dst_ep=endpoint_id, + data=t.SerializableBytes(data), + ) + ) # ZHA event emitted because we pass non "action_report" # reports to the base class handler. assert cluster_listener.zha_send_event.call_count == 1 diff --git a/zhaquirks/sinope/__init__.py b/zhaquirks/sinope/__init__.py index 1a2bffbb94..ff59f319da 100644 --- a/zhaquirks/sinope/__init__.py +++ b/zhaquirks/sinope/__init__.py @@ -1,21 +1,25 @@ """Module for Sinope quirks implementations.""" from zigpy.quirks import CustomCluster +import zigpy.types as t from zigpy.zcl.clusters.general import DeviceTemperature from zhaquirks.const import ( ARGS, ATTRIBUTE_ID, ATTRIBUTE_NAME, + BUTTON, CLUSTER_ID, COMMAND, - COMMAND_BUTTON_DOUBLE, - COMMAND_BUTTON_HOLD, - COMMAND_BUTTON_SINGLE, + COMMAND_M_INITIAL_PRESS, + COMMAND_M_LONG_RELEASE, + COMMAND_M_MULTI_PRESS_COMPLETE, + COMMAND_M_SHORT_RELEASE, DOUBLE_PRESS, ENDPOINT_ID, LONG_PRESS, SHORT_PRESS, + SHORT_RELEASE, TURN_OFF, TURN_ON, VALUE, @@ -25,42 +29,108 @@ SINOPE_MANUFACTURER_CLUSTER_ID = 0xFF01 ATTRIBUTE_ACTION = "action_report" + +class ButtonAction(t.enum8): + """Action_report values.""" + + Pressed_on = 0x01 + Released_on = 0x02 + Long_on = 0x03 + Double_on = 0x04 + Pressed_off = 0x11 + Released_off = 0x12 + Long_off = 0x13 + Double_off = 0x14 + + LIGHT_DEVICE_TRIGGERS = { (SHORT_PRESS, TURN_ON): { ENDPOINT_ID: 1, CLUSTER_ID: 65281, - COMMAND: COMMAND_BUTTON_SINGLE, - ARGS: {ATTRIBUTE_ID: 84, ATTRIBUTE_NAME: ATTRIBUTE_ACTION, VALUE: 2}, + COMMAND: COMMAND_M_INITIAL_PRESS, + ARGS: { + ATTRIBUTE_ID: 84, + ATTRIBUTE_NAME: ATTRIBUTE_ACTION, + BUTTON: TURN_ON, + VALUE: ButtonAction.Pressed_on, + }, }, (SHORT_PRESS, TURN_OFF): { ENDPOINT_ID: 1, CLUSTER_ID: 65281, - COMMAND: COMMAND_BUTTON_SINGLE, - ARGS: {ATTRIBUTE_ID: 84, ATTRIBUTE_NAME: ATTRIBUTE_ACTION, VALUE: 18}, + COMMAND: COMMAND_M_INITIAL_PRESS, + ARGS: { + ATTRIBUTE_ID: 84, + ATTRIBUTE_NAME: ATTRIBUTE_ACTION, + BUTTON: TURN_OFF, + VALUE: ButtonAction.Pressed_off, + }, + }, + (SHORT_RELEASE, TURN_ON): { + ENDPOINT_ID: 1, + CLUSTER_ID: 65281, + COMMAND: COMMAND_M_SHORT_RELEASE, + ARGS: { + ATTRIBUTE_ID: 84, + ATTRIBUTE_NAME: ATTRIBUTE_ACTION, + BUTTON: TURN_ON, + VALUE: ButtonAction.Released_on, + }, + }, + (SHORT_RELEASE, TURN_OFF): { + ENDPOINT_ID: 1, + CLUSTER_ID: 65281, + COMMAND: COMMAND_M_SHORT_RELEASE, + ARGS: { + ATTRIBUTE_ID: 84, + ATTRIBUTE_NAME: ATTRIBUTE_ACTION, + BUTTON: TURN_OFF, + VALUE: ButtonAction.Released_off, + }, }, (DOUBLE_PRESS, TURN_ON): { ENDPOINT_ID: 1, CLUSTER_ID: 65281, - COMMAND: COMMAND_BUTTON_DOUBLE, - ARGS: {ATTRIBUTE_ID: 84, ATTRIBUTE_NAME: ATTRIBUTE_ACTION, VALUE: 4}, + COMMAND: COMMAND_M_MULTI_PRESS_COMPLETE, + ARGS: { + ATTRIBUTE_ID: 84, + ATTRIBUTE_NAME: ATTRIBUTE_ACTION, + BUTTON: TURN_ON, + VALUE: ButtonAction.Double_on, + }, }, (DOUBLE_PRESS, TURN_OFF): { ENDPOINT_ID: 1, CLUSTER_ID: 65281, - COMMAND: COMMAND_BUTTON_DOUBLE, - ARGS: {ATTRIBUTE_ID: 84, ATTRIBUTE_NAME: ATTRIBUTE_ACTION, VALUE: 20}, + COMMAND: COMMAND_M_MULTI_PRESS_COMPLETE, + ARGS: { + ATTRIBUTE_ID: 84, + ATTRIBUTE_NAME: ATTRIBUTE_ACTION, + BUTTON: TURN_OFF, + VALUE: ButtonAction.Double_off, + }, }, (LONG_PRESS, TURN_ON): { ENDPOINT_ID: 1, CLUSTER_ID: 65281, - COMMAND: COMMAND_BUTTON_HOLD, - ARGS: {ATTRIBUTE_ID: 84, ATTRIBUTE_NAME: ATTRIBUTE_ACTION, VALUE: 3}, + COMMAND: COMMAND_M_LONG_RELEASE, + ARGS: { + ATTRIBUTE_ID: 84, + ATTRIBUTE_NAME: ATTRIBUTE_ACTION, + BUTTON: TURN_ON, + VALUE: ButtonAction.Long_on, + }, }, (LONG_PRESS, TURN_OFF): { ENDPOINT_ID: 1, CLUSTER_ID: 65281, - COMMAND: COMMAND_BUTTON_HOLD, - ARGS: {ATTRIBUTE_ID: 84, ATTRIBUTE_NAME: ATTRIBUTE_ACTION, VALUE: 19}, + COMMAND: COMMAND_M_LONG_RELEASE, + ARGS: { + ATTRIBUTE_ID: 84, + ATTRIBUTE_NAME: ATTRIBUTE_ACTION, + BUTTON: TURN_OFF, + VALUE: ButtonAction.Long_off, + }, }, } diff --git a/zhaquirks/sinope/light.py b/zhaquirks/sinope/light.py index a95d4b35a9..328315f5bb 100644 --- a/zhaquirks/sinope/light.py +++ b/zhaquirks/sinope/light.py @@ -29,14 +29,20 @@ from zhaquirks.const import ( ATTRIBUTE_ID, ATTRIBUTE_NAME, - COMMAND_BUTTON_DOUBLE, - COMMAND_BUTTON_HOLD, + BUTTON, + COMMAND_M_INITIAL_PRESS, + COMMAND_M_LONG_RELEASE, + COMMAND_M_MULTI_PRESS_COMPLETE, + COMMAND_M_SHORT_RELEASE, + DESCRIPTION, DEVICE_TYPE, ENDPOINTS, INPUT_CLUSTERS, MODELS_INFO, OUTPUT_CLUSTERS, PROFILE_ID, + TURN_OFF, + TURN_ON, VALUE, ZHA_SEND_EVENT, ) @@ -45,6 +51,7 @@ LIGHT_DEVICE_TRIGGERS, SINOPE, SINOPE_MANUFACTURER_CLUSTER_ID, + ButtonAction, CustomDeviceTemperatureCluster, ) @@ -73,19 +80,6 @@ class DoubleFull(t.enum8): On = 0x01 -class ButtonAction(t.enum8): - """Action_report values.""" - - Single_on = 0x01 - Single_release_on = 0x02 - Long_on = 0x03 - Double_on = 0x04 - Single_off = 0x11 - Single_release_off = 0x12 - Long_off = 0x13 - Double_off = 0x14 - - class SinopeTechnologiesManufacturerCluster(CustomCluster): """SinopeTechnologiesManufacturerCluster manufacturer cluster.""" @@ -191,29 +185,53 @@ def handle_cluster_general_request( hdr, args, dst_addressing=dst_addressing ) - value = attr.value.value + action = self.Action(attr.value.value) + + command, button = self._get_command_from_action(action) + if not command or not button: + return + event_args = { ATTRIBUTE_ID: 84, ATTRIBUTE_NAME: ATTRIBUTE_ACTION, - VALUE: value.value, + BUTTON: button, + DESCRIPTION: action.name, + VALUE: action.value, } - action = self._get_command_from_action(self.Action(value)) - if not action: - return - self.listener_event(ZHA_SEND_EVENT, action, event_args) - def _get_command_from_action(self, action: ButtonAction) -> str | None: - # const lookup = {2: 'up_single', 3: 'up_hold', 4: 'up_double', - # 18: 'down_single', 19: 'down_hold', 20: 'down_double'}; + self.debug( + "SINOPE ZHA_SEND_EVENT command: '%s' event_args: %s", + command, + event_args, + ) + + self.listener_event(ZHA_SEND_EVENT, command, event_args) + + def _get_command_from_action( + self, action: ButtonAction + ) -> tuple[str | None, str | None]: + # const lookup = {1: 'up_single', 2: 'up_single_released', 3: 'up_hold', 4: 'up_double', + # 17: 'down_single, 18: 'down_single_released', 19: 'down_hold', 20: 'down_double'}; match action: - case self.Action.Single_off | self.Action.Single_on: - return None - case self.Action.Double_off | self.Action.Double_on: - return COMMAND_BUTTON_DOUBLE - case self.Action.Long_off | self.Action.Long_on: - return COMMAND_BUTTON_HOLD + case self.Action.Pressed_off: + return COMMAND_M_INITIAL_PRESS, TURN_OFF + case self.Action.Pressed_on: + return COMMAND_M_INITIAL_PRESS, TURN_ON + case self.Action.Released_off: + return COMMAND_M_SHORT_RELEASE, TURN_OFF + case self.Action.Released_on: + return COMMAND_M_SHORT_RELEASE, TURN_ON + case self.Action.Double_off: + return COMMAND_M_MULTI_PRESS_COMPLETE, TURN_OFF + case self.Action.Double_on: + return COMMAND_M_MULTI_PRESS_COMPLETE, TURN_ON + case self.Action.Long_off: + return COMMAND_M_LONG_RELEASE, TURN_OFF + case self.Action.Long_on: + return COMMAND_M_LONG_RELEASE, TURN_ON case _: - return None + self.debug("SINOPE unhandled action: %s", action) + return None, None class LightManufacturerCluster(EventableCluster, SinopeTechnologiesManufacturerCluster):