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
26 changes: 26 additions & 0 deletions tests/test_develco.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
"""Tests for Develco/Frient."""

from unittest import mock
from unittest.mock import MagicMock

import zigpy.types as t
from zigpy.zcl import ClusterType, foundation
from zigpy.zcl.clusters.smartenergy import Metering

from tests.common import ClusterListener
import zhaquirks
from zhaquirks.develco.motion import FrientTamperIasZone

zhaquirks.setup()

Expand Down Expand Up @@ -187,3 +189,27 @@ async def test_mfg_cluster_events(zigpy_device_from_v2_quirk):
assert (
metering_cluster.get(Metering.AttributeDefs.current_summ_delivered.id) == 1234
)


def test_frient_tamper_zone_updates_tamper_state():
"""Verify tamper attribute mirrors zone status bit 2."""
endpoint = MagicMock()
cluster = FrientTamperIasZone(endpoint)

cluster._update_attribute(
FrientTamperIasZone.AttributeDefs.zone_status.id,
0x0000,
)
assert cluster._attr_cache[FrientTamperIasZone.AttributeDefs.tamper.id] is False

cluster._update_attribute(
FrientTamperIasZone.AttributeDefs.zone_status.id,
0x0004,
)
assert cluster._attr_cache[FrientTamperIasZone.AttributeDefs.tamper.id] is True

cluster._update_attribute(
FrientTamperIasZone.AttributeDefs.zone_status.id,
0x0000,
)
assert cluster._attr_cache[FrientTamperIasZone.AttributeDefs.tamper.id] is False
200 changes: 193 additions & 7 deletions zhaquirks/develco/motion.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,207 @@
"""Develco Motion Sensor Pro."""
"""frient Motion Sensor/Pro/PET."""

from typing import Final

from zigpy.quirks import CustomCluster
from zigpy.quirks.v2 import QuirkBuilder
from zigpy.quirks.v2.homeassistant import UnitOfTime
import zigpy.types as t
from zigpy.types import uint16_t
from zigpy.zcl.clusters.general import BinaryInput
from zigpy.zcl.clusters.measurement import OccupancySensing
from zigpy.zcl.clusters.security import IasZone
from zigpy.zcl.foundation import ZCLAttributeDef

from zhaquirks.develco import DEVELCO, FRIENT, DevelcoIasZone, DevelcoPowerConfiguration


class FrientOccupancySensing(CustomCluster, OccupancySensing):
"""Custom occupancy sensing cluster for frient motion sensors."""

class AttributeDefs(OccupancySensing.AttributeDefs):
"""Attribute definitions."""

pir_o_to_u_delay: Final = ZCLAttributeDef(
id=0x0010,
type=uint16_t,
access="rw",
)

pir_u_to_o_delay: Final = ZCLAttributeDef(
id=0x0011,
type=uint16_t,
access="rw",
)


class FrientTamperIasZone(DevelcoIasZone):
"""Custom IAS Zone cluster for frient motion sensors with tamper support."""

def _update_attribute(self, attrid, value):
super()._update_attribute(attrid, value)
if attrid == self.AttributeDefs.zone_status.id:
# Update tamper state from zone_status bit 2
tamper_state = bool(value & 0b00000100)
super()._update_attribute(self.AttributeDefs.tamper.id, tamper_state)

class AttributeDefs(IasZone.AttributeDefs):
"""Attribute definitions."""

tamper: Final = ZCLAttributeDef(
id=0xFFF2, # Custom attribute ID
type=t.Bool,
)


class FrientPETSensitivityIasZone(DevelcoIasZone):
"""Custom IAS Zone cluster for frient PET motion sensor with sensitivity levels."""

class AttributeDefs(IasZone.AttributeDefs):
"""Attribute definitions."""

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",
)


# MOSZB-140 (Motion Sensor Pro) - has tamper sensor, no sensitivity levels
(
QuirkBuilder("frient A/S", "MOSZB-153")
QuirkBuilder(FRIENT, "MOSZB-140")
.applies_to(DEVELCO, "MOSZB-140")
.applies_to(FRIENT, "MOSZB-140")
.replaces(DevelcoPowerConfiguration, endpoint_id=35)
.replaces(FrientTamperIasZone, endpoint_id=35)
.replaces(
FrientOccupancySensing, cluster_id=OccupancySensing.cluster_id, endpoint_id=34
)
.binary_sensor(
attribute_name="tamper",
cluster_id=IasZone.cluster_id,
endpoint_id=35,
entity_type="tamper",
translation_key="tamper",
fallback_name="Tamper",
)
.number(
attribute_name="pir_o_to_u_delay",
cluster_id=OccupancySensing.cluster_id,
endpoint_id=34,
min_value=0,
max_value=65535,
step=1,
translation_key="occupancy_delay",
fallback_name="Occupied to unoccupied delay",
unit=UnitOfTime.SECONDS,
)
.number(
attribute_name="pir_u_to_o_delay",
cluster_id=OccupancySensing.cluster_id,
endpoint_id=34,
min_value=0,
max_value=65535,
step=1,
translation_key="unoccupancy_delay",
fallback_name="Unoccupied to occupied delay",
unit=UnitOfTime.SECONDS,
)
.prevent_default_entity_creation(endpoint_id=35, cluster_id=BinaryInput.cluster_id)
.prevent_default_entity_creation(endpoint_id=40)
.prevent_default_entity_creation(endpoint_id=41)
.add_to_registry()
)

# MOSZB-141 (Motion Sensor) - no tamper sensor, no sensitivity levels
(
QuirkBuilder(FRIENT, "MOSZB-141")
.applies_to(DEVELCO, "MOSZB-141")
.replaces(DevelcoPowerConfiguration, endpoint_id=35)
.replaces(DevelcoIasZone, endpoint_id=35)
# This entity does not do anything
.replaces(
FrientOccupancySensing, cluster_id=OccupancySensing.cluster_id, endpoint_id=34
)
.number(
attribute_name="pir_o_to_u_delay",
cluster_id=OccupancySensing.cluster_id,
endpoint_id=34,
min_value=0,
max_value=65535,
step=1,
translation_key="occupancy_delay",
fallback_name="Occupied to unoccupied delay",
unit=UnitOfTime.SECONDS,
)
.number(
attribute_name="pir_u_to_o_delay",
cluster_id=OccupancySensing.cluster_id,
endpoint_id=34,
min_value=0,
max_value=65535,
step=1,
translation_key="unoccupancy_delay",
fallback_name="Unoccupied to occupied delay",
unit=UnitOfTime.SECONDS,
)
.prevent_default_entity_creation(endpoint_id=35, cluster_id=BinaryInput.cluster_id)
.prevent_default_entity_creation(endpoint_id=40)
.prevent_default_entity_creation(endpoint_id=41)
.add_to_registry()
)

# MOSZB-153 (Motion Sensor 2 PET) - no tamper sensor, but has sensitivity levels
(
QuirkBuilder(FRIENT, "MOSZB-153")
.replaces(DevelcoPowerConfiguration, endpoint_id=35)
.replaces(FrientPETSensitivityIasZone, endpoint_id=35)
.replaces(
FrientOccupancySensing, cluster_id=OccupancySensing.cluster_id, endpoint_id=34
)
.number(
attribute_name="current_zone_sensitivity_level",
cluster_id=IasZone.cluster_id,
endpoint_id=35,
min_value=1,
max_value=4,
step=1,
translation_key="sensitivity_level",
fallback_name="Sensitivity level (1-4)",
)
.number(
attribute_name="pir_o_to_u_delay",
cluster_id=OccupancySensing.cluster_id,
endpoint_id=34,
min_value=0,
max_value=65535,
step=1,
translation_key="occupancy_delay",
fallback_name="Occupied to unoccupied delay",
unit=UnitOfTime.SECONDS,
)
.number(
attribute_name="pir_u_to_o_delay",
cluster_id=OccupancySensing.cluster_id,
endpoint_id=34,
min_value=0,
max_value=65535,
step=1,
translation_key="unoccupancy_delay",
fallback_name="Unoccupied to occupied delay",
unit=UnitOfTime.SECONDS,
)
.sensor(
attribute_name="number_of_zone_sensitivity_levels_supported",
cluster_id=IasZone.cluster_id,
endpoint_id=35,
translation_key="sensitivity_levels_supported",
fallback_name="Sensitivity levels supported",
)
.prevent_default_entity_creation(endpoint_id=35, cluster_id=BinaryInput.cluster_id)
# This endpoint holds only an occupancy cluster that updates unusably slowly
.prevent_default_entity_creation(endpoint_id=34)
# These endpoints are duplicates of 35 and do not create useful entities
.prevent_default_entity_creation(endpoint_id=40)
.prevent_default_entity_creation(endpoint_id=41)
.add_to_registry()
Expand Down
Loading