diff --git a/tests/test_third_button.py b/tests/test_third_button.py new file mode 100644 index 0000000000..23f9d13a47 --- /dev/null +++ b/tests/test_third_button.py @@ -0,0 +1,76 @@ +"""Test the module for the "third button" function to verify the ZHA event capture logic.""" + +import pytest + +import zhaquirks +from zhaquirks.thirdreality.button_v2 import MultistateInputCluster + + +class MockListener: + """Simulate listener class for capturing ZHA events.""" + + def __init__(self): + """Initialize listener with empty event list.""" + self.zha_send_events = [] + + def zha_send_event(self, action, event_args): + """Record ZHA events. + + Args: + action (str): The type of action for the event. + event_args (dict): Relevant parameters of the event. + + """ + self.zha_send_events.append((action, event_args)) + + +zhaquirks.setup() + + +@pytest.mark.parametrize( + "manufacturer, model", + [("Third Reality, Inc", "3RSB22BZ")], +) +async def test_third_reality_button_v2(zigpy_device_from_v2_quirk, manufacturer, model): + """Test Third Reality button event conversion and triggering functionality.""" + # Create mock device based on the v2 quirk + device = zigpy_device_from_v2_quirk(manufacturer, model) + + # Find the MultistateInputCluster + multistate_cluster = next( + ( + cluster + for cluster in device.endpoints[1].in_clusters.values() + if isinstance(cluster, MultistateInputCluster) + ), + None, + ) + assert multistate_cluster is not None, "MultistateInputCluster not found" + + # Create mock listener and register it with the cluster + mock_listener = MockListener() + multistate_cluster.add_listener(mock_listener) + + # Test 1: Verify single click event conversion + mock_listener.zha_send_events.clear() + multistate_cluster.update_attribute(0x0055, 1) # 1 corresponds to single click + assert len(mock_listener.zha_send_events) == 1 + assert mock_listener.zha_send_events[0][0] == "single" + + # Test 2: Verify double click event conversion + mock_listener.zha_send_events.clear() + multistate_cluster.update_attribute(0x0055, 2) # 2 corresponds to double click + assert len(mock_listener.zha_send_events) == 1 + assert mock_listener.zha_send_events[0][0] == "double" + + # Test 3: Verify hold event conversion + mock_listener.zha_send_events.clear() + multistate_cluster.update_attribute(0x0055, 0) # 0 corresponds to hold + assert len(mock_listener.zha_send_events) == 1 + assert mock_listener.zha_send_events[0][0] == "hold" + + # Test 4: Verify release event conversion + mock_listener.zha_send_events.clear() + multistate_cluster.update_attribute(0x0055, 255) # 255 corresponds to release + assert len(mock_listener.zha_send_events) == 1 + assert mock_listener.zha_send_events[0][0] == "release" diff --git a/zhaquirks/thirdreality/button_v2.py b/zhaquirks/thirdreality/button_v2.py new file mode 100644 index 0000000000..e0b51efff7 --- /dev/null +++ b/zhaquirks/thirdreality/button_v2.py @@ -0,0 +1,92 @@ +"""Third Reality button devices.""" + +from typing import Final + +from zigpy.quirks import CustomCluster +from zigpy.quirks.v2 import QuirkBuilder +import zigpy.types as t +from zigpy.zcl.clusters.general import MultistateInput +from zigpy.zcl.foundation import BaseAttributeDefs, ZCLAttributeDef + +from zhaquirks.const import ( + COMMAND, + COMMAND_DOUBLE, + COMMAND_HOLD, + COMMAND_RELEASE, + COMMAND_SINGLE, + DOUBLE_PRESS, + LONG_PRESS, + LONG_RELEASE, + SHORT_PRESS, + VALUE, + ZHA_SEND_EVENT, +) + +MOVEMENT_TYPE = { + 0: COMMAND_HOLD, + 1: COMMAND_SINGLE, + 2: COMMAND_DOUBLE, + 255: COMMAND_RELEASE, +} + + +class MultistateInputCluster(CustomCluster, MultistateInput): + """Multistate input cluster.""" + + def __init__(self, *args, **kwargs): + """Init.""" + self._current_state = {} + super().__init__(*args, **kwargs) + + def _update_attribute(self, attrid, value): + super()._update_attribute(attrid, value) + if attrid == 0x0055: + self._current_state[0x0055] = action = MOVEMENT_TYPE.get(value) + event_args = {VALUE: value} + if action is not None: + self.listener_event(ZHA_SEND_EVENT, action, event_args) + + # show something in the sensor in HA + super()._update_attribute(0, action) + + +class ThirdRealityButtonCluster(CustomCluster): + """Third Reality's button private cluster.""" + + cluster_id = 0xFF01 + + class AttributeDefs(BaseAttributeDefs): + """Define the attributes of a private cluster.""" + + # cancel double click + cancel_bouble_click: Final = ZCLAttributeDef( + id=0x0000, + type=t.uint8_t, + is_manufacturer_specific=True, + ) + + +( + QuirkBuilder("Third Reality, Inc", "3RSB22BZ") + .replaces(ThirdRealityButtonCluster) + .replaces(MultistateInputCluster) + .number( + attribute_name=ThirdRealityButtonCluster.AttributeDefs.cancel_bouble_click.name, + cluster_id=ThirdRealityButtonCluster.cluster_id, + endpoint_id=1, + min_value=0, + max_value=65535, + step=1, + translation_key="cancel_bouble_click", + fallback_name="Cancel double click", + ) + .device_automation_triggers( + { + (DOUBLE_PRESS, DOUBLE_PRESS): {COMMAND: COMMAND_DOUBLE}, + (SHORT_PRESS, SHORT_PRESS): {COMMAND: COMMAND_SINGLE}, + (LONG_PRESS, LONG_PRESS): {COMMAND: COMMAND_HOLD}, + (LONG_RELEASE, LONG_RELEASE): {COMMAND: COMMAND_RELEASE}, + } + ) + .add_to_registry() +)