From c07c896944dc996a58665429d65006bcb46db4ca Mon Sep 17 00:00:00 2001 From: jintj Date: Fri, 30 Aug 2024 13:41:32 +0800 Subject: [PATCH 01/31] add 3r project py --- zhaquirks/thirdreality/garage.py | 90 +++++++++ .../temperature_humidity and soil.py | 186 ++++++++++++++++++ 2 files changed, 276 insertions(+) create mode 100644 zhaquirks/thirdreality/garage.py create mode 100644 zhaquirks/thirdreality/temperature_humidity and soil.py diff --git a/zhaquirks/thirdreality/garage.py b/zhaquirks/thirdreality/garage.py new file mode 100644 index 0000000000..d1e53574ab --- /dev/null +++ b/zhaquirks/thirdreality/garage.py @@ -0,0 +1,90 @@ +"""Third Reality garage sensor devices.""" +from zigpy.profiles import zha +from typing import Final +from zigpy.quirks import CustomDevice +import zigpy.types as t +from zigpy.zcl.clusters.general import Basic, Ota, PowerConfiguration +from zigpy.zcl.clusters.security import IasZone + +from zhaquirks import CustomCluster +from zhaquirks.const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) +from zhaquirks.thirdreality import THIRD_REALITY +from zigpy.zcl.foundation import BaseAttributeDefs, ZCLAttributeDef + +THIRD_REALITY_GARAGE_CLUSTER_ID = 0xFF01 +DELAY_OPEN_ATTR_ID = 0x0000 +ZCL_CABRATION_ATTR_ID = 0x0003 + +class ControlMode(t.uint16_t): + """Reset mode for not clear and clear.""" + pass + + +class ThirdRealityGarageCluster(CustomCluster): + """ThirdReality Acceleration Cluster.""" + + cluster_id = THIRD_REALITY_GARAGE_CLUSTER_ID + + class AttributeDefs(BaseAttributeDefs): + """ThirdReality Acceleration Cluster.""" + + delay_open: Final = ZCLAttributeDef( + id=DELAY_OPEN_ATTR_ID, + type=ControlMode, + is_manufacturer_specific=True + ) + zcl_cabration: Final = ZCLAttributeDef( + id=ZCL_CABRATION_ATTR_ID, + type=t.uint8_t, + is_manufacturer_specific=True + ) + + + + + +class Garage(CustomDevice): + """ThirdReality garage device.""" + + signature = { + MODELS_INFO: [(THIRD_REALITY, "3RDTS01056Z")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.IAS_ZONE, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + IasZone.cluster_id, + ThirdRealityGarageCluster.cluster_id, + ], + OUTPUT_CLUSTERS: [ + Ota.cluster_id, + ], + } + }, + } + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.IAS_ZONE, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + IasZone.cluster_id, + ThirdRealityGarageCluster, + ], + OUTPUT_CLUSTERS: [ + Ota.cluster_id, + ], + } + }, + } diff --git a/zhaquirks/thirdreality/temperature_humidity and soil.py b/zhaquirks/thirdreality/temperature_humidity and soil.py new file mode 100644 index 0000000000..2ad8e3cf2b --- /dev/null +++ b/zhaquirks/thirdreality/temperature_humidity and soil.py @@ -0,0 +1,186 @@ +"""Third Reality Temperature humidity devices.""" +from typing import Final +from zigpy.profiles import zha +from zigpy.quirks import CustomDevice +import zigpy.types as t +from zigpy.zcl.clusters.general import Basic, Ota, PowerConfiguration +from zigpy.zcl.clusters.measurement import TemperatureMeasurement ,RelativeHumidity +from zhaquirks import CustomCluster +from zigpy.zcl.foundation import BaseAttributeDefs, ZCLAttributeDef +from zigpy.zcl.clusters.general import ( + Basic, + Identify, + Ota, +) + +from zhaquirks.const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) +from zhaquirks.thirdreality import THIRD_REALITY + +THIRDREALITY_CLUSTER_ID = 0xFF01 + + +class ControlMode(t.int16s): + """ThirdReality Temperature humidity Cluster.""" + pass + +class ThirdRealityCluster(CustomCluster): + """ThirdReality Temperature humidity Cluster.""" + + cluster_id = THIRDREALITY_CLUSTER_ID + + class AttributeDefs(BaseAttributeDefs): + """Attribute definitions.""" + + Celsius_degree_calibration: Final = ZCLAttributeDef( + id=0x0031, + type=ControlMode, + is_manufacturer_specific=True, + ) + + humidity_calibration: Final = ZCLAttributeDef( + id=0x0032, + type=ControlMode, + is_manufacturer_specific=True, + ) + + Fahrenheit_degree_calibration: Final = ZCLAttributeDef( + id=0x0033, + type=ControlMode, + is_manufacturer_specific=True, + ) + + + +class Temperature_humidity(CustomDevice): + """ThirdReality Temperature humidity device.""" + + signature = { + MODELS_INFO: [(THIRD_REALITY, "3RTHS24BZ")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.TEMPERATURE_SENSOR, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + TemperatureMeasurement.cluster_id, + RelativeHumidity.cluster_id, + ThirdRealityCluster.cluster_id, + ], + OUTPUT_CLUSTERS: [ + Ota.cluster_id, + ], + } + }, + } + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.TEMPERATURE_SENSOR, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + TemperatureMeasurement.cluster_id, + RelativeHumidity.cluster_id, + ThirdRealityCluster, + ], + OUTPUT_CLUSTERS: [ + Ota.cluster_id, + ], + } + }, + } + + +class Temperature_humidity_lite(CustomDevice): + """ThirdReality Temperature humidity lite device.""" + + signature = { + MODELS_INFO: [(THIRD_REALITY, "3RTHS0224Z")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.TEMPERATURE_SENSOR, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + TemperatureMeasurement.cluster_id, + RelativeHumidity.cluster_id, + ThirdRealityCluster.cluster_id, + ], + OUTPUT_CLUSTERS: [ + Ota.cluster_id, + ], + } + }, + } + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.TEMPERATURE_SENSOR, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + TemperatureMeasurement.cluster_id, + RelativeHumidity.cluster_id, + ThirdRealityCluster, + ], + OUTPUT_CLUSTERS: [ + Ota.cluster_id, + ], + } + }, + } + + +class Soil_Moisture_Sensor(CustomDevice): + """ThirdReality Soil Moisture Sensor device.""" + + signature = { + MODELS_INFO: [(THIRD_REALITY, "3RSM0147Z")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.TEMPERATURE_SENSOR, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + TemperatureMeasurement.cluster_id, + RelativeHumidity.cluster_id, + ThirdRealityCluster.cluster_id, + ], + OUTPUT_CLUSTERS: [ + Ota.cluster_id, + ], + } + }, + } + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.TEMPERATURE_SENSOR, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + TemperatureMeasurement.cluster_id, + RelativeHumidity.cluster_id, + ThirdRealityCluster, + ], + OUTPUT_CLUSTERS: [ + Ota.cluster_id, + ], + } + }, + } From 138ae97fc2fdb418a98dacad3259e23fc3d12dfc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 30 Aug 2024 05:45:33 +0000 Subject: [PATCH 02/31] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- zhaquirks/thirdreality/garage.py | 10 +++--- .../temperature_humidity and soil.py | 34 +++++++++---------- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/zhaquirks/thirdreality/garage.py b/zhaquirks/thirdreality/garage.py index d1e53574ab..c406575c19 100644 --- a/zhaquirks/thirdreality/garage.py +++ b/zhaquirks/thirdreality/garage.py @@ -29,9 +29,9 @@ class ControlMode(t.uint16_t): class ThirdRealityGarageCluster(CustomCluster): """ThirdReality Acceleration Cluster.""" - + cluster_id = THIRD_REALITY_GARAGE_CLUSTER_ID - + class AttributeDefs(BaseAttributeDefs): """ThirdReality Acceleration Cluster.""" @@ -45,9 +45,9 @@ class AttributeDefs(BaseAttributeDefs): type=t.uint8_t, is_manufacturer_specific=True ) - - - + + + class Garage(CustomDevice): diff --git a/zhaquirks/thirdreality/temperature_humidity and soil.py b/zhaquirks/thirdreality/temperature_humidity and soil.py index 2ad8e3cf2b..538fe87848 100644 --- a/zhaquirks/thirdreality/temperature_humidity and soil.py +++ b/zhaquirks/thirdreality/temperature_humidity and soil.py @@ -1,19 +1,16 @@ """Third Reality Temperature humidity devices.""" + from typing import Final -from zigpy.profiles import zha -from zigpy.quirks import CustomDevice -import zigpy.types as t -from zigpy.zcl.clusters.general import Basic, Ota, PowerConfiguration -from zigpy.zcl.clusters.measurement import TemperatureMeasurement ,RelativeHumidity -from zhaquirks import CustomCluster -from zigpy.zcl.foundation import BaseAttributeDefs, ZCLAttributeDef -from zigpy.zcl.clusters.general import ( - Basic, - Identify, - Ota, -) -from zhaquirks.const import ( +from zigpy.profiles import zha +from zigpy.quirks import CustomDevice +import zigpy.types as t +from zigpy.zcl.clusters.general import Basic, Identify, Ota, PowerConfiguration +from zigpy.zcl.clusters.measurement import RelativeHumidity, TemperatureMeasurement +from zigpy.zcl.foundation import BaseAttributeDefs, ZCLAttributeDef + +from zhaquirks import CustomCluster +from zhaquirks.const import ( DEVICE_TYPE, ENDPOINTS, INPUT_CLUSTERS, @@ -21,24 +18,26 @@ OUTPUT_CLUSTERS, PROFILE_ID, ) -from zhaquirks.thirdreality import THIRD_REALITY +from zhaquirks.thirdreality import THIRD_REALITY THIRDREALITY_CLUSTER_ID = 0xFF01 class ControlMode(t.int16s): """ThirdReality Temperature humidity Cluster.""" + pass + class ThirdRealityCluster(CustomCluster): """ThirdReality Temperature humidity Cluster.""" cluster_id = THIRDREALITY_CLUSTER_ID - class AttributeDefs(BaseAttributeDefs): + class AttributeDefs(BaseAttributeDefs): """Attribute definitions.""" - Celsius_degree_calibration: Final = ZCLAttributeDef( + Celsius_degree_calibration: Final = ZCLAttributeDef( id=0x0031, type=ControlMode, is_manufacturer_specific=True, @@ -50,12 +49,11 @@ class AttributeDefs(BaseAttributeDefs): is_manufacturer_specific=True, ) - Fahrenheit_degree_calibration: Final = ZCLAttributeDef( + Fahrenheit_degree_calibration: Final = ZCLAttributeDef( id=0x0033, type=ControlMode, is_manufacturer_specific=True, ) - class Temperature_humidity(CustomDevice): From ef0efaa3753c94e85b99ed6d9e453704c334e1a8 Mon Sep 17 00:00:00 2001 From: jintj Date: Sat, 20 Sep 2025 10:16:42 +0800 Subject: [PATCH 03/31] update --- zhaquirks/thirdreality/button-v2.py | 91 +++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 zhaquirks/thirdreality/button-v2.py diff --git a/zhaquirks/thirdreality/button-v2.py b/zhaquirks/thirdreality/button-v2.py new file mode 100644 index 0000000000..c9803f5b2f --- /dev/null +++ b/zhaquirks/thirdreality/button-v2.py @@ -0,0 +1,91 @@ +"""Third Reality plug devices.""" + +from typing import Final + +from zigpy.quirks import CustomCluster +from zigpy.quirks.v2 import QuirkBuilder +from zigpy.zcl.clusters.general import MultistateInput +import zigpy.types as t +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, + ZHA_SEND_EVENT, + VALUE, +) + +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() +) From 0148d86b940a72ebbf5de2578daa8ce116db3f80 Mon Sep 17 00:00:00 2001 From: jintj Date: Sat, 20 Sep 2025 10:23:31 +0800 Subject: [PATCH 04/31] update --- zhaquirks/thirdreality/garage.py | 90 --------- .../temperature_humidity and soil.py | 184 ------------------ 2 files changed, 274 deletions(-) delete mode 100644 zhaquirks/thirdreality/garage.py delete mode 100644 zhaquirks/thirdreality/temperature_humidity and soil.py diff --git a/zhaquirks/thirdreality/garage.py b/zhaquirks/thirdreality/garage.py deleted file mode 100644 index c406575c19..0000000000 --- a/zhaquirks/thirdreality/garage.py +++ /dev/null @@ -1,90 +0,0 @@ -"""Third Reality garage sensor devices.""" -from zigpy.profiles import zha -from typing import Final -from zigpy.quirks import CustomDevice -import zigpy.types as t -from zigpy.zcl.clusters.general import Basic, Ota, PowerConfiguration -from zigpy.zcl.clusters.security import IasZone - -from zhaquirks import CustomCluster -from zhaquirks.const import ( - DEVICE_TYPE, - ENDPOINTS, - INPUT_CLUSTERS, - MODELS_INFO, - OUTPUT_CLUSTERS, - PROFILE_ID, -) -from zhaquirks.thirdreality import THIRD_REALITY -from zigpy.zcl.foundation import BaseAttributeDefs, ZCLAttributeDef - -THIRD_REALITY_GARAGE_CLUSTER_ID = 0xFF01 -DELAY_OPEN_ATTR_ID = 0x0000 -ZCL_CABRATION_ATTR_ID = 0x0003 - -class ControlMode(t.uint16_t): - """Reset mode for not clear and clear.""" - pass - - -class ThirdRealityGarageCluster(CustomCluster): - """ThirdReality Acceleration Cluster.""" - - cluster_id = THIRD_REALITY_GARAGE_CLUSTER_ID - - class AttributeDefs(BaseAttributeDefs): - """ThirdReality Acceleration Cluster.""" - - delay_open: Final = ZCLAttributeDef( - id=DELAY_OPEN_ATTR_ID, - type=ControlMode, - is_manufacturer_specific=True - ) - zcl_cabration: Final = ZCLAttributeDef( - id=ZCL_CABRATION_ATTR_ID, - type=t.uint8_t, - is_manufacturer_specific=True - ) - - - - - -class Garage(CustomDevice): - """ThirdReality garage device.""" - - signature = { - MODELS_INFO: [(THIRD_REALITY, "3RDTS01056Z")], - ENDPOINTS: { - 1: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.IAS_ZONE, - INPUT_CLUSTERS: [ - Basic.cluster_id, - PowerConfiguration.cluster_id, - IasZone.cluster_id, - ThirdRealityGarageCluster.cluster_id, - ], - OUTPUT_CLUSTERS: [ - Ota.cluster_id, - ], - } - }, - } - replacement = { - ENDPOINTS: { - 1: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.IAS_ZONE, - INPUT_CLUSTERS: [ - Basic.cluster_id, - PowerConfiguration.cluster_id, - IasZone.cluster_id, - ThirdRealityGarageCluster, - ], - OUTPUT_CLUSTERS: [ - Ota.cluster_id, - ], - } - }, - } diff --git a/zhaquirks/thirdreality/temperature_humidity and soil.py b/zhaquirks/thirdreality/temperature_humidity and soil.py deleted file mode 100644 index 538fe87848..0000000000 --- a/zhaquirks/thirdreality/temperature_humidity and soil.py +++ /dev/null @@ -1,184 +0,0 @@ -"""Third Reality Temperature humidity devices.""" - -from typing import Final - -from zigpy.profiles import zha -from zigpy.quirks import CustomDevice -import zigpy.types as t -from zigpy.zcl.clusters.general import Basic, Identify, Ota, PowerConfiguration -from zigpy.zcl.clusters.measurement import RelativeHumidity, TemperatureMeasurement -from zigpy.zcl.foundation import BaseAttributeDefs, ZCLAttributeDef - -from zhaquirks import CustomCluster -from zhaquirks.const import ( - DEVICE_TYPE, - ENDPOINTS, - INPUT_CLUSTERS, - MODELS_INFO, - OUTPUT_CLUSTERS, - PROFILE_ID, -) -from zhaquirks.thirdreality import THIRD_REALITY - -THIRDREALITY_CLUSTER_ID = 0xFF01 - - -class ControlMode(t.int16s): - """ThirdReality Temperature humidity Cluster.""" - - pass - - -class ThirdRealityCluster(CustomCluster): - """ThirdReality Temperature humidity Cluster.""" - - cluster_id = THIRDREALITY_CLUSTER_ID - - class AttributeDefs(BaseAttributeDefs): - """Attribute definitions.""" - - Celsius_degree_calibration: Final = ZCLAttributeDef( - id=0x0031, - type=ControlMode, - is_manufacturer_specific=True, - ) - - humidity_calibration: Final = ZCLAttributeDef( - id=0x0032, - type=ControlMode, - is_manufacturer_specific=True, - ) - - Fahrenheit_degree_calibration: Final = ZCLAttributeDef( - id=0x0033, - type=ControlMode, - is_manufacturer_specific=True, - ) - - -class Temperature_humidity(CustomDevice): - """ThirdReality Temperature humidity device.""" - - signature = { - MODELS_INFO: [(THIRD_REALITY, "3RTHS24BZ")], - ENDPOINTS: { - 1: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.TEMPERATURE_SENSOR, - INPUT_CLUSTERS: [ - Basic.cluster_id, - PowerConfiguration.cluster_id, - TemperatureMeasurement.cluster_id, - RelativeHumidity.cluster_id, - ThirdRealityCluster.cluster_id, - ], - OUTPUT_CLUSTERS: [ - Ota.cluster_id, - ], - } - }, - } - replacement = { - ENDPOINTS: { - 1: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.TEMPERATURE_SENSOR, - INPUT_CLUSTERS: [ - Basic.cluster_id, - PowerConfiguration.cluster_id, - TemperatureMeasurement.cluster_id, - RelativeHumidity.cluster_id, - ThirdRealityCluster, - ], - OUTPUT_CLUSTERS: [ - Ota.cluster_id, - ], - } - }, - } - - -class Temperature_humidity_lite(CustomDevice): - """ThirdReality Temperature humidity lite device.""" - - signature = { - MODELS_INFO: [(THIRD_REALITY, "3RTHS0224Z")], - ENDPOINTS: { - 1: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.TEMPERATURE_SENSOR, - INPUT_CLUSTERS: [ - Basic.cluster_id, - PowerConfiguration.cluster_id, - Identify.cluster_id, - TemperatureMeasurement.cluster_id, - RelativeHumidity.cluster_id, - ThirdRealityCluster.cluster_id, - ], - OUTPUT_CLUSTERS: [ - Ota.cluster_id, - ], - } - }, - } - replacement = { - ENDPOINTS: { - 1: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.TEMPERATURE_SENSOR, - INPUT_CLUSTERS: [ - Basic.cluster_id, - PowerConfiguration.cluster_id, - Identify.cluster_id, - TemperatureMeasurement.cluster_id, - RelativeHumidity.cluster_id, - ThirdRealityCluster, - ], - OUTPUT_CLUSTERS: [ - Ota.cluster_id, - ], - } - }, - } - - -class Soil_Moisture_Sensor(CustomDevice): - """ThirdReality Soil Moisture Sensor device.""" - - signature = { - MODELS_INFO: [(THIRD_REALITY, "3RSM0147Z")], - ENDPOINTS: { - 1: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.TEMPERATURE_SENSOR, - INPUT_CLUSTERS: [ - Basic.cluster_id, - PowerConfiguration.cluster_id, - TemperatureMeasurement.cluster_id, - RelativeHumidity.cluster_id, - ThirdRealityCluster.cluster_id, - ], - OUTPUT_CLUSTERS: [ - Ota.cluster_id, - ], - } - }, - } - replacement = { - ENDPOINTS: { - 1: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.TEMPERATURE_SENSOR, - INPUT_CLUSTERS: [ - Basic.cluster_id, - PowerConfiguration.cluster_id, - TemperatureMeasurement.cluster_id, - RelativeHumidity.cluster_id, - ThirdRealityCluster, - ], - OUTPUT_CLUSTERS: [ - Ota.cluster_id, - ], - } - }, - } From 20f777e60efeddde53a81f98f8de0ad467060664 Mon Sep 17 00:00:00 2001 From: jintj Date: Sat, 20 Sep 2025 10:24:58 +0800 Subject: [PATCH 05/31] update --- zhaquirks/thirdreality/button-v2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zhaquirks/thirdreality/button-v2.py b/zhaquirks/thirdreality/button-v2.py index c9803f5b2f..844676a594 100644 --- a/zhaquirks/thirdreality/button-v2.py +++ b/zhaquirks/thirdreality/button-v2.py @@ -1,4 +1,4 @@ -"""Third Reality plug devices.""" +"""Third Reality button devices.""" from typing import Final From 0e87d698513a6bb7613e41e911b68be336df129e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 20 Sep 2025 02:34:43 +0000 Subject: [PATCH 06/31] Apply pre-commit auto fixes --- zhaquirks/thirdreality/button-v2.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/zhaquirks/thirdreality/button-v2.py b/zhaquirks/thirdreality/button-v2.py index 844676a594..e0b51efff7 100644 --- a/zhaquirks/thirdreality/button-v2.py +++ b/zhaquirks/thirdreality/button-v2.py @@ -4,9 +4,10 @@ from zigpy.quirks import CustomCluster from zigpy.quirks.v2 import QuirkBuilder -from zigpy.zcl.clusters.general import MultistateInput 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, @@ -17,8 +18,8 @@ LONG_PRESS, LONG_RELEASE, SHORT_PRESS, - ZHA_SEND_EVENT, VALUE, + ZHA_SEND_EVENT, ) MOVEMENT_TYPE = { @@ -28,6 +29,7 @@ 255: COMMAND_RELEASE, } + class MultistateInputCluster(CustomCluster, MultistateInput): """Multistate input cluster.""" @@ -64,7 +66,6 @@ class AttributeDefs(BaseAttributeDefs): ) - ( QuirkBuilder("Third Reality, Inc", "3RSB22BZ") .replaces(ThirdRealityButtonCluster) From 126ecd89a90440cf7fb2fc77f18ea9aff0cc6175 Mon Sep 17 00:00:00 2001 From: jintj Date: Wed, 24 Sep 2025 18:57:19 +0800 Subject: [PATCH 07/31] update button tes --- tests/test_third_button.py | 100 +++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 tests/test_third_button.py diff --git a/tests/test_third_button.py b/tests/test_third_button.py new file mode 100644 index 0000000000..8272d4cc45 --- /dev/null +++ b/tests/test_third_button.py @@ -0,0 +1,100 @@ + +import pytest +from zigpy.zcl.clusters.security import IasZone + +from tests.common import ClusterListener +import zhaquirks +import zhaquirks.thirdreality.night_light +import zhaquirks.thirdreality.button_v2 + +zhaquirks.setup() + + +@pytest.mark.parametrize("quirk", (zhaquirks.thirdreality.night_light.Nightlight,)) +async def test_third_reality_nightlight(zigpy_device_from_quirk, quirk): + """Test Third Reality night light forwarding motion attribute to IasZone cluster.""" + + device = zigpy_device_from_quirk(quirk) + + ias_zone_cluster = device.endpoints[1].ias_zone + ias_zone_listener = ClusterListener(ias_zone_cluster) + + ias_zone_status_id = IasZone.AttributeDefs.zone_status.id + + third_reality_cluster = device.endpoints[1].in_clusters[0xFC00] + + # 0x0002 is also used on manufacturer specific cluster for motion events + third_reality_cluster.update_attribute(0x0002, IasZone.ZoneStatus.Alarm_1) + + assert len(ias_zone_listener.attribute_updates) == 1 + assert ias_zone_listener.attribute_updates[0][0] == ias_zone_status_id + assert ias_zone_listener.attribute_updates[0][1] == IasZone.ZoneStatus.Alarm_1 + + # turn off motion alarm + third_reality_cluster.update_attribute(0x0002, 0) + + 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("manufacturer, model", [("Third Reality, Inc", "3RSB22BZ")]) +async def test_third_reality_button_v2(zigpy_device_from_v2_quirk, manufacturer, model): + """Test Third Reality button v2 device.""" + + device = zigpy_device_from_v2_quirk(manufacturer, model) + + + assert 1 in device.endpoints + + + multistate_cluster = None + for cluster in device.endpoints[1].in_clusters.values(): + if isinstance(cluster, zhaquirks.thirdreality.button_v2.MultistateInputCluster): + multistate_cluster = cluster + break + + assert multistate_cluster is not None, "MultistateInputCluster not found" + + + class MockListener: + def __init__(self): + self.events = [] + + def zha_send_event(self, cluster, command, args): + self.events.append((command, args)) + + mock_listener = MockListener() + multistate_cluster.add_listener(mock_listener) + + + test_values = [ + (0, "command_hold"), + (1, "command_single"), + (2, "command_double"), + (255, "command_release") + ] + + for value, expected_command in test_values: + + mock_listener.events = [] + + + multistate_cluster._update_attribute(0x0055, value) + + + assert len(mock_listener.events) == 1 + assert mock_listener.events[0][0] == expected_command + assert mock_listener.events[0][1]["value"] == value + + + private_cluster = None + for cluster in device.endpoints[1].in_clusters.values(): + if isinstance(cluster, zhaquirks.thirdreality.button_v2.ThirdRealityButtonCluster): + private_cluster = cluster + break + + assert private_cluster is not None, "ThirdRealityButtonCluster not found" + assert private_cluster.cluster_id == 0xFF01 + assert hasattr(private_cluster.AttributeDefs, "cancel_bouble_click") + assert private_cluster.AttributeDefs.cancel_bouble_click.id == 0x0000 From 36eee8fe76f585b7fd53f345d53a39d74702ed5c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 24 Sep 2025 10:57:39 +0000 Subject: [PATCH 08/31] Apply pre-commit auto fixes --- tests/test_third_button.py | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/tests/test_third_button.py b/tests/test_third_button.py index 8272d4cc45..74eb17d93e 100644 --- a/tests/test_third_button.py +++ b/tests/test_third_button.py @@ -1,11 +1,10 @@ - import pytest from zigpy.zcl.clusters.security import IasZone from tests.common import ClusterListener import zhaquirks -import zhaquirks.thirdreality.night_light import zhaquirks.thirdreality.button_v2 +import zhaquirks.thirdreality.night_light zhaquirks.setup() @@ -43,57 +42,51 @@ async def test_third_reality_button_v2(zigpy_device_from_v2_quirk, manufacturer, """Test Third Reality button v2 device.""" device = zigpy_device_from_v2_quirk(manufacturer, model) - assert 1 in device.endpoints - multistate_cluster = None for cluster in device.endpoints[1].in_clusters.values(): if isinstance(cluster, zhaquirks.thirdreality.button_v2.MultistateInputCluster): multistate_cluster = cluster break - + assert multistate_cluster is not None, "MultistateInputCluster not found" - class MockListener: def __init__(self): self.events = [] - + def zha_send_event(self, cluster, command, args): self.events.append((command, args)) - + mock_listener = MockListener() multistate_cluster.add_listener(mock_listener) - test_values = [ (0, "command_hold"), (1, "command_single"), (2, "command_double"), - (255, "command_release") + (255, "command_release"), ] - - for value, expected_command in test_values: + for value, expected_command in test_values: mock_listener.events = [] - multistate_cluster._update_attribute(0x0055, value) - assert len(mock_listener.events) == 1 assert mock_listener.events[0][0] == expected_command assert mock_listener.events[0][1]["value"] == value - private_cluster = None for cluster in device.endpoints[1].in_clusters.values(): - if isinstance(cluster, zhaquirks.thirdreality.button_v2.ThirdRealityButtonCluster): + if isinstance( + cluster, zhaquirks.thirdreality.button_v2.ThirdRealityButtonCluster + ): private_cluster = cluster break - + assert private_cluster is not None, "ThirdRealityButtonCluster not found" assert private_cluster.cluster_id == 0xFF01 assert hasattr(private_cluster.AttributeDefs, "cancel_bouble_click") From 2cc011fb7caa5720b6ffab3b6797f1b460691859 Mon Sep 17 00:00:00 2001 From: jintj Date: Wed, 24 Sep 2025 19:05:46 +0800 Subject: [PATCH 09/31] update --- tests/test_third_button.py | 135 +++++++++++++------------------------ 1 file changed, 46 insertions(+), 89 deletions(-) diff --git a/tests/test_third_button.py b/tests/test_third_button.py index 8272d4cc45..386a1ceb2c 100644 --- a/tests/test_third_button.py +++ b/tests/test_third_button.py @@ -1,100 +1,57 @@ +"""Tests for Third Reality button quirks.""" import pytest -from zigpy.zcl.clusters.security import IasZone from tests.common import ClusterListener import zhaquirks -import zhaquirks.thirdreality.night_light -import zhaquirks.thirdreality.button_v2 +from zhaquirks.thirdreality.button import ( + MultistateInputCluster, + ThirdRealityButtonCluster, +) zhaquirks.setup() -@pytest.mark.parametrize("quirk", (zhaquirks.thirdreality.night_light.Nightlight,)) -async def test_third_reality_nightlight(zigpy_device_from_quirk, quirk): - """Test Third Reality night light forwarding motion attribute to IasZone cluster.""" - +@pytest.mark.parametrize( + "quirk", + (zhaquirks.thirdreality.button.ThirdRealityButton,), # Replace with actual device quirk class name +) +async def test_third_reality_button(zigpy_device_from_quirk, quirk): + """Test Third Reality button event conversion and triggering functionality.""" + # Create mock device based on the quirk device = zigpy_device_from_quirk(quirk) - - ias_zone_cluster = device.endpoints[1].ias_zone - ias_zone_listener = ClusterListener(ias_zone_cluster) - - ias_zone_status_id = IasZone.AttributeDefs.zone_status.id - - third_reality_cluster = device.endpoints[1].in_clusters[0xFC00] - - # 0x0002 is also used on manufacturer specific cluster for motion events - third_reality_cluster.update_attribute(0x0002, IasZone.ZoneStatus.Alarm_1) - - assert len(ias_zone_listener.attribute_updates) == 1 - assert ias_zone_listener.attribute_updates[0][0] == ias_zone_status_id - assert ias_zone_listener.attribute_updates[0][1] == IasZone.ZoneStatus.Alarm_1 - - # turn off motion alarm - third_reality_cluster.update_attribute(0x0002, 0) - - 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("manufacturer, model", [("Third Reality, Inc", "3RSB22BZ")]) -async def test_third_reality_button_v2(zigpy_device_from_v2_quirk, manufacturer, model): - """Test Third Reality button v2 device.""" - - device = zigpy_device_from_v2_quirk(manufacturer, model) - - - assert 1 in device.endpoints - - - multistate_cluster = None - for cluster in device.endpoints[1].in_clusters.values(): - if isinstance(cluster, zhaquirks.thirdreality.button_v2.MultistateInputCluster): - multistate_cluster = cluster - break - - assert multistate_cluster is not None, "MultistateInputCluster not found" - - - class MockListener: - def __init__(self): - self.events = [] - - def zha_send_event(self, cluster, command, args): - self.events.append((command, args)) - - mock_listener = MockListener() - multistate_cluster.add_listener(mock_listener) - - - test_values = [ - (0, "command_hold"), - (1, "command_single"), - (2, "command_double"), - (255, "command_release") - ] - - for value, expected_command in test_values: - - mock_listener.events = [] - - - multistate_cluster._update_attribute(0x0055, value) - - - assert len(mock_listener.events) == 1 - assert mock_listener.events[0][0] == expected_command - assert mock_listener.events[0][1]["value"] == value - - - private_cluster = None - for cluster in device.endpoints[1].in_clusters.values(): - if isinstance(cluster, zhaquirks.thirdreality.button_v2.ThirdRealityButtonCluster): - private_cluster = cluster - break - assert private_cluster is not None, "ThirdRealityButtonCluster not found" - assert private_cluster.cluster_id == 0xFF01 - assert hasattr(private_cluster.AttributeDefs, "cancel_bouble_click") - assert private_cluster.AttributeDefs.cancel_bouble_click.id == 0x0000 + # Get relevant clusters + multistate_cluster = device.endpoints[1].multistate_input + private_cluster = device.endpoints[1].in_clusters[ThirdRealityButtonCluster.cluster_id] + + # Create cluster listener + multistate_listener = ClusterListener(multistate_cluster) + + # Test 1: Verify single click event conversion + multistate_cluster.update_attribute(0x0055, 1) # 1 corresponds to single click + assert len(multistate_listener.zha_send_events) == 1 + assert multistate_listener.zha_send_events[0][0] == "single" + assert multistate_listener.attribute_updates[-1][0] == 0 + assert multistate_listener.attribute_updates[-1][1] == "single" + + # Test 2: Verify double click event conversion + multistate_cluster.update_attribute(0x0055, 2) # 2 corresponds to double click + assert len(multistate_listener.zha_send_events) == 2 + assert multistate_listener.zha_send_events[1][0] == "double" + + # Test 3: Verify hold event conversion + multistate_cluster.update_attribute(0x0055, 0) # 0 corresponds to hold + assert len(multistate_listener.zha_send_events) == 3 + assert multistate_listener.zha_send_events[2][0] == "hold" + + # Test 4: Verify release event conversion + multistate_cluster.update_attribute(0x0055, 255) # 255 corresponds to release + assert len(multistate_listener.zha_send_events) == 4 + assert multistate_listener.zha_send_events[3][0] == "release" + + # Test 5: Verify private cluster attributes + assert hasattr(private_cluster.attributes, "cancel_bouble_click") + attr = private_cluster.attributes.cancel_bouble_click + assert attr.id == 0x0000 + assert attr.type == int # Corresponds to t.uint8_t From 89e80cb1d7c05befd6b67ca5e52faeab5cf2ed4d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 24 Sep 2025 11:06:49 +0000 Subject: [PATCH 10/31] Apply pre-commit auto fixes --- tests/test_third_button.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/tests/test_third_button.py b/tests/test_third_button.py index 386a1ceb2c..5756066f62 100644 --- a/tests/test_third_button.py +++ b/tests/test_third_button.py @@ -4,52 +4,53 @@ from tests.common import ClusterListener import zhaquirks -from zhaquirks.thirdreality.button import ( - MultistateInputCluster, - ThirdRealityButtonCluster, -) +from zhaquirks.thirdreality.button import ThirdRealityButtonCluster zhaquirks.setup() @pytest.mark.parametrize( "quirk", - (zhaquirks.thirdreality.button.ThirdRealityButton,), # Replace with actual device quirk class name + ( + zhaquirks.thirdreality.button.ThirdRealityButton, + ), # Replace with actual device quirk class name ) async def test_third_reality_button(zigpy_device_from_quirk, quirk): """Test Third Reality button event conversion and triggering functionality.""" # Create mock device based on the quirk device = zigpy_device_from_quirk(quirk) - + # Get relevant clusters multistate_cluster = device.endpoints[1].multistate_input - private_cluster = device.endpoints[1].in_clusters[ThirdRealityButtonCluster.cluster_id] - + private_cluster = device.endpoints[1].in_clusters[ + ThirdRealityButtonCluster.cluster_id + ] + # Create cluster listener multistate_listener = ClusterListener(multistate_cluster) - + # Test 1: Verify single click event conversion multistate_cluster.update_attribute(0x0055, 1) # 1 corresponds to single click assert len(multistate_listener.zha_send_events) == 1 assert multistate_listener.zha_send_events[0][0] == "single" assert multistate_listener.attribute_updates[-1][0] == 0 assert multistate_listener.attribute_updates[-1][1] == "single" - + # Test 2: Verify double click event conversion multistate_cluster.update_attribute(0x0055, 2) # 2 corresponds to double click assert len(multistate_listener.zha_send_events) == 2 assert multistate_listener.zha_send_events[1][0] == "double" - + # Test 3: Verify hold event conversion multistate_cluster.update_attribute(0x0055, 0) # 0 corresponds to hold assert len(multistate_listener.zha_send_events) == 3 assert multistate_listener.zha_send_events[2][0] == "hold" - + # Test 4: Verify release event conversion multistate_cluster.update_attribute(0x0055, 255) # 255 corresponds to release assert len(multistate_listener.zha_send_events) == 4 assert multistate_listener.zha_send_events[3][0] == "release" - + # Test 5: Verify private cluster attributes assert hasattr(private_cluster.attributes, "cancel_bouble_click") attr = private_cluster.attributes.cancel_bouble_click From 115e9e47c002726112631eaa67f42e224e494730 Mon Sep 17 00:00:00 2001 From: support <117250319+3reality-support@users.noreply.github.com> Date: Wed, 24 Sep 2025 19:08:59 +0800 Subject: [PATCH 11/31] Update test_third_button.py --- tests/test_third_button.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/tests/test_third_button.py b/tests/test_third_button.py index 5756066f62..386a1ceb2c 100644 --- a/tests/test_third_button.py +++ b/tests/test_third_button.py @@ -4,53 +4,52 @@ from tests.common import ClusterListener import zhaquirks -from zhaquirks.thirdreality.button import ThirdRealityButtonCluster +from zhaquirks.thirdreality.button import ( + MultistateInputCluster, + ThirdRealityButtonCluster, +) zhaquirks.setup() @pytest.mark.parametrize( "quirk", - ( - zhaquirks.thirdreality.button.ThirdRealityButton, - ), # Replace with actual device quirk class name + (zhaquirks.thirdreality.button.ThirdRealityButton,), # Replace with actual device quirk class name ) async def test_third_reality_button(zigpy_device_from_quirk, quirk): """Test Third Reality button event conversion and triggering functionality.""" # Create mock device based on the quirk device = zigpy_device_from_quirk(quirk) - + # Get relevant clusters multistate_cluster = device.endpoints[1].multistate_input - private_cluster = device.endpoints[1].in_clusters[ - ThirdRealityButtonCluster.cluster_id - ] - + private_cluster = device.endpoints[1].in_clusters[ThirdRealityButtonCluster.cluster_id] + # Create cluster listener multistate_listener = ClusterListener(multistate_cluster) - + # Test 1: Verify single click event conversion multistate_cluster.update_attribute(0x0055, 1) # 1 corresponds to single click assert len(multistate_listener.zha_send_events) == 1 assert multistate_listener.zha_send_events[0][0] == "single" assert multistate_listener.attribute_updates[-1][0] == 0 assert multistate_listener.attribute_updates[-1][1] == "single" - + # Test 2: Verify double click event conversion multistate_cluster.update_attribute(0x0055, 2) # 2 corresponds to double click assert len(multistate_listener.zha_send_events) == 2 assert multistate_listener.zha_send_events[1][0] == "double" - + # Test 3: Verify hold event conversion multistate_cluster.update_attribute(0x0055, 0) # 0 corresponds to hold assert len(multistate_listener.zha_send_events) == 3 assert multistate_listener.zha_send_events[2][0] == "hold" - + # Test 4: Verify release event conversion multistate_cluster.update_attribute(0x0055, 255) # 255 corresponds to release assert len(multistate_listener.zha_send_events) == 4 assert multistate_listener.zha_send_events[3][0] == "release" - + # Test 5: Verify private cluster attributes assert hasattr(private_cluster.attributes, "cancel_bouble_click") attr = private_cluster.attributes.cancel_bouble_click From b484bc784b31baf719e8039b65ce0a3f6ed805cb Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 24 Sep 2025 11:09:07 +0000 Subject: [PATCH 12/31] Apply pre-commit auto fixes --- tests/test_third_button.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/tests/test_third_button.py b/tests/test_third_button.py index 386a1ceb2c..5756066f62 100644 --- a/tests/test_third_button.py +++ b/tests/test_third_button.py @@ -4,52 +4,53 @@ from tests.common import ClusterListener import zhaquirks -from zhaquirks.thirdreality.button import ( - MultistateInputCluster, - ThirdRealityButtonCluster, -) +from zhaquirks.thirdreality.button import ThirdRealityButtonCluster zhaquirks.setup() @pytest.mark.parametrize( "quirk", - (zhaquirks.thirdreality.button.ThirdRealityButton,), # Replace with actual device quirk class name + ( + zhaquirks.thirdreality.button.ThirdRealityButton, + ), # Replace with actual device quirk class name ) async def test_third_reality_button(zigpy_device_from_quirk, quirk): """Test Third Reality button event conversion and triggering functionality.""" # Create mock device based on the quirk device = zigpy_device_from_quirk(quirk) - + # Get relevant clusters multistate_cluster = device.endpoints[1].multistate_input - private_cluster = device.endpoints[1].in_clusters[ThirdRealityButtonCluster.cluster_id] - + private_cluster = device.endpoints[1].in_clusters[ + ThirdRealityButtonCluster.cluster_id + ] + # Create cluster listener multistate_listener = ClusterListener(multistate_cluster) - + # Test 1: Verify single click event conversion multistate_cluster.update_attribute(0x0055, 1) # 1 corresponds to single click assert len(multistate_listener.zha_send_events) == 1 assert multistate_listener.zha_send_events[0][0] == "single" assert multistate_listener.attribute_updates[-1][0] == 0 assert multistate_listener.attribute_updates[-1][1] == "single" - + # Test 2: Verify double click event conversion multistate_cluster.update_attribute(0x0055, 2) # 2 corresponds to double click assert len(multistate_listener.zha_send_events) == 2 assert multistate_listener.zha_send_events[1][0] == "double" - + # Test 3: Verify hold event conversion multistate_cluster.update_attribute(0x0055, 0) # 0 corresponds to hold assert len(multistate_listener.zha_send_events) == 3 assert multistate_listener.zha_send_events[2][0] == "hold" - + # Test 4: Verify release event conversion multistate_cluster.update_attribute(0x0055, 255) # 255 corresponds to release assert len(multistate_listener.zha_send_events) == 4 assert multistate_listener.zha_send_events[3][0] == "release" - + # Test 5: Verify private cluster attributes assert hasattr(private_cluster.attributes, "cancel_bouble_click") attr = private_cluster.attributes.cancel_bouble_click From c798a96b8302584da59ac1fa9ea3747ece6b50ad Mon Sep 17 00:00:00 2001 From: support <117250319+3reality-support@users.noreply.github.com> Date: Thu, 25 Sep 2025 08:39:34 +0800 Subject: [PATCH 13/31] Update test_third_button.py --- tests/test_third_button.py | 105 +++++++++++++++++++++---------------- 1 file changed, 59 insertions(+), 46 deletions(-) diff --git a/tests/test_third_button.py b/tests/test_third_button.py index 5756066f62..fb951ff20e 100644 --- a/tests/test_third_button.py +++ b/tests/test_third_button.py @@ -4,55 +4,68 @@ from tests.common import ClusterListener import zhaquirks -from zhaquirks.thirdreality.button import ThirdRealityButtonCluster +from zhaquirks.thirdreality.button_v2 import ( + MultistateInputCluster, + ThirdRealityButtonCluster, +) zhaquirks.setup() -@pytest.mark.parametrize( - "quirk", - ( - zhaquirks.thirdreality.button.ThirdRealityButton, - ), # Replace with actual device quirk class name -) -async def test_third_reality_button(zigpy_device_from_quirk, quirk): - """Test Third Reality button event conversion and triggering functionality.""" - # Create mock device based on the quirk - device = zigpy_device_from_quirk(quirk) - - # Get relevant clusters - multistate_cluster = device.endpoints[1].multistate_input - private_cluster = device.endpoints[1].in_clusters[ - ThirdRealityButtonCluster.cluster_id + +@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 v2 event conversion and triggering functionality.""" + + device = zigpy_device_from_v2_quirk(manufacturer, model) + + + multistate_cluster = None + for cluster in device.endpoints[1].in_clusters.values(): + if isinstance(cluster, MultistateInputCluster): + multistate_cluster = cluster + break + + assert multistate_cluster is not None, "MultistateInputCluster not found" + + + class MockListener: + def __init__(self): + self.events = [] + + def zha_send_event(self, cluster, command, args): + self.events.append((command, args)) + + mock_listener = MockListener() + multistate_cluster.add_listener(mock_listener) + + test_values = [ + (0, "command_hold"), # HOLD + (1, "command_single"), # SINGLE + (2, "command_double"), # DOUBLE + (255, "command_release") # RELEASE ] + + for value, expected_command in test_values: + + mock_listener.events = [] + + + multistate_cluster._update_attribute(0x0055, value) + + + assert len(mock_listener.events) == 1 + assert mock_listener.events[0][0] == expected_command + assert mock_listener.events[0][1]["value"] == value + - # Create cluster listener - multistate_listener = ClusterListener(multistate_cluster) - - # Test 1: Verify single click event conversion - multistate_cluster.update_attribute(0x0055, 1) # 1 corresponds to single click - assert len(multistate_listener.zha_send_events) == 1 - assert multistate_listener.zha_send_events[0][0] == "single" - assert multistate_listener.attribute_updates[-1][0] == 0 - assert multistate_listener.attribute_updates[-1][1] == "single" - - # Test 2: Verify double click event conversion - multistate_cluster.update_attribute(0x0055, 2) # 2 corresponds to double click - assert len(multistate_listener.zha_send_events) == 2 - assert multistate_listener.zha_send_events[1][0] == "double" - - # Test 3: Verify hold event conversion - multistate_cluster.update_attribute(0x0055, 0) # 0 corresponds to hold - assert len(multistate_listener.zha_send_events) == 3 - assert multistate_listener.zha_send_events[2][0] == "hold" - - # Test 4: Verify release event conversion - multistate_cluster.update_attribute(0x0055, 255) # 255 corresponds to release - assert len(multistate_listener.zha_send_events) == 4 - assert multistate_listener.zha_send_events[3][0] == "release" - - # Test 5: Verify private cluster attributes - assert hasattr(private_cluster.attributes, "cancel_bouble_click") - attr = private_cluster.attributes.cancel_bouble_click - assert attr.id == 0x0000 - assert attr.type == int # Corresponds to t.uint8_t + private_cluster = None + for cluster in device.endpoints[1].in_clusters.values(): + if isinstance(cluster, ThirdRealityButtonCluster): + private_cluster = cluster + break + + assert private_cluster is not None, "ThirdRealityButtonCluster not found" + assert private_cluster.cluster_id == 0xFF01 + assert hasattr(private_cluster.AttributeDefs, "cancel_bouble_click") + assert private_cluster.AttributeDefs.cancel_bouble_click.id == 0x0000 From 88c984748f49889ebda108591238ace4027450de Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 25 Sep 2025 00:39:42 +0000 Subject: [PATCH 14/31] Apply pre-commit auto fixes --- tests/test_third_button.py | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/tests/test_third_button.py b/tests/test_third_button.py index fb951ff20e..bb6ac899db 100644 --- a/tests/test_third_button.py +++ b/tests/test_third_button.py @@ -2,7 +2,6 @@ import pytest -from tests.common import ClusterListener import zhaquirks from zhaquirks.thirdreality.button_v2 import ( MultistateInputCluster, @@ -12,59 +11,52 @@ 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 v2 event conversion and triggering functionality.""" device = zigpy_device_from_v2_quirk(manufacturer, model) - multistate_cluster = None for cluster in device.endpoints[1].in_clusters.values(): if isinstance(cluster, MultistateInputCluster): multistate_cluster = cluster break - + assert multistate_cluster is not None, "MultistateInputCluster not found" - class MockListener: def __init__(self): self.events = [] - + def zha_send_event(self, cluster, command, args): self.events.append((command, args)) - + mock_listener = MockListener() multistate_cluster.add_listener(mock_listener) test_values = [ - (0, "command_hold"), # HOLD - (1, "command_single"), # SINGLE - (2, "command_double"), # DOUBLE - (255, "command_release") # RELEASE + (0, "command_hold"), # HOLD + (1, "command_single"), # SINGLE + (2, "command_double"), # DOUBLE + (255, "command_release"), # RELEASE ] - - for value, expected_command in test_values: + for value, expected_command in test_values: mock_listener.events = [] - multistate_cluster._update_attribute(0x0055, value) - assert len(mock_listener.events) == 1 assert mock_listener.events[0][0] == expected_command assert mock_listener.events[0][1]["value"] == value - private_cluster = None for cluster in device.endpoints[1].in_clusters.values(): if isinstance(cluster, ThirdRealityButtonCluster): private_cluster = cluster break - + assert private_cluster is not None, "ThirdRealityButtonCluster not found" assert private_cluster.cluster_id == 0xFF01 assert hasattr(private_cluster.AttributeDefs, "cancel_bouble_click") From 4f74b2e855627aa8a11cda260dd6c10115b96478 Mon Sep 17 00:00:00 2001 From: support <117250319+3reality-support@users.noreply.github.com> Date: Thu, 25 Sep 2025 08:44:56 +0800 Subject: [PATCH 15/31] Update test_third_button.py --- tests/test_third_button.py | 94 +++++++++++++++++++++----------------- 1 file changed, 51 insertions(+), 43 deletions(-) diff --git a/tests/test_third_button.py b/tests/test_third_button.py index bb6ac899db..b17e237f2b 100644 --- a/tests/test_third_button.py +++ b/tests/test_third_button.py @@ -1,63 +1,71 @@ -"""Tests for Third Reality button quirks.""" - import pytest +from tests.common import ClusterListener import zhaquirks from zhaquirks.thirdreality.button_v2 import ( MultistateInputCluster, ThirdRealityButtonCluster, ) +# Create a mock listener to capture ZHA events +class MockListener: + def __init__(self): + self.zha_send_events = [] + + def zha_send_event(self, action, event_args): + self.zha_send_events.append((action, event_args)) + zhaquirks.setup() -@pytest.mark.parametrize("manufacturer, model", [("Third Reality, Inc", "3RSB22BZ")]) +@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 v2 event conversion and triggering functionality.""" - + """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 = None + private_cluster = None + for cluster in device.endpoints[1].in_clusters.values(): if isinstance(cluster, MultistateInputCluster): multistate_cluster = cluster - break - + if cluster.cluster_id == ThirdRealityButtonCluster.cluster_id: + private_cluster = cluster + assert multistate_cluster is not None, "MultistateInputCluster not found" - - class MockListener: - def __init__(self): - self.events = [] - - def zha_send_event(self, cluster, command, args): - self.events.append((command, args)) - + assert private_cluster is not None, "ThirdRealityButtonCluster not found" + + # Create mock listener and register it with the cluster mock_listener = MockListener() multistate_cluster.add_listener(mock_listener) - - test_values = [ - (0, "command_hold"), # HOLD - (1, "command_single"), # SINGLE - (2, "command_double"), # DOUBLE - (255, "command_release"), # RELEASE - ] - - for value, expected_command in test_values: - mock_listener.events = [] - - multistate_cluster._update_attribute(0x0055, value) - - assert len(mock_listener.events) == 1 - assert mock_listener.events[0][0] == expected_command - assert mock_listener.events[0][1]["value"] == value - - private_cluster = None - for cluster in device.endpoints[1].in_clusters.values(): - if isinstance(cluster, ThirdRealityButtonCluster): - private_cluster = cluster - break - - assert private_cluster is not None, "ThirdRealityButtonCluster not found" - assert private_cluster.cluster_id == 0xFF01 - assert hasattr(private_cluster.AttributeDefs, "cancel_bouble_click") - assert private_cluster.AttributeDefs.cancel_bouble_click.id == 0x0000 + + # Test 1: Verify single click event conversion + 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 + multistate_cluster.update_attribute(0x0055, 2) # 2 corresponds to double click + assert len(mock_listener.zha_send_events) == 2 + assert mock_listener.zha_send_events[1][0] == "double" + + # Test 3: Verify hold event conversion + multistate_cluster.update_attribute(0x0055, 0) # 0 corresponds to hold + assert len(mock_listener.zha_send_events) == 3 + assert mock_listener.zha_send_events[2][0] == "hold" + + # Test 4: Verify release event conversion + multistate_cluster.update_attribute(0x0055, 255) # 255 corresponds to release + assert len(mock_listener.zha_send_events) == 4 + assert mock_listener.zha_send_events[3][0] == "release" + + # Test 5: Verify private cluster attributes + assert hasattr(private_cluster.attributes, "cancel_bouble_click") + attr = private_cluster.attributes.cancel_bouble_click + assert attr.id == 0x0000 + assert attr.type == int # Corresponds to t.uint8_t From e25a8382f0a4bc5d2acbaf5225d4c7ead4f51077 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 25 Sep 2025 00:45:14 +0000 Subject: [PATCH 16/31] Apply pre-commit auto fixes --- tests/test_third_button.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/tests/test_third_button.py b/tests/test_third_button.py index b17e237f2b..42e6b665d0 100644 --- a/tests/test_third_button.py +++ b/tests/test_third_button.py @@ -1,20 +1,21 @@ import pytest -from tests.common import ClusterListener import zhaquirks from zhaquirks.thirdreality.button_v2 import ( MultistateInputCluster, ThirdRealityButtonCluster, ) + # Create a mock listener to capture ZHA events class MockListener: def __init__(self): self.zha_send_events = [] - + def zha_send_event(self, action, event_args): self.zha_send_events.append((action, event_args)) + zhaquirks.setup() @@ -26,44 +27,44 @@ async def test_third_reality_button_v2(zigpy_device_from_v2_quirk, manufacturer, """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 = None private_cluster = None - + for cluster in device.endpoints[1].in_clusters.values(): if isinstance(cluster, MultistateInputCluster): multistate_cluster = cluster if cluster.cluster_id == ThirdRealityButtonCluster.cluster_id: private_cluster = cluster - + assert multistate_cluster is not None, "MultistateInputCluster not found" assert private_cluster is not None, "ThirdRealityButtonCluster 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 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 multistate_cluster.update_attribute(0x0055, 2) # 2 corresponds to double click assert len(mock_listener.zha_send_events) == 2 assert mock_listener.zha_send_events[1][0] == "double" - + # Test 3: Verify hold event conversion multistate_cluster.update_attribute(0x0055, 0) # 0 corresponds to hold assert len(mock_listener.zha_send_events) == 3 assert mock_listener.zha_send_events[2][0] == "hold" - + # Test 4: Verify release event conversion multistate_cluster.update_attribute(0x0055, 255) # 255 corresponds to release assert len(mock_listener.zha_send_events) == 4 assert mock_listener.zha_send_events[3][0] == "release" - + # Test 5: Verify private cluster attributes assert hasattr(private_cluster.attributes, "cancel_bouble_click") attr = private_cluster.attributes.cancel_bouble_click From e275a43046fec983835d31e95a4db971ba36e0fc Mon Sep 17 00:00:00 2001 From: support <117250319+3reality-support@users.noreply.github.com> Date: Thu, 25 Sep 2025 09:20:59 +0800 Subject: [PATCH 17/31] Update test_third_button.py --- tests/test_third_button.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/tests/test_third_button.py b/tests/test_third_button.py index 42e6b665d0..e7ceea46ec 100644 --- a/tests/test_third_button.py +++ b/tests/test_third_button.py @@ -1,15 +1,19 @@ +"""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, - ThirdRealityButtonCluster, ) # Create a mock listener to capture ZHA events class MockListener: + """Simulate listener class, used to capture and store ZHA (Zigbee Home Automation) events.""" + def __init__(self): + """Initialize listener instance and create an empty list to store ZHA events.""" self.zha_send_events = [] def zha_send_event(self, action, event_args): @@ -30,16 +34,12 @@ async def test_third_reality_button_v2(zigpy_device_from_v2_quirk, manufacturer, # Find the MultistateInputCluster multistate_cluster = None - private_cluster = None for cluster in device.endpoints[1].in_clusters.values(): if isinstance(cluster, MultistateInputCluster): multistate_cluster = cluster - if cluster.cluster_id == ThirdRealityButtonCluster.cluster_id: - private_cluster = cluster assert multistate_cluster is not None, "MultistateInputCluster not found" - assert private_cluster is not None, "ThirdRealityButtonCluster not found" # Create mock listener and register it with the cluster mock_listener = MockListener() @@ -64,9 +64,3 @@ async def test_third_reality_button_v2(zigpy_device_from_v2_quirk, manufacturer, multistate_cluster.update_attribute(0x0055, 255) # 255 corresponds to release assert len(mock_listener.zha_send_events) == 4 assert mock_listener.zha_send_events[3][0] == "release" - - # Test 5: Verify private cluster attributes - assert hasattr(private_cluster.attributes, "cancel_bouble_click") - attr = private_cluster.attributes.cancel_bouble_click - assert attr.id == 0x0000 - assert attr.type == int # Corresponds to t.uint8_t From c5b8f9a73d8cef2695aab7c822e0d1f998233f6e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 25 Sep 2025 01:23:51 +0000 Subject: [PATCH 18/31] Apply pre-commit auto fixes --- tests/test_third_button.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_third_button.py b/tests/test_third_button.py index e7ceea46ec..97fa754bcc 100644 --- a/tests/test_third_button.py +++ b/tests/test_third_button.py @@ -3,9 +3,7 @@ import pytest import zhaquirks -from zhaquirks.thirdreality.button_v2 import ( - MultistateInputCluster, -) +from zhaquirks.thirdreality.button_v2 import MultistateInputCluster # Create a mock listener to capture ZHA events From 86ac6c40ebbf18cbe73f288b3801f90f71fc02c9 Mon Sep 17 00:00:00 2001 From: support <117250319+3reality-support@users.noreply.github.com> Date: Thu, 25 Sep 2025 09:30:13 +0800 Subject: [PATCH 19/31] Update test_third_button.py --- tests/test_third_button.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_third_button.py b/tests/test_third_button.py index 97fa754bcc..427269017d 100644 --- a/tests/test_third_button.py +++ b/tests/test_third_button.py @@ -15,6 +15,11 @@ def __init__(self): self.zha_send_events = [] def zha_send_event(self, action, event_args): + """The method of recording ZHA events. + Parameters: + Action: The type of action for an event + Event_args: Relevant parameters of the event + """ self.zha_send_events.append((action, event_args)) From 44748cb6a03ac8200d307c7d38ea48d154e51621 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 25 Sep 2025 01:30:24 +0000 Subject: [PATCH 20/31] Apply pre-commit auto fixes --- tests/test_third_button.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_third_button.py b/tests/test_third_button.py index 427269017d..fd12ba12da 100644 --- a/tests/test_third_button.py +++ b/tests/test_third_button.py @@ -16,9 +16,11 @@ def __init__(self): def zha_send_event(self, action, event_args): """The method of recording ZHA events. + Parameters: Action: The type of action for an event Event_args: Relevant parameters of the event + """ self.zha_send_events.append((action, event_args)) From 4a8b81573fba208736507c5d038eaf12593ae492 Mon Sep 17 00:00:00 2001 From: support <117250319+3reality-support@users.noreply.github.com> Date: Thu, 25 Sep 2025 09:32:23 +0800 Subject: [PATCH 21/31] Update test_third_button.py --- tests/test_third_button.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/test_third_button.py b/tests/test_third_button.py index fd12ba12da..0dfcfe2901 100644 --- a/tests/test_third_button.py +++ b/tests/test_third_button.py @@ -15,13 +15,12 @@ def __init__(self): self.zha_send_events = [] def zha_send_event(self, action, event_args): - """The method of recording ZHA events. + """Record ZHA events. - Parameters: - Action: The type of action for an event - Event_args: Relevant parameters of the event - - """ + Args: + action: The type of action for the event. + event_args: Relevant parameters of the event. + """ self.zha_send_events.append((action, event_args)) From 58b6ed2a51da0adbd92fa4d5fe463cddeb1f5480 Mon Sep 17 00:00:00 2001 From: support <117250319+3reality-support@users.noreply.github.com> Date: Thu, 25 Sep 2025 09:34:59 +0800 Subject: [PATCH 22/31] Update test_third_button.py --- tests/test_third_button.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_third_button.py b/tests/test_third_button.py index 0dfcfe2901..a64c3b89af 100644 --- a/tests/test_third_button.py +++ b/tests/test_third_button.py @@ -15,12 +15,12 @@ def __init__(self): self.zha_send_events = [] def zha_send_event(self, action, event_args): - """Record ZHA events. + """Record ZHA events. - Args: - action: The type of action for the event. - event_args: Relevant parameters of the event. - """ + Args: + action: The type of action for the event. + event_args: Relevant parameters of the event. + """ self.zha_send_events.append((action, event_args)) From b95f82c3d760a90d498e1130d2e15be2235d6d53 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 25 Sep 2025 01:35:08 +0000 Subject: [PATCH 23/31] Apply pre-commit auto fixes --- tests/test_third_button.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_third_button.py b/tests/test_third_button.py index a64c3b89af..195c7e42ea 100644 --- a/tests/test_third_button.py +++ b/tests/test_third_button.py @@ -20,6 +20,7 @@ def zha_send_event(self, action, event_args): Args: action: The type of action for the event. event_args: Relevant parameters of the event. + """ self.zha_send_events.append((action, event_args)) From 9f0034f735b89c8b51c746e212aa456a9957691d Mon Sep 17 00:00:00 2001 From: support <117250319+3reality-support@users.noreply.github.com> Date: Thu, 25 Sep 2025 09:43:13 +0800 Subject: [PATCH 24/31] Update test_third_button.py --- tests/test_third_button.py | 39 +++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/tests/test_third_button.py b/tests/test_third_button.py index 195c7e42ea..4e264b19fc 100644 --- a/tests/test_third_button.py +++ b/tests/test_third_button.py @@ -6,21 +6,19 @@ from zhaquirks.thirdreality.button_v2 import MultistateInputCluster -# Create a mock listener to capture ZHA events class MockListener: - """Simulate listener class, used to capture and store ZHA (Zigbee Home Automation) events.""" + """Simulate listener class for capturing ZHA events.""" def __init__(self): - """Initialize listener instance and create an empty list to store ZHA events.""" + """Initialize listener with empty event list.""" self.zha_send_events = [] def zha_send_event(self, action, event_args): """Record ZHA events. Args: - action: The type of action for the event. - event_args: Relevant parameters of the event. - + 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)) @@ -30,7 +28,7 @@ def zha_send_event(self, action, event_args): @pytest.mark.parametrize( "manufacturer, model", - ["Third Reality, Inc", "3RSB22BZ"], + [("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.""" @@ -38,12 +36,11 @@ async def test_third_reality_button_v2(zigpy_device_from_v2_quirk, manufacturer, device = zigpy_device_from_v2_quirk(manufacturer, model) # Find the MultistateInputCluster - multistate_cluster = None - - for cluster in device.endpoints[1].in_clusters.values(): - if isinstance(cluster, MultistateInputCluster): - multistate_cluster = cluster - + 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 @@ -51,21 +48,25 @@ async def test_third_reality_button_v2(zigpy_device_from_v2_quirk, manufacturer, 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) == 2 - assert mock_listener.zha_send_events[1][0] == "double" + 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) == 3 - assert mock_listener.zha_send_events[2][0] == "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) == 4 - assert mock_listener.zha_send_events[3][0] == "release" + assert len(mock_listener.zha_send_events) == 1 + assert mock_listener.zha_send_events[0][0] == "release" From cfd1145cc6bb89dcf59bc4eb47c9c8a9c2aa15f6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 25 Sep 2025 01:43:20 +0000 Subject: [PATCH 25/31] Apply pre-commit auto fixes --- tests/test_third_button.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/test_third_button.py b/tests/test_third_button.py index 4e264b19fc..23f9d13a47 100644 --- a/tests/test_third_button.py +++ b/tests/test_third_button.py @@ -19,6 +19,7 @@ def zha_send_event(self, action, event_args): 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)) @@ -37,9 +38,12 @@ async def test_third_reality_button_v2(zigpy_device_from_v2_quirk, manufacturer, # Find the MultistateInputCluster multistate_cluster = next( - (cluster for cluster in device.endpoints[1].in_clusters.values() - if isinstance(cluster, MultistateInputCluster)), - None + ( + cluster + for cluster in device.endpoints[1].in_clusters.values() + if isinstance(cluster, MultistateInputCluster) + ), + None, ) assert multistate_cluster is not None, "MultistateInputCluster not found" From 63a1b15579ac94cb795414512597d441347131bc Mon Sep 17 00:00:00 2001 From: support <117250319+3reality-support@users.noreply.github.com> Date: Thu, 25 Sep 2025 09:46:05 +0800 Subject: [PATCH 26/31] Update test_third_button.py --- tests/test_third_button.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_third_button.py b/tests/test_third_button.py index 23f9d13a47..f99c0c4f56 100644 --- a/tests/test_third_button.py +++ b/tests/test_third_button.py @@ -3,7 +3,7 @@ import pytest import zhaquirks -from zhaquirks.thirdreality.button_v2 import MultistateInputCluster +from zhaquirks.thirdreality.button-v2 import MultistateInputCluster class MockListener: From 60447b77fc0640efaa599630349f8ba404bb1095 Mon Sep 17 00:00:00 2001 From: jintj Date: Thu, 25 Sep 2025 09:49:41 +0800 Subject: [PATCH 27/31] update --- tests/test_third_button.py | 2 +- zhaquirks/thirdreality/{button-v2.py => button_v2.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename zhaquirks/thirdreality/{button-v2.py => button_v2.py} (100%) diff --git a/tests/test_third_button.py b/tests/test_third_button.py index f99c0c4f56..23f9d13a47 100644 --- a/tests/test_third_button.py +++ b/tests/test_third_button.py @@ -3,7 +3,7 @@ import pytest import zhaquirks -from zhaquirks.thirdreality.button-v2 import MultistateInputCluster +from zhaquirks.thirdreality.button_v2 import MultistateInputCluster class MockListener: diff --git a/zhaquirks/thirdreality/button-v2.py b/zhaquirks/thirdreality/button_v2.py similarity index 100% rename from zhaquirks/thirdreality/button-v2.py rename to zhaquirks/thirdreality/button_v2.py From 183d16b41192ffdf0a7cd435596ffac052ee300e Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Tue, 28 Oct 2025 20:35:16 +0100 Subject: [PATCH 28/31] Clean up quirk, replace old v1 quirk --- tests/test_third_button.py | 2 +- zhaquirks/thirdreality/button.py | 134 +++++++++++----------------- zhaquirks/thirdreality/button_v2.py | 92 ------------------- 3 files changed, 52 insertions(+), 176 deletions(-) delete mode 100644 zhaquirks/thirdreality/button_v2.py diff --git a/tests/test_third_button.py b/tests/test_third_button.py index 23f9d13a47..ff957acfa7 100644 --- a/tests/test_third_button.py +++ b/tests/test_third_button.py @@ -3,7 +3,7 @@ import pytest import zhaquirks -from zhaquirks.thirdreality.button_v2 import MultistateInputCluster +from zhaquirks.thirdreality.button import MultistateInputCluster class MockListener: 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() +) diff --git a/zhaquirks/thirdreality/button_v2.py b/zhaquirks/thirdreality/button_v2.py deleted file mode 100644 index e0b51efff7..0000000000 --- a/zhaquirks/thirdreality/button_v2.py +++ /dev/null @@ -1,92 +0,0 @@ -"""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() -) From 60f7652932be29297a78e35d432f95d25889e5f1 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Tue, 28 Oct 2025 20:38:33 +0100 Subject: [PATCH 29/31] Remove from quirks v1 `KNOWN_DUPLICATE_TRIGGERS` test --- tests/test_quirks.py | 6 ------ 1 file changed, 6 deletions(-) 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), - ] - ], } From 68fcff74d72ce6a567052f6f309fb2d3564ea6c0 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Tue, 28 Oct 2025 20:51:28 +0100 Subject: [PATCH 30/31] Move and clean up test --- tests/test_third_button.py | 76 -------------------------------------- tests/test_thirdreality.py | 56 ++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 76 deletions(-) delete mode 100644 tests/test_third_button.py diff --git a/tests/test_third_button.py b/tests/test_third_button.py deleted file mode 100644 index ff957acfa7..0000000000 --- a/tests/test_third_button.py +++ /dev/null @@ -1,76 +0,0 @@ -"""Test the module for the "third button" function to verify the ZHA event capture logic.""" - -import pytest - -import zhaquirks -from zhaquirks.thirdreality.button 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/tests/test_thirdreality.py b/tests/test_thirdreality.py index 6d0f31fe71..caa12a5437 100644 --- a/tests/test_thirdreality.py +++ b/tests/test_thirdreality.py @@ -1,15 +1,36 @@ """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() +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)) + + @pytest.mark.parametrize("quirk", (zhaquirks.thirdreality.night_light.Nightlight,)) async def test_third_reality_nightlight(zigpy_device_from_quirk, quirk): """Test Third Reality night light forwarding motion attribute to IasZone cluster.""" @@ -36,3 +57,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} + ) From 61dc2e5e70d780c9f4e5af3a7da443e93d491048 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Tue, 28 Oct 2025 20:53:16 +0100 Subject: [PATCH 31/31] Remove unnecessary MockListener from test --- tests/test_thirdreality.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/tests/test_thirdreality.py b/tests/test_thirdreality.py index caa12a5437..36beef1f7e 100644 --- a/tests/test_thirdreality.py +++ b/tests/test_thirdreality.py @@ -13,24 +13,6 @@ zhaquirks.setup() -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)) - - @pytest.mark.parametrize("quirk", (zhaquirks.thirdreality.night_light.Nightlight,)) async def test_third_reality_nightlight(zigpy_device_from_quirk, quirk): """Test Third Reality night light forwarding motion attribute to IasZone cluster."""