diff --git a/tests/test_quirks.py b/tests/test_quirks.py index 639bbd0fd4..0696fa4299 100644 --- a/tests/test_quirks.py +++ b/tests/test_quirks.py @@ -661,12 +661,6 @@ def test_migrated_lighting_automation_triggers(quirk: CustomDevice) -> None: (const.LONG_RELEASE, const.BUTTON_4), ], ], - zhaquirks.thirdreality.button.Button: [ - [ - (const.LONG_PRESS, const.LONG_PRESS), - (const.LONG_RELEASE, const.LONG_RELEASE), - ] - ], } diff --git a/tests/test_thirdreality.py b/tests/test_thirdreality.py index 6d0f31fe71..36beef1f7e 100644 --- a/tests/test_thirdreality.py +++ b/tests/test_thirdreality.py @@ -1,10 +1,13 @@ """Tests for Third Reality quirks.""" +from unittest import mock + import pytest from zigpy.zcl.clusters.security import IasZone from tests.common import ClusterListener import zhaquirks +from zhaquirks.thirdreality.button import MultistateInputCluster import zhaquirks.thirdreality.night_light zhaquirks.setup() @@ -36,3 +39,38 @@ async def test_third_reality_nightlight(zigpy_device_from_quirk, quirk): assert len(ias_zone_listener.attribute_updates) == 2 assert ias_zone_listener.attribute_updates[1][0] == ias_zone_status_id assert ias_zone_listener.attribute_updates[1][1] == 0 + + +@pytest.mark.parametrize( + ("attr_value", "expected_action"), + [ + (1, "single"), # 1 corresponds to single click + (2, "double"), # 2 corresponds to double click + (0, "hold"), # 0 corresponds to hold + (255, "release"), # 255 corresponds to release + ], +) +@pytest.mark.parametrize( + ("manufacturer", "model"), + [("Third Reality, Inc", "3RSB22BZ")], +) +async def test_third_reality_button_v2( + zigpy_device_from_v2_quirk, manufacturer, model, attr_value, expected_action +): + """Test Third Reality button event conversion and triggering functionality.""" + device = zigpy_device_from_v2_quirk(manufacturer, model) + multistate_cluster = device.endpoints[1].in_clusters[ + MultistateInputCluster.cluster_id + ] + + # Create mock listener and register it with the cluster + listener = mock.MagicMock() + multistate_cluster.add_listener(listener) + + multistate_cluster.update_attribute( + 0x0055, attr_value + ) # 1 corresponds to single click + assert listener.zha_send_event.call_count == 1 + assert listener.zha_send_event.call_args_list[0] == mock.call( + expected_action, {"value": attr_value} + ) diff --git a/zhaquirks/thirdreality/button.py b/zhaquirks/thirdreality/button.py index e5c54f77c1..1a596e1955 100644 --- a/zhaquirks/thirdreality/button.py +++ b/zhaquirks/thirdreality/button.py @@ -1,41 +1,28 @@ """Third Reality button devices.""" -from zigpy.profiles import zha -from zigpy.quirks import CustomDevice -from zigpy.zcl.clusters.general import Basic, LevelControl, MultistateInput, OnOff, Ota +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 import CustomCluster, PowerConfigurationCluster from zhaquirks.const import ( COMMAND, COMMAND_DOUBLE, COMMAND_HOLD, COMMAND_RELEASE, COMMAND_SINGLE, - DEVICE_TYPE, DOUBLE_PRESS, - ENDPOINTS, - INPUT_CLUSTERS, LONG_PRESS, LONG_RELEASE, - MODELS_INFO, - OUTPUT_CLUSTERS, - PROFILE_ID, SHORT_PRESS, - SKIP_CONFIGURATION, VALUE, ZHA_SEND_EVENT, ) -from zhaquirks.thirdreality import THIRD_REALITY - - -class CustomPowerConfigurationCluster(PowerConfigurationCluster): - """Custom PowerConfigurationCluster.""" - MIN_VOLTS = 2.1 - MAX_VOLTS = 3.0 - - -MOVEMENT_TYPE = { +PRESS_TYPE = { 0: COMMAND_HOLD, 1: COMMAND_SINGLE, 2: COMMAND_DOUBLE, @@ -46,67 +33,48 @@ class CustomPowerConfigurationCluster(PowerConfigurationCluster): 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 Button(CustomDevice): - """thirdreality button device - alternate version.""" - - signature = { - MODELS_INFO: [(THIRD_REALITY, "3RSB22BZ")], - ENDPOINTS: { - 1: { - PROFILE_ID: 0x0104, - DEVICE_TYPE: 0x0006, - INPUT_CLUSTERS: [ - Basic.cluster_id, - MultistateInput.cluster_id, - CustomPowerConfigurationCluster.cluster_id, - ], - OUTPUT_CLUSTERS: [ - OnOff.cluster_id, - LevelControl.cluster_id, - Ota.cluster_id, - ], - } - }, - } - replacement = { - SKIP_CONFIGURATION: True, - ENDPOINTS: { - 1: { - DEVICE_TYPE: zha.DeviceType.REMOTE_CONTROL, - INPUT_CLUSTERS: [ - Basic.cluster_id, - CustomPowerConfigurationCluster, - MultistateInputCluster, - ], - OUTPUT_CLUSTERS: [ - OnOff.cluster_id, - LevelControl.cluster_id, - Ota.cluster_id, - ], - } - }, - } - - 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}, - } + if attrid == 0x0055 and (action := PRESS_TYPE.get(value)) is not None: + self.listener_event(ZHA_SEND_EVENT, action, {VALUE: value}) + + +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: 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_double_click.name, + cluster_id=ThirdRealityButtonCluster.cluster_id, + endpoint_id=1, + min_value=0, + max_value=65535, + step=1, + translation_key="cancel_double_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() +)