Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions tests/test_develco.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@

from tests.common import ClusterListener
import zhaquirks
from zhaquirks.develco.vibration import (
FrientAccelerationMeasurement,
FrientVibrationIasZone,
)

zhaquirks.setup()

Expand Down Expand Up @@ -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
)
176 changes: 176 additions & 0 deletions zhaquirks/develco/vibration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
"""frient Vibration Sensor WISZB-137."""

from typing import Final

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

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()
)
Loading