From a4852b23d9370880900a4a2f3fbad40658de2a24 Mon Sep 17 00:00:00 2001 From: PrintingRat <138309640+PrintingRat@users.noreply.github.com> Date: Sun, 26 Oct 2025 15:39:01 +0100 Subject: [PATCH 1/6] Add support for frient Vibration Sensor WISZB-137 --- zhaquirks/develco/vibration.py | 176 +++++++++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 zhaquirks/develco/vibration.py diff --git a/zhaquirks/develco/vibration.py b/zhaquirks/develco/vibration.py new file mode 100644 index 0000000000..c152fa5ebe --- /dev/null +++ b/zhaquirks/develco/vibration.py @@ -0,0 +1,176 @@ +"""frient Vibration Sensor WISZB-137""" + +from typing import Final + +import zigpy.types as t +from zigpy.quirks import CustomCluster +from zigpy.quirks.v2 import QuirkBuilder, ReportingConfig, SensorStateClass +from zigpy.zcl.clusters.general import BinaryInput +from zigpy.zcl.clusters.security import IasZone +from zigpy.zcl.foundation import BaseAttributeDefs, ZCLAttributeDef + +from zhaquirks.develco import DevelcoIasZone + + +class FrientAccelerationMeasurement(CustomCluster): + """Manufacturer specific acceleration measurement cluster.""" + + cluster_id = 0xFC04 + manufacturer_id_override = 0x1015 + + class AttributeDefs(BaseAttributeDefs): + """Attribute definitions.""" + + measured_value_x: Final = ZCLAttributeDef( + id=0x0000, + type=t.int16s, + is_manufacturer_specific=True, + access="rp", + ) + + measured_value_y: Final = ZCLAttributeDef( + id=0x0001, + type=t.int16s, + is_manufacturer_specific=True, + access="rp", + ) + + measured_value_z: Final = ZCLAttributeDef( + id=0x0002, + type=t.int16s, + is_manufacturer_specific=True, + access="rp", + ) + + +class FrientVibrationIasZone(DevelcoIasZone): + """Custom IAS Zone cluster for frient vibration sensor with movement and vibration detection.""" + + def _update_attribute(self, attrid, value): + super()._update_attribute(attrid, value) + if attrid == self.AttributeDefs.zone_status.id: + # According to the manual: + # Bit0: Alarm 1 (Movement) + # Bit1: Alarm 2 (Vibration) + # Bit3: Battery + # Bit4: Supervision reports + # Bit5: Restore reports + movement_state = bool(value & 0b00000001) # Bit 0 + vibration_state = bool(value & 0b00000010) # Bit 1 + + super()._update_attribute(self.AttributeDefs.movement.id, movement_state) + super()._update_attribute(self.AttributeDefs.vibration.id, vibration_state) + + class AttributeDefs(IasZone.AttributeDefs): + """Attribute definitions.""" + + movement: Final = ZCLAttributeDef( + id=0xFFF0, # Custom attribute ID for movement detection + type=t.Bool, + ) + + vibration: Final = ZCLAttributeDef( + id=0xFFF1, # Custom attribute ID for vibration detection + type=t.Bool, + ) + + number_of_zone_sensitivity_levels_supported: Final = ZCLAttributeDef( + id=0x0012, + type=t.uint8_t, + access="r", + ) + + current_zone_sensitivity_level: Final = ZCLAttributeDef( + id=0x0013, + type=t.uint8_t, + access="rw", + ) + + +# WISZB-137 (Vibration Sensor) +( + QuirkBuilder("frient A/S", "WISZB-137") + .applies_to("Develco Products A/S", "WISZB-137") + .replaces(FrientAccelerationMeasurement, endpoint_id=45) + .replaces(FrientVibrationIasZone, endpoint_id=45) + .binary_sensor( + attribute_name="vibration", + cluster_id=IasZone.cluster_id, + endpoint_id=45, + entity_type="vibration", + translation_key="vibration", + fallback_name="Vibration", + ) + .binary_sensor( + attribute_name="movement", + cluster_id=IasZone.cluster_id, + endpoint_id=45, + entity_type="motion", + translation_key="movement", + fallback_name="Movement", + ) + .sensor( + attribute_name="number_of_zone_sensitivity_levels_supported", + cluster_id=IasZone.cluster_id, + endpoint_id=45, + translation_key="sensitivity_levels_supported", + fallback_name="Sensitivity Levels Supported", + ) + .number( + attribute_name="current_zone_sensitivity_level", + cluster_id=IasZone.cluster_id, + endpoint_id=45, + min_value=1, + max_value=15, + step=1, + translation_key="sensitivity_level", + fallback_name="Sensitivity Level (1-15)", + ) + .sensor( + attribute_name=FrientAccelerationMeasurement.AttributeDefs.measured_value_x.name, + cluster_id=FrientAccelerationMeasurement.cluster_id, + endpoint_id=45, + state_class=SensorStateClass.MEASUREMENT, + unit="g", + divisor=1000, + translation_key="acceleration_x", + fallback_name="Acceleration X", + reporting_config=ReportingConfig( + min_interval=0, + max_interval=900, + reportable_change=50, + ), + ) + .sensor( + attribute_name=FrientAccelerationMeasurement.AttributeDefs.measured_value_y.name, + cluster_id=FrientAccelerationMeasurement.cluster_id, + endpoint_id=45, + state_class=SensorStateClass.MEASUREMENT, + unit="g", + divisor=1000, + translation_key="acceleration_y", + fallback_name="Acceleration Y", + reporting_config=ReportingConfig( + min_interval=0, + max_interval=900, + reportable_change=50, + ), + ) + .sensor( + attribute_name=FrientAccelerationMeasurement.AttributeDefs.measured_value_z.name, + cluster_id=FrientAccelerationMeasurement.cluster_id, + endpoint_id=45, + state_class=SensorStateClass.MEASUREMENT, + unit="g", + divisor=1000, + translation_key="acceleration_z", + fallback_name="Acceleration Z", + reporting_config=ReportingConfig( + min_interval=0, + max_interval=900, + reportable_change=50, + ), + ) + .prevent_default_entity_creation(endpoint_id=45, cluster_id=BinaryInput.cluster_id) + .add_to_registry() +) \ No newline at end of file From 1cfd9704b3e44570daed71009567ba488dfa3ffe Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 26 Oct 2025 14:42:44 +0000 Subject: [PATCH 2/6] Apply pre-commit auto fixes --- zhaquirks/develco/vibration.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/zhaquirks/develco/vibration.py b/zhaquirks/develco/vibration.py index c152fa5ebe..538e8e3b8c 100644 --- a/zhaquirks/develco/vibration.py +++ b/zhaquirks/develco/vibration.py @@ -2,9 +2,9 @@ from typing import Final -import zigpy.types as t from zigpy.quirks import CustomCluster from zigpy.quirks.v2 import QuirkBuilder, ReportingConfig, SensorStateClass +import zigpy.types as t from zigpy.zcl.clusters.general import BinaryInput from zigpy.zcl.clusters.security import IasZone from zigpy.zcl.foundation import BaseAttributeDefs, ZCLAttributeDef @@ -57,29 +57,29 @@ def _update_attribute(self, attrid, value): # Bit5: Restore reports movement_state = bool(value & 0b00000001) # Bit 0 vibration_state = bool(value & 0b00000010) # Bit 1 - + super()._update_attribute(self.AttributeDefs.movement.id, movement_state) super()._update_attribute(self.AttributeDefs.vibration.id, vibration_state) class AttributeDefs(IasZone.AttributeDefs): """Attribute definitions.""" - + movement: Final = ZCLAttributeDef( id=0xFFF0, # Custom attribute ID for movement detection type=t.Bool, ) - + vibration: Final = ZCLAttributeDef( id=0xFFF1, # Custom attribute ID for vibration detection type=t.Bool, ) - + number_of_zone_sensitivity_levels_supported: Final = ZCLAttributeDef( id=0x0012, type=t.uint8_t, access="r", ) - + current_zone_sensitivity_level: Final = ZCLAttributeDef( id=0x0013, type=t.uint8_t, @@ -98,7 +98,7 @@ class AttributeDefs(IasZone.AttributeDefs): cluster_id=IasZone.cluster_id, endpoint_id=45, entity_type="vibration", - translation_key="vibration", + translation_key="vibration", fallback_name="Vibration", ) .binary_sensor( @@ -173,4 +173,4 @@ class AttributeDefs(IasZone.AttributeDefs): ) .prevent_default_entity_creation(endpoint_id=45, cluster_id=BinaryInput.cluster_id) .add_to_registry() -) \ No newline at end of file +) From eca6d9c2fabd57ad6ca46bad4dcfaaed2ad49eb4 Mon Sep 17 00:00:00 2001 From: PrintingRat <138309640+PrintingRat@users.noreply.github.com> Date: Sun, 26 Oct 2025 15:47:08 +0100 Subject: [PATCH 3/6] Missing period --- zhaquirks/develco/vibration.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/zhaquirks/develco/vibration.py b/zhaquirks/develco/vibration.py index 538e8e3b8c..9400938813 100644 --- a/zhaquirks/develco/vibration.py +++ b/zhaquirks/develco/vibration.py @@ -1,4 +1,4 @@ -"""frient Vibration Sensor WISZB-137""" +"""frient Vibration Sensor WISZB-137.""" from typing import Final @@ -174,3 +174,4 @@ class AttributeDefs(IasZone.AttributeDefs): .prevent_default_entity_creation(endpoint_id=45, cluster_id=BinaryInput.cluster_id) .add_to_registry() ) + From 2ef275b4489f2ce0ae58b3803f611d17fa85765c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 26 Oct 2025 14:47:15 +0000 Subject: [PATCH 4/6] Apply pre-commit auto fixes --- zhaquirks/develco/vibration.py | 1 - 1 file changed, 1 deletion(-) diff --git a/zhaquirks/develco/vibration.py b/zhaquirks/develco/vibration.py index 9400938813..6b25ce60aa 100644 --- a/zhaquirks/develco/vibration.py +++ b/zhaquirks/develco/vibration.py @@ -174,4 +174,3 @@ class AttributeDefs(IasZone.AttributeDefs): .prevent_default_entity_creation(endpoint_id=45, cluster_id=BinaryInput.cluster_id) .add_to_registry() ) - From dbe942830459210dddc5e7c57f23ad8dea280aa0 Mon Sep 17 00:00:00 2001 From: PrintingRat <138309640+PrintingRat@users.noreply.github.com> Date: Wed, 29 Oct 2025 18:40:36 +0100 Subject: [PATCH 5/6] Fixing fallback names + including tests --- tests/test_develco.py | 84 ++++++++++++++++++++++++++++++++++ zhaquirks/develco/vibration.py | 12 ++--- 2 files changed, 90 insertions(+), 6 deletions(-) diff --git a/tests/test_develco.py b/tests/test_develco.py index dd95d12130..f34875a9d1 100644 --- a/tests/test_develco.py +++ b/tests/test_develco.py @@ -8,6 +8,10 @@ from tests.common import ClusterListener import zhaquirks +from zhaquirks.develco.vibration import ( + FrientAccelerationMeasurement, + FrientVibrationIasZone, +) zhaquirks.setup() @@ -187,3 +191,83 @@ async def test_mfg_cluster_events(zigpy_device_from_v2_quirk): assert ( metering_cluster.get(Metering.AttributeDefs.current_summ_delivered.id) == 1234 ) + + +async def test_frient_vibration_zone_status(zigpy_device_from_v2_quirk): + """Ensure vibration IAS zone exposes movement and vibration state attributes.""" + + device = zigpy_device_from_v2_quirk( + "frient A/S", + "WISZB-137", + endpoint_ids=[1, 45], + cluster_ids={ + 45: { + FrientAccelerationMeasurement.cluster_id: ClusterType.Server, + FrientVibrationIasZone.cluster_id: ClusterType.Server, + } + }, + ) + + vibration_cluster = device.endpoints[45].ias_zone + assert isinstance(vibration_cluster, FrientVibrationIasZone) + + vibration_cluster._update_attribute( + vibration_cluster.AttributeDefs.zone_status.id, + 0b00000011, + ) + assert vibration_cluster.get(vibration_cluster.AttributeDefs.movement.id) is True + assert vibration_cluster.get(vibration_cluster.AttributeDefs.vibration.id) is True + + vibration_cluster._update_attribute( + vibration_cluster.AttributeDefs.zone_status.id, + 0, + ) + assert vibration_cluster.get(vibration_cluster.AttributeDefs.movement.id) is False + assert vibration_cluster.get(vibration_cluster.AttributeDefs.vibration.id) is False + + +async def test_frient_vibration_acceleration_attributes(zigpy_device_from_v2_quirk): + """Ensure acceleration cluster stores axis readings via custom attribute defs.""" + + device = zigpy_device_from_v2_quirk( + "frient A/S", + "WISZB-137", + endpoint_ids=[1, 45], + cluster_ids={ + 45: { + FrientAccelerationMeasurement.cluster_id: ClusterType.Server, + FrientVibrationIasZone.cluster_id: ClusterType.Server, + } + }, + ) + + acceleration_cluster = device.endpoints[45].in_clusters[ + FrientAccelerationMeasurement.cluster_id + ] + assert isinstance(acceleration_cluster, FrientAccelerationMeasurement) + + acceleration_cluster._update_attribute( + acceleration_cluster.AttributeDefs.measured_value_x.id, + 1500, + ) + acceleration_cluster._update_attribute( + acceleration_cluster.AttributeDefs.measured_value_y.id, + -500, + ) + acceleration_cluster._update_attribute( + acceleration_cluster.AttributeDefs.measured_value_z.id, + 0, + ) + + assert ( + acceleration_cluster.get(acceleration_cluster.AttributeDefs.measured_value_x.id) + == 1500 + ) + assert ( + acceleration_cluster.get(acceleration_cluster.AttributeDefs.measured_value_y.id) + == -500 + ) + assert ( + acceleration_cluster.get(acceleration_cluster.AttributeDefs.measured_value_z.id) + == 0 + ) diff --git a/zhaquirks/develco/vibration.py b/zhaquirks/develco/vibration.py index 6b25ce60aa..f2156d599f 100644 --- a/zhaquirks/develco/vibration.py +++ b/zhaquirks/develco/vibration.py @@ -114,7 +114,7 @@ class AttributeDefs(IasZone.AttributeDefs): cluster_id=IasZone.cluster_id, endpoint_id=45, translation_key="sensitivity_levels_supported", - fallback_name="Sensitivity Levels Supported", + fallback_name="Sensitivity levels supported", ) .number( attribute_name="current_zone_sensitivity_level", @@ -124,7 +124,7 @@ class AttributeDefs(IasZone.AttributeDefs): max_value=15, step=1, translation_key="sensitivity_level", - fallback_name="Sensitivity Level (1-15)", + fallback_name="Sensitivity level (1-15)", ) .sensor( attribute_name=FrientAccelerationMeasurement.AttributeDefs.measured_value_x.name, @@ -134,7 +134,7 @@ class AttributeDefs(IasZone.AttributeDefs): unit="g", divisor=1000, translation_key="acceleration_x", - fallback_name="Acceleration X", + fallback_name="Acceleration x", reporting_config=ReportingConfig( min_interval=0, max_interval=900, @@ -149,7 +149,7 @@ class AttributeDefs(IasZone.AttributeDefs): unit="g", divisor=1000, translation_key="acceleration_y", - fallback_name="Acceleration Y", + fallback_name="Acceleration y", reporting_config=ReportingConfig( min_interval=0, max_interval=900, @@ -164,7 +164,7 @@ class AttributeDefs(IasZone.AttributeDefs): unit="g", divisor=1000, translation_key="acceleration_z", - fallback_name="Acceleration Z", + fallback_name="Acceleration z", reporting_config=ReportingConfig( min_interval=0, max_interval=900, @@ -173,4 +173,4 @@ class AttributeDefs(IasZone.AttributeDefs): ) .prevent_default_entity_creation(endpoint_id=45, cluster_id=BinaryInput.cluster_id) .add_to_registry() -) +) \ No newline at end of file From 88a0b274eeda72b125c1fe443d4f1ad526e1a65e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 17:40:45 +0000 Subject: [PATCH 6/6] Apply pre-commit auto fixes --- zhaquirks/develco/vibration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zhaquirks/develco/vibration.py b/zhaquirks/develco/vibration.py index f2156d599f..b9be5eca33 100644 --- a/zhaquirks/develco/vibration.py +++ b/zhaquirks/develco/vibration.py @@ -173,4 +173,4 @@ class AttributeDefs(IasZone.AttributeDefs): ) .prevent_default_entity_creation(endpoint_id=45, cluster_id=BinaryInput.cluster_id) .add_to_registry() -) \ No newline at end of file +)