From 951710848b7ca653b8ce55bd316fb9483db97ad5 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 10 Apr 2025 13:45:33 -0400 Subject: [PATCH 01/12] Initial implementation --- zhaquirks/develco/intelligent_keypad.py | 34 ++ zhaquirks/develco/motion.py | 410 +----------------------- zhaquirks/develco/open_close.py | 220 +++---------- zhaquirks/develco/smart_button.py | 18 ++ zhaquirks/develco/smoke_alarm.py | 128 +------- 5 files changed, 115 insertions(+), 695 deletions(-) create mode 100644 zhaquirks/develco/intelligent_keypad.py create mode 100644 zhaquirks/develco/smart_button.py diff --git a/zhaquirks/develco/intelligent_keypad.py b/zhaquirks/develco/intelligent_keypad.py new file mode 100644 index 0000000000..6b9709c6be --- /dev/null +++ b/zhaquirks/develco/intelligent_keypad.py @@ -0,0 +1,34 @@ +"""Intelligent keypad.""" + +from zigpy.quirks.v2 import QuirkBuilder +from zigpy.quirks.v2.homeassistant.binary_sensor import BinarySensorDeviceClass +from zigpy.zcl.clusters.general import BinaryInput +from zigpy.zcl.clusters.security import IasWd, IasZone + +( + QuirkBuilder("frient A/S", "KEPZB-110") + .prevent_default_entity_creation(endpoint_id=35, cluster_id=BinaryInput.cluster_id) + .prevent_default_entity_creation(endpoint_id=35, cluster_id=IasZone.cluster_id) + .prevent_default_entity_creation( + endpoint_id=35, + cluster_id=IasWd.cluster_id, + function=lambda entity: entity.translation_key + in ( + "default_siren_tone", + "default_siren_level", + "default_strobe_level", + "default_strobe", + ), + ) + .binary_sensor( + endpoint_id=35, + cluster_id=IasZone.cluster_id, + attribute_name=IasZone.AttributeDefs.zone_status.name, + device_class=BinarySensorDeviceClass.TAMPER, + attribute_converter=lambda value: bool(value & IasZone.ZoneStatus.Tamper), + unique_id_suffix="tamper", + translation_key="tamper", + fallback_name="Tamper", + ) + .add_to_registry() +) diff --git a/zhaquirks/develco/motion.py b/zhaquirks/develco/motion.py index bb5f5e909d..758e37f8d1 100644 --- a/zhaquirks/develco/motion.py +++ b/zhaquirks/develco/motion.py @@ -1,398 +1,22 @@ """Develco Motion Sensor Pro.""" -from zigpy.profiles import zha -from zigpy.quirks import CustomDevice -from zigpy.zcl.clusters.general import ( - Basic, - BinaryInput, - Identify, - OnOff, - Ota, - PollControl, - PowerConfiguration, - Scenes, - Time, -) -from zigpy.zcl.clusters.measurement import ( - IlluminanceMeasurement, - OccupancySensing, - TemperatureMeasurement, -) -from zigpy.zcl.clusters.security import IasZone +from zigpy.quirks.v2 import QuirkBuilder +from zigpy.zcl.clusters.general import BinaryInput -from zhaquirks.const import ( - DEVICE_TYPE, - ENDPOINTS, - INPUT_CLUSTERS, - MODELS_INFO, - OUTPUT_CLUSTERS, - PROFILE_ID, -) from zhaquirks.develco import DEVELCO, FRIENT, DevelcoIasZone, DevelcoPowerConfiguration -MANUFACTURER = 0x1015 - - -class MOSZB140(CustomDevice): - """Custom device Develco Motion Sensor Pro.""" - - manufacturer_id_override = MANUFACTURER - - signature = { - # - # - # - # - # - # - # - MODELS_INFO: [(DEVELCO, "MOSZB-140"), (FRIENT, "MOSZB-140")], - ENDPOINTS: { - 1: { - PROFILE_ID: 0xC0C9, - DEVICE_TYPE: 1, - INPUT_CLUSTERS: [ - Identify.cluster_id, - Scenes.cluster_id, - OnOff.cluster_id, - ], - OUTPUT_CLUSTERS: [], - }, - 34: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.OCCUPANCY_SENSOR, - INPUT_CLUSTERS: [ - Basic.cluster_id, - Identify.cluster_id, - OccupancySensing.cluster_id, - ], - OUTPUT_CLUSTERS: [], - }, - 35: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.IAS_ZONE, - INPUT_CLUSTERS: [ - Basic.cluster_id, - PowerConfiguration.cluster_id, - Identify.cluster_id, - BinaryInput.cluster_id, - PollControl.cluster_id, - IasZone.cluster_id, - ], - OUTPUT_CLUSTERS: [ - Time.cluster_id, - Ota.cluster_id, - ], - }, - 38: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.TEMPERATURE_SENSOR, - INPUT_CLUSTERS: [ - Basic.cluster_id, - Identify.cluster_id, - TemperatureMeasurement.cluster_id, - ], - OUTPUT_CLUSTERS: [Identify.cluster_id], - }, - 39: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.LIGHT_SENSOR, - INPUT_CLUSTERS: [ - Basic.cluster_id, - Identify.cluster_id, - IlluminanceMeasurement.cluster_id, - ], - OUTPUT_CLUSTERS: [], - }, - 40: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.OCCUPANCY_SENSOR, - INPUT_CLUSTERS: [ - Basic.cluster_id, - Identify.cluster_id, - OccupancySensing.cluster_id, - ], - OUTPUT_CLUSTERS: [], - }, - 41: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.OCCUPANCY_SENSOR, - INPUT_CLUSTERS: [ - Basic.cluster_id, - Identify.cluster_id, - OccupancySensing.cluster_id, - ], - OUTPUT_CLUSTERS: [], - }, - }, - } - - replacement = { - ENDPOINTS: { - 1: { - PROFILE_ID: 0xC0C9, - DEVICE_TYPE: 1, - INPUT_CLUSTERS: [ - Identify.cluster_id, - Scenes.cluster_id, - OnOff.cluster_id, - ], - OUTPUT_CLUSTERS: [], - }, - 34: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.OCCUPANCY_SENSOR, - INPUT_CLUSTERS: [ - Basic.cluster_id, - Identify.cluster_id, - OccupancySensing.cluster_id, - ], - OUTPUT_CLUSTERS: [], - }, - 35: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.IAS_ZONE, - INPUT_CLUSTERS: [ - Basic.cluster_id, - DevelcoPowerConfiguration, - Identify.cluster_id, - BinaryInput.cluster_id, - PollControl.cluster_id, - DevelcoIasZone, - ], - OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], - }, - 38: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.TEMPERATURE_SENSOR, - INPUT_CLUSTERS: [ - Basic.cluster_id, - Identify.cluster_id, - TemperatureMeasurement.cluster_id, - ], - OUTPUT_CLUSTERS: [Identify.cluster_id], - }, - 39: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.LIGHT_SENSOR, - INPUT_CLUSTERS: [ - Basic.cluster_id, - Identify.cluster_id, - IlluminanceMeasurement.cluster_id, - ], - OUTPUT_CLUSTERS: [], - }, - 40: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.OCCUPANCY_SENSOR, - INPUT_CLUSTERS: [ - Basic.cluster_id, - Identify.cluster_id, - OccupancySensing.cluster_id, - ], - OUTPUT_CLUSTERS: [], - }, - 41: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.OCCUPANCY_SENSOR, - INPUT_CLUSTERS: [ - Basic.cluster_id, - Identify.cluster_id, - OccupancySensing.cluster_id, - ], - OUTPUT_CLUSTERS: [], - }, - } - } - - -class MOSZB140_Var02(CustomDevice): - """Custom device Develco Motion Sensor Pro (variation 02).""" - - manufacturer_id_override = MANUFACTURER - - signature = { - # - # - # - # - # - # - # - MODELS_INFO: [(DEVELCO, "MOSZB-140"), (FRIENT, "MOSZB-140")], - ENDPOINTS: { - 1: { - PROFILE_ID: 0xC0C9, - DEVICE_TYPE: 1, - INPUT_CLUSTERS: [ - Identify.cluster_id, - Scenes.cluster_id, - OnOff.cluster_id, - ], - OUTPUT_CLUSTERS: [], - }, - 34: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.OCCUPANCY_SENSOR, - INPUT_CLUSTERS: [ - Basic.cluster_id, - Identify.cluster_id, - OccupancySensing.cluster_id, - ], - OUTPUT_CLUSTERS: [], - }, - 35: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.IAS_ZONE, - INPUT_CLUSTERS: [ - Basic.cluster_id, - PowerConfiguration.cluster_id, - Identify.cluster_id, - BinaryInput.cluster_id, - PollControl.cluster_id, - IasZone.cluster_id, - ], - OUTPUT_CLUSTERS: [ - Identify.cluster_id, - Time.cluster_id, - Ota.cluster_id, - ], - }, - 38: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.TEMPERATURE_SENSOR, - INPUT_CLUSTERS: [ - Basic.cluster_id, - Identify.cluster_id, - TemperatureMeasurement.cluster_id, - ], - OUTPUT_CLUSTERS: [], - }, - 39: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.LIGHT_SENSOR, - INPUT_CLUSTERS: [ - Basic.cluster_id, - Identify.cluster_id, - IlluminanceMeasurement.cluster_id, - ], - OUTPUT_CLUSTERS: [], - }, - 40: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.OCCUPANCY_SENSOR, - INPUT_CLUSTERS: [ - Basic.cluster_id, - Identify.cluster_id, - OccupancySensing.cluster_id, - ], - OUTPUT_CLUSTERS: [], - }, - 41: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.OCCUPANCY_SENSOR, - INPUT_CLUSTERS: [ - Basic.cluster_id, - Identify.cluster_id, - OccupancySensing.cluster_id, - ], - OUTPUT_CLUSTERS: [], - }, - }, - } - - replacement = { - ENDPOINTS: { - 1: { - PROFILE_ID: 0xC0C9, - DEVICE_TYPE: 1, - INPUT_CLUSTERS: [ - Identify.cluster_id, - Scenes.cluster_id, - OnOff.cluster_id, - ], - OUTPUT_CLUSTERS: [], - }, - 34: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.OCCUPANCY_SENSOR, - INPUT_CLUSTERS: [ - Basic.cluster_id, - Identify.cluster_id, - OccupancySensing.cluster_id, - ], - OUTPUT_CLUSTERS: [], - }, - 35: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.IAS_ZONE, - INPUT_CLUSTERS: [ - Basic.cluster_id, - DevelcoPowerConfiguration, - Identify.cluster_id, - BinaryInput.cluster_id, - PollControl.cluster_id, - DevelcoIasZone, - ], - OUTPUT_CLUSTERS: [ - Identify.cluster_id, - Time.cluster_id, - Ota.cluster_id, - ], - }, - 38: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.TEMPERATURE_SENSOR, - INPUT_CLUSTERS: [ - Basic.cluster_id, - Identify.cluster_id, - TemperatureMeasurement.cluster_id, - ], - OUTPUT_CLUSTERS: [], - }, - 39: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.LIGHT_SENSOR, - INPUT_CLUSTERS: [ - Basic.cluster_id, - Identify.cluster_id, - IlluminanceMeasurement.cluster_id, - ], - OUTPUT_CLUSTERS: [], - }, - 40: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.OCCUPANCY_SENSOR, - INPUT_CLUSTERS: [ - Basic.cluster_id, - Identify.cluster_id, - OccupancySensing.cluster_id, - ], - OUTPUT_CLUSTERS: [], - }, - 41: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.OCCUPANCY_SENSOR, - INPUT_CLUSTERS: [ - Basic.cluster_id, - Identify.cluster_id, - OccupancySensing.cluster_id, - ], - OUTPUT_CLUSTERS: [], - }, - } - } +( + QuirkBuilder("frient A/S", "MOSZB-153") + .applies_to(DEVELCO, "MOSZB-140") + .applies_to(FRIENT, "MOSZB-140") + .replaces(DevelcoPowerConfiguration, endpoint_id=35) + .replaces(DevelcoIasZone, endpoint_id=35) + # This entity does not do anything + .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() +) diff --git a/zhaquirks/develco/open_close.py b/zhaquirks/develco/open_close.py index 22dbc9bd31..6f6f58a9c1 100644 --- a/zhaquirks/develco/open_close.py +++ b/zhaquirks/develco/open_close.py @@ -1,33 +1,14 @@ """Door/Windows sensors.""" -from zigpy.profiles import zha -from zigpy.quirks import CustomCluster, CustomDevice +from typing import Final + +from zigpy.quirks.v2 import QuirkBuilder import zigpy.types as t -from zigpy.zcl import foundation -from zigpy.zcl.clusters.general import ( - Basic, - BinaryInput, - Identify, - OnOff, - Ota, - PollControl, - PowerConfiguration, - Scenes, - Time, -) -from zigpy.zcl.clusters.measurement import TemperatureMeasurement +from zigpy.zcl.clusters.general import BinaryInput from zigpy.zcl.clusters.security import IasZone +from zigpy.zcl.foundation import Direction, ZCLCommandDef, ZoneStatus from zhaquirks import PowerConfigurationCluster -from zhaquirks.const import ( - DEVICE_TYPE, - ENDPOINTS, - INPUT_CLUSTERS, - MODELS_INFO, - OUTPUT_CLUSTERS, - PROFILE_ID, -) -from zhaquirks.develco import DEVELCO, FRIENT class DevelcoPowerConfiguration(PowerConfigurationCluster): @@ -37,163 +18,34 @@ class DevelcoPowerConfiguration(PowerConfigurationCluster): MAX_VOLTS = 3.0 -class DevelcoIASZone(CustomCluster, IasZone): - """IAS Zone.""" - - client_commands = IasZone.client_commands.copy() - client_commands[0x0000] = foundation.ZCLCommandDef( - "status_change_notification", - { - "zone_status": IasZone.ZoneStatus, - "extended_status": t.bitmap8, - # These two should not be optional - "zone_id?": t.uint8_t, - "delay?": t.uint16_t, - }, - False, - is_manufacturer_specific=True, - ) - - -class WISZB120(CustomDevice): - """Custom device representing door/windows sensors, with built-in temperature measuring.""" - - signature = { - # - # - # - MODELS_INFO: [(DEVELCO, "WISZB-120"), (FRIENT, "WISZB-120")], - ENDPOINTS: { - 1: { - PROFILE_ID: 0xC0C9, - DEVICE_TYPE: 1, - INPUT_CLUSTERS: [ - Identify.cluster_id, - Scenes.cluster_id, - OnOff.cluster_id, - ], - OUTPUT_CLUSTERS: [], - }, - 35: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.IAS_ZONE, - INPUT_CLUSTERS: [ - Basic.cluster_id, - PowerConfiguration.cluster_id, - Identify.cluster_id, - BinaryInput.cluster_id, - PollControl.cluster_id, - IasZone.cluster_id, - ], - OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], - }, - 38: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.TEMPERATURE_SENSOR, - INPUT_CLUSTERS: [ - Basic.cluster_id, - Identify.cluster_id, - TemperatureMeasurement.cluster_id, - ], - OUTPUT_CLUSTERS: [Identify.cluster_id], - }, - }, - } - - replacement = { - ENDPOINTS: { - 1: { - INPUT_CLUSTERS: [ - Identify.cluster_id, - Scenes.cluster_id, - OnOff.cluster_id, - ], - OUTPUT_CLUSTERS: [], - }, - 35: { - INPUT_CLUSTERS: [ - Basic.cluster_id, - DevelcoPowerConfiguration, - Identify.cluster_id, - BinaryInput.cluster_id, - PollControl.cluster_id, - DevelcoIASZone, - ], - OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], - }, - 38: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.TEMPERATURE_SENSOR, - INPUT_CLUSTERS: [ - Basic.cluster_id, - Identify.cluster_id, - TemperatureMeasurement.cluster_id, - ], - OUTPUT_CLUSTERS: [Identify.cluster_id], - }, - } - } - - -class WISZB121(CustomDevice): - """Custom device representing door/windows sensors, without built-in temperature measuring.""" - - signature = { - # - # - MODELS_INFO: [(DEVELCO, "WISZB-121"), (FRIENT, "WISZB-121")], - ENDPOINTS: { - 1: { - PROFILE_ID: 0xC0C9, - DEVICE_TYPE: 1, - INPUT_CLUSTERS: [ - Identify.cluster_id, - Scenes.cluster_id, - OnOff.cluster_id, - ], - OUTPUT_CLUSTERS: [], - }, - 35: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.IAS_ZONE, - INPUT_CLUSTERS: [ - Basic.cluster_id, - PowerConfiguration.cluster_id, - Identify.cluster_id, - BinaryInput.cluster_id, - PollControl.cluster_id, - IasZone.cluster_id, - ], - OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], - }, - }, - } - - replacement = { - ENDPOINTS: { - 1: { - INPUT_CLUSTERS: [ - Identify.cluster_id, - Scenes.cluster_id, - OnOff.cluster_id, - ], - OUTPUT_CLUSTERS: [], - }, - 35: { - INPUT_CLUSTERS: [ - Basic.cluster_id, - DevelcoPowerConfiguration, - Identify.cluster_id, - BinaryInput.cluster_id, - PollControl.cluster_id, - DevelcoIASZone, - ], - OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], - }, - } - } +class DevelcoIASZone(IasZone): + """IAS Zone, patched to fix a bug with the status change notification command.""" + + class ClientCommandDefs(IasZone.ClientCommandDefs): + """IAS Zone command definitions.""" + + status_change_notification: Final = ZCLCommandDef( + id=0x00, + schema={ + "zone_status": ZoneStatus, + "extended_status": t.bitmap8, + # These two should not be optional + "zone_id?": t.uint8_t, + "delay?": t.uint16_t, + }, + direction=Direction.Client_to_Server, + ) + + +( + QuirkBuilder("frient A/S", "WISZB-131") + .applies_to("Develco Products A/S", "WISZB-120") + .applies_to("frient A/S", "WISZB-120") + .applies_to("Develco Products A/S", "WISZB-121") + .applies_to("frient A/S", "WISZB-121") + .replaces(DevelcoIASZone, endpoint_id=35) + .replaces(DevelcoPowerConfiguration, endpoint_id=35) + # The binary input cluster is a duplicate + .prevent_default_entity_creation(endpoint_id=35, cluster_id=BinaryInput.cluster_id) + .add_to_registry() +) diff --git a/zhaquirks/develco/smart_button.py b/zhaquirks/develco/smart_button.py new file mode 100644 index 0000000000..6a9389f812 --- /dev/null +++ b/zhaquirks/develco/smart_button.py @@ -0,0 +1,18 @@ +"""Smart button.""" + +from zigpy.quirks.v2 import QuirkBuilder +from zigpy.zcl import ClusterType +from zigpy.zcl.clusters.general import OnOff + +( + QuirkBuilder("frient A/S", "SBTZB-110") + # The button emits `toggle()` commands in addition to attribute updates. + # The `toggle()` command is unreliable, since the entity state will never match the + # real state of the button if a command is lost. + .prevent_default_entity_creation( + endpoint_id=32, + cluster_id=OnOff.cluster_id, + cluster_type=ClusterType.Client, + ) + .add_to_registry() +) diff --git a/zhaquirks/develco/smoke_alarm.py b/zhaquirks/develco/smoke_alarm.py index 6caff9fc70..30123063b0 100644 --- a/zhaquirks/develco/smoke_alarm.py +++ b/zhaquirks/develco/smoke_alarm.py @@ -1,121 +1,13 @@ -"""Develco Heat Alarm.""" +"""Develco Smoke Alarm.""" -import zigpy.profiles.zha -from zigpy.quirks import CustomDevice -from zigpy.zcl.clusters.general import ( - Basic, - BinaryInput, - Identify, - OnOff, - Ota, - PollControl, - PowerConfiguration, - Scenes, - Time, -) -from zigpy.zcl.clusters.measurement import TemperatureMeasurement -from zigpy.zcl.clusters.security import IasWd, IasZone - -from zhaquirks.const import ( - DEVICE_TYPE, - ENDPOINTS, - INPUT_CLUSTERS, - MODELS_INFO, - OUTPUT_CLUSTERS, - PROFILE_ID, -) - -from . import DEVELCO, FRIENT, DevelcoIasZone, DevelcoPowerConfiguration - -MANUFACTURER = 0x1015 +from zigpy.quirks.v2 import QuirkBuilder +from . import DevelcoIasZone, DevelcoPowerConfiguration -class SMSZB120(CustomDevice): - """Custom device heat alarm.""" - - manufacturer_id_override = MANUFACTURER - - signature = { - # - # - # - MODELS_INFO: [(DEVELCO, "SMSZB-120"), (FRIENT, "SMSZB-120")], - ENDPOINTS: { - 1: { - PROFILE_ID: 49353, - DEVICE_TYPE: 1, - INPUT_CLUSTERS: [ - Identify.cluster_id, - Scenes.cluster_id, - OnOff.cluster_id, - ], - OUTPUT_CLUSTERS: [], - }, - 35: { - PROFILE_ID: zigpy.profiles.zha.PROFILE_ID, - DEVICE_TYPE: zigpy.profiles.zha.DeviceType.IAS_ZONE, - INPUT_CLUSTERS: [ - Basic.cluster_id, - PowerConfiguration.cluster_id, - Identify.cluster_id, - BinaryInput.cluster_id, - PollControl.cluster_id, - IasZone.cluster_id, - IasWd.cluster_id, - ], - OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], - }, - 38: { - PROFILE_ID: zigpy.profiles.zha.PROFILE_ID, - DEVICE_TYPE: zigpy.profiles.zha.DeviceType.TEMPERATURE_SENSOR, - INPUT_CLUSTERS: [ - Basic.cluster_id, - Identify.cluster_id, - TemperatureMeasurement.cluster_id, - ], - OUTPUT_CLUSTERS: [Identify.cluster_id], - }, - }, - } - - replacement = { - ENDPOINTS: { - 1: { - PROFILE_ID: 49353, - DEVICE_TYPE: 1, - INPUT_CLUSTERS: [ - Identify.cluster_id, - Scenes.cluster_id, - OnOff.cluster_id, - ], - OUTPUT_CLUSTERS: [], - }, - 35: { - PROFILE_ID: zigpy.profiles.zha.PROFILE_ID, - DEVICE_TYPE: zigpy.profiles.zha.DeviceType.IAS_ZONE, - INPUT_CLUSTERS: [ - Basic.cluster_id, - DevelcoPowerConfiguration, - Identify.cluster_id, - BinaryInput.cluster_id, - PollControl.cluster_id, - DevelcoIasZone, - IasWd.cluster_id, - ], - OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], - }, - 38: { - PROFILE_ID: zigpy.profiles.zha.PROFILE_ID, - DEVICE_TYPE: zigpy.profiles.zha.DeviceType.TEMPERATURE_SENSOR, - INPUT_CLUSTERS: [ - Basic.cluster_id, - Identify.cluster_id, - TemperatureMeasurement.cluster_id, - ], - OUTPUT_CLUSTERS: [Identify.cluster_id], - }, - }, - } +( + QuirkBuilder("frient A/S", "SMSZB-120") + .applies_to("Develco Products A/S", "SMSZB-120") + .replaces(DevelcoIasZone, endpoint_id=35) + .replaces(DevelcoPowerConfiguration, endpoint_id=35) + .add_to_registry() +) From e902f307dfa71ef9afd9b96724e329372fe011c1 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 10 Apr 2025 13:50:05 -0400 Subject: [PATCH 02/12] Clean up duplicate clusters --- zhaquirks/develco/__init__.py | 36 ++++++++++++++++----------------- zhaquirks/develco/open_close.py | 28 +++---------------------- 2 files changed, 20 insertions(+), 44 deletions(-) diff --git a/zhaquirks/develco/__init__.py b/zhaquirks/develco/__init__.py index cbf7cce5cb..5e6e658ecd 100644 --- a/zhaquirks/develco/__init__.py +++ b/zhaquirks/develco/__init__.py @@ -1,9 +1,10 @@ """Quirks for Develco Products A/S.""" -from zigpy import types as t -from zigpy.quirks import CustomCluster +from typing import Final + +import zigpy.types as t from zigpy.zcl import foundation -from zigpy.zcl.clusters.security import IasZone +from zigpy.zcl.clusters.security import IasZone, ZoneStatus from zhaquirks import PowerConfigurationCluster @@ -18,23 +19,20 @@ class DevelcoPowerConfiguration(PowerConfigurationCluster): MAX_VOLTS = 3.0 # old 3.2 -class DevelcoIasZone(CustomCluster, IasZone): - """Custom IasZone for Develco.""" +class DevelcoIasZone(IasZone): + """IAS Zone, patched to fix a bug with the status change notification command.""" + + class ClientCommandDefs(IasZone.ClientCommandDefs): + """IAS Zone command definitions.""" - client_commands = { - 0x00: foundation.ZCLCommandDef( - "status_change_notification", - { - "zone_status": IasZone.ZoneStatus, - "extended_status?": t.bitmap8, + status_change_notification: Final = foundation.ZCLCommandDef( + id=0x00, + schema={ + "zone_status": ZoneStatus, + "extended_status": t.bitmap8, + # These two should not be optional "zone_id?": t.uint8_t, "delay?": t.uint16_t, }, - False, - ), - 0x01: foundation.ZCLCommandDef( - "enroll", - {"zone_type": IasZone.ZoneType, "manufacturer_code": t.uint16_t}, - False, - ), - } + direction=foundation.Direction.Client_to_Server, + ) diff --git a/zhaquirks/develco/open_close.py b/zhaquirks/develco/open_close.py index 6f6f58a9c1..cc0f6dda9d 100644 --- a/zhaquirks/develco/open_close.py +++ b/zhaquirks/develco/open_close.py @@ -1,15 +1,12 @@ """Door/Windows sensors.""" -from typing import Final - from zigpy.quirks.v2 import QuirkBuilder -import zigpy.types as t from zigpy.zcl.clusters.general import BinaryInput -from zigpy.zcl.clusters.security import IasZone -from zigpy.zcl.foundation import Direction, ZCLCommandDef, ZoneStatus from zhaquirks import PowerConfigurationCluster +from . import DevelcoIasZone + class DevelcoPowerConfiguration(PowerConfigurationCluster): """Power configuration cluster.""" @@ -18,32 +15,13 @@ class DevelcoPowerConfiguration(PowerConfigurationCluster): MAX_VOLTS = 3.0 -class DevelcoIASZone(IasZone): - """IAS Zone, patched to fix a bug with the status change notification command.""" - - class ClientCommandDefs(IasZone.ClientCommandDefs): - """IAS Zone command definitions.""" - - status_change_notification: Final = ZCLCommandDef( - id=0x00, - schema={ - "zone_status": ZoneStatus, - "extended_status": t.bitmap8, - # These two should not be optional - "zone_id?": t.uint8_t, - "delay?": t.uint16_t, - }, - direction=Direction.Client_to_Server, - ) - - ( QuirkBuilder("frient A/S", "WISZB-131") .applies_to("Develco Products A/S", "WISZB-120") .applies_to("frient A/S", "WISZB-120") .applies_to("Develco Products A/S", "WISZB-121") .applies_to("frient A/S", "WISZB-121") - .replaces(DevelcoIASZone, endpoint_id=35) + .replaces(DevelcoIasZone, endpoint_id=35) .replaces(DevelcoPowerConfiguration, endpoint_id=35) # The binary input cluster is a duplicate .prevent_default_entity_creation(endpoint_id=35, cluster_id=BinaryInput.cluster_id) From 3580596fc050ee70c0b8926c5754fc6faa2f2295 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 10 Apr 2025 14:47:09 -0400 Subject: [PATCH 03/12] Fix up --- zhaquirks/develco/__init__.py | 3 ++- zhaquirks/develco/intelligent_keypad.py | 13 +++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/zhaquirks/develco/__init__.py b/zhaquirks/develco/__init__.py index 5e6e658ecd..0889a41f72 100644 --- a/zhaquirks/develco/__init__.py +++ b/zhaquirks/develco/__init__.py @@ -2,6 +2,7 @@ from typing import Final +from zigpy.quirks import CustomCluster import zigpy.types as t from zigpy.zcl import foundation from zigpy.zcl.clusters.security import IasZone, ZoneStatus @@ -19,7 +20,7 @@ class DevelcoPowerConfiguration(PowerConfigurationCluster): MAX_VOLTS = 3.0 # old 3.2 -class DevelcoIasZone(IasZone): +class DevelcoIasZone(CustomCluster, IasZone): """IAS Zone, patched to fix a bug with the status change notification command.""" class ClientCommandDefs(IasZone.ClientCommandDefs): diff --git a/zhaquirks/develco/intelligent_keypad.py b/zhaquirks/develco/intelligent_keypad.py index 6b9709c6be..92edea143f 100644 --- a/zhaquirks/develco/intelligent_keypad.py +++ b/zhaquirks/develco/intelligent_keypad.py @@ -7,10 +7,15 @@ ( QuirkBuilder("frient A/S", "KEPZB-110") - .prevent_default_entity_creation(endpoint_id=35, cluster_id=BinaryInput.cluster_id) - .prevent_default_entity_creation(endpoint_id=35, cluster_id=IasZone.cluster_id) + .prevent_default_entity_creation(endpoint_id=44, cluster_id=BinaryInput.cluster_id) + # Hide the default `ias_zone` entity .prevent_default_entity_creation( - endpoint_id=35, + endpoint_id=44, + cluster_id=IasZone.cluster_id, + function=lambda entity: entity.translation_key == "ias_zone", + ) + .prevent_default_entity_creation( + endpoint_id=44, cluster_id=IasWd.cluster_id, function=lambda entity: entity.translation_key in ( @@ -21,7 +26,7 @@ ), ) .binary_sensor( - endpoint_id=35, + endpoint_id=44, cluster_id=IasZone.cluster_id, attribute_name=IasZone.AttributeDefs.zone_status.name, device_class=BinarySensorDeviceClass.TAMPER, From 9da60a440b894d82cb1e1f7af84be49e1f3a1061 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 21 Apr 2025 16:43:14 -0400 Subject: [PATCH 04/12] More devices --- zhaquirks/develco/air_quality.py | 279 +++++++------------------------ zhaquirks/develco/humidity.py | 11 ++ zhaquirks/develco/power_plug.py | 130 +++----------- 3 files changed, 97 insertions(+), 323 deletions(-) create mode 100644 zhaquirks/develco/humidity.py diff --git a/zhaquirks/develco/air_quality.py b/zhaquirks/develco/air_quality.py index c8819ec292..03358a4340 100644 --- a/zhaquirks/develco/air_quality.py +++ b/zhaquirks/develco/air_quality.py @@ -1,241 +1,80 @@ """Develco Air Quality Sensor.""" import logging +from typing import Final -from zigpy.profiles import zha -from zigpy.quirks import CustomCluster, CustomDevice +from zigpy.quirks import CustomCluster +from zigpy.quirks.v2 import QuirkBuilder, SensorDeviceClass, SensorStateClass +from zigpy.quirks.v2.homeassistant import CONCENTRATION_PARTS_PER_BILLION import zigpy.types as t -from zigpy.zcl.clusters.general import ( - Basic, - Identify, - OnOff, - Ota, - PollControl, - PowerConfiguration, - Scenes, - Time, +from zigpy.zcl.foundation import ( + ZCL_CLUSTER_REVISION_ATTR, + ZCL_REPORTING_STATUS_ATTR, + BaseAttributeDefs, + ZCLAttributeDef, ) -from zigpy.zcl.clusters.measurement import RelativeHumidity, TemperatureMeasurement -from zhaquirks import Bus, LocalDataCluster -from zhaquirks.const import ( - DEVICE_TYPE, - ENDPOINTS, - INPUT_CLUSTERS, - MODELS_INFO, - OUTPUT_CLUSTERS, - PROFILE_ID, -) -from zhaquirks.develco import DEVELCO, DevelcoPowerConfiguration - -MANUFACTURER = 0x1015 -VOC_MEASURED_VALUE = 0x0000 -VOC_MIN_MEASURED_VALUE = 0x0001 -VOC_MAX_MEASURED_VALUE = 0x0002 -VOC_RESOLUTION = 0x0003 - -VOC_REPORTED = "voc_reported" -MIN_VOC_REPORTED = "min_voc_reported" -MAX_VOC_REPORTED = "max_voc_reported" -VOC_RESOLUTION_REPORTED = "voc_resolution_reported" +from zhaquirks.develco import DevelcoPowerConfiguration _LOGGER = logging.getLogger(__name__) class DevelcoVOCMeasurement(CustomCluster): - """Input Cluster to route manufacturer specific VOC cluster to actual VOC cluster.""" + """Develco VOC cluster definition.""" cluster_id = 0xFC03 name = "VOC Level" - ep_attribute = "voc_level" - attributes = { - VOC_MEASURED_VALUE: ("measured_value", t.uint16_t, True), - VOC_MIN_MEASURED_VALUE: ("min_measured_value", t.uint16_t, True), - VOC_MAX_MEASURED_VALUE: ("max_measured_value", t.uint16_t, True), - VOC_RESOLUTION: ("resolution", t.uint16_t, True), - } - server_commands = {} - client_commands = {} + ep_attribute = "develco_voc_level" - def __init__(self, *args, **kwargs): - """Init.""" - self._current_state = {} - super().__init__(*args, **kwargs) - self.endpoint.device.app_cluster = self + class AttributeDefs(BaseAttributeDefs): + """Attribute definitions, same as all the other `Measurement` clusters.""" - def _update_attribute(self, attrid, value): - super()._update_attribute(attrid, value) - if attrid == VOC_MEASURED_VALUE and value is not None: - self.endpoint.device.voc_bus.listener_event(VOC_REPORTED, value) - if attrid == VOC_MIN_MEASURED_VALUE and value is not None: - self.endpoint.device.voc_bus.listener_event(MIN_VOC_REPORTED, value) - if attrid == VOC_MAX_MEASURED_VALUE and value is not None: - self.endpoint.device.voc_bus.listener_event(MAX_VOC_REPORTED, value) - if attrid == VOC_RESOLUTION and value is not None: - self.endpoint.device.voc_bus.listener_event(VOC_RESOLUTION_REPORTED, value) - _LOGGER.debug( - "%s Develco VOC : [%s]", - self.endpoint.device.ieee, - self._attr_cache, + measured_value: Final = ZCLAttributeDef( + id=0x0000, + type=t.uint16_t, # In parts per billion + access="rp", + mandatory=True, + is_manufacturer_specific=True, ) - - -class DevelcoRelativeHumidity(CustomCluster, RelativeHumidity): - """Handles invalid values for Humidity.""" - - def _update_attribute(self, attrid, value): - # Drop values out of specified range (0-100% RH) - if 0 <= value <= 10000: - super()._update_attribute(attrid, value) - _LOGGER.debug( - "%s Develco Humidity : [%s]", - self.endpoint.device.ieee, - self._attr_cache, + min_measured_value: Final = ZCLAttributeDef( + id=0x0001, + type=t.uint16_t, + access="r", + mandatory=True, + is_manufacturer_specific=True, ) - - -class DevelcoTemperatureMeasurement(CustomCluster, TemperatureMeasurement): - """Handles invalid values for Temperature.""" - - def _update_attribute(self, attrid, value): - # Drop values out of specified range (0-50°C) - if 0 <= value <= 5000: - super()._update_attribute(attrid, value) - _LOGGER.debug( - "%s Develco Temperature : [%s]", - self.endpoint.device.ieee, - self._attr_cache, + max_measured_value: Final = ZCLAttributeDef( + id=0x0002, + type=t.uint16_t, + access="r", + mandatory=True, + is_manufacturer_specific=True, + ) + tolerance: Final = ZCLAttributeDef( + id=0x0003, + type=t.uint16_t, + access="r", + is_manufacturer_specific=True, ) - -class EmulatedVOCMeasurement(LocalDataCluster): - """VOC measurement cluster to receive reports from the Develco VOC cluster.""" - - cluster_id = 0x042E - name = "VOC Level" - ep_attribute = "voc_level" - attributes = { - VOC_MEASURED_VALUE: ("measured_value", t.uint16_t, True), - VOC_MIN_MEASURED_VALUE: ("min_measured_value", t.uint16_t, True), - VOC_MAX_MEASURED_VALUE: ("max_measured_value", t.uint16_t, True), - VOC_RESOLUTION: ("resolution", t.uint16_t, True), - } - MEASURED_VALUE_ID = 0x0000 - MIN_MEASURED_VALUE_ID = 0x0001 - MAX_MEASURED_VALUE_ID = 0x0002 - RESOLUTION_ID = 0x0003 - - def __init__(self, *args, **kwargs): - """Init.""" - super().__init__(*args, **kwargs) - self.endpoint.device.voc_bus.add_listener(self) - - async def bind(self): - """Bind cluster.""" - result = await self.endpoint.device.app_cluster.bind() - return result - - async def write_attributes(self, attributes, manufacturer=None): - """Ignore write_attributes.""" - return (0,) - - def _update_attribute(self, attrid, value): - # Drop values out of specified range (0-60000 ppb) - if 0 <= value <= 60000: - # Convert ppb into mg/m³ approximation according to develco spec - value = value * 0.0000045 - super()._update_attribute(attrid, value) - - def voc_reported(self, value): - """VOC reported.""" - self._update_attribute(self.MEASURED_VALUE_ID, value) - - def min_voc_reported(self, value): - """Minimum Measured VOC reported.""" - self._update_attribute(self.MIN_MEASURED_VALUE_ID, value) - - def max_voc_reported(self, value): - """Maximum Measured VOC reported.""" - self._update_attribute(self.MAX_MEASURED_VALUE_ID, value) - - def voc_resolution_reported(self, value): - """VOC Resolution reported.""" - self._update_attribute(self.RESOLUTION_ID, value) - - -class AQSZB110(CustomDevice): - """Custom device Develco air quality sensor.""" - - manufacturer_id_override = MANUFACTURER - - def __init__(self, *args, **kwargs): - """Init.""" - self.voc_bus = Bus() - super().__init__(*args, **kwargs) - - signature = { - # - # - MODELS_INFO: [ - (DEVELCO, "AQSZB-110"), - ("frient A/S", "AQSZB-110"), - ], - ENDPOINTS: { - 1: { - PROFILE_ID: 0xC0C9, - DEVICE_TYPE: 1, - INPUT_CLUSTERS: [ - Identify.cluster_id, - Scenes.cluster_id, - OnOff.cluster_id, - ], - OUTPUT_CLUSTERS: [], - }, - 38: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.TEMPERATURE_SENSOR, - INPUT_CLUSTERS: [ - Basic.cluster_id, - PowerConfiguration.cluster_id, - Identify.cluster_id, - PollControl.cluster_id, - TemperatureMeasurement.cluster_id, - RelativeHumidity.cluster_id, - 0xFC03, - ], - OUTPUT_CLUSTERS: [Identify.cluster_id, Time.cluster_id, Ota.cluster_id], - }, - }, - } - - replacement = { - ENDPOINTS: { - 1: { - PROFILE_ID: 0xC0C9, - DEVICE_TYPE: 1, - INPUT_CLUSTERS: [ - Identify.cluster_id, - Scenes.cluster_id, - OnOff.cluster_id, - ], - OUTPUT_CLUSTERS: [], - }, - 38: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.TEMPERATURE_SENSOR, - INPUT_CLUSTERS: [ - Basic.cluster_id, - DevelcoPowerConfiguration, - Identify.cluster_id, - PollControl.cluster_id, - DevelcoTemperatureMeasurement, - DevelcoRelativeHumidity, - DevelcoVOCMeasurement, - EmulatedVOCMeasurement, - ], - OUTPUT_CLUSTERS: [Identify.cluster_id, Time.cluster_id, Ota.cluster_id], - }, - }, - } + cluster_revision: Final = ZCL_CLUSTER_REVISION_ATTR + reporting_status: Final = ZCL_REPORTING_STATUS_ATTR + + +( + QuirkBuilder("frient A/S", "AQSZB-110") + .applies_to("Develco Products A/S", "AQSZB-110") + .replaces(DevelcoVOCMeasurement, endpoint_id=38) + .replaces(DevelcoPowerConfiguration, endpoint_id=38) + .sensor( + attribute_name=DevelcoVOCMeasurement.AttributeDefs.measured_value.name, + cluster_id=DevelcoVOCMeasurement.cluster_id, + endpoint_id=38, + device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, + state_class=SensorStateClass.MEASUREMENT, + unit=CONCENTRATION_PARTS_PER_BILLION, + fallback_name="VOC Level", + unique_id_suffix="voc_level", + ) + .add_to_registry() +) diff --git a/zhaquirks/develco/humidity.py b/zhaquirks/develco/humidity.py new file mode 100644 index 0000000000..8029f56796 --- /dev/null +++ b/zhaquirks/develco/humidity.py @@ -0,0 +1,11 @@ +"""Develco Smart Humidity Sensor.""" + +from zigpy.quirks.v2 import QuirkBuilder + +from zhaquirks.develco import DevelcoPowerConfiguration + +( + QuirkBuilder("frient A/S", "HMSZB-120") + .replaces(DevelcoPowerConfiguration, endpoint_id=38) + .add_to_registry() +) diff --git a/zhaquirks/develco/power_plug.py b/zhaquirks/develco/power_plug.py index d89521fb48..3f08205d35 100644 --- a/zhaquirks/develco/power_plug.py +++ b/zhaquirks/develco/power_plug.py @@ -1,107 +1,31 @@ """Develco smart plugs.""" -from zigpy.profiles import zha -from zigpy.quirks import CustomCluster, CustomDevice -from zigpy.zcl.clusters.general import ( - Alarms, - Basic, - DeviceTemperature, - Groups, - Identify, - OnOff, - Ota, - Scenes, - Time, +from zigpy.quirks.v2 import ( + EntityType, + QuirkBuilder, + SensorDeviceClass, + SensorStateClass, + UnitOfTemperature, ) -from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement -from zigpy.zcl.clusters.measurement import OccupancySensing -from zigpy.zcl.clusters.smartenergy import Metering - -from zhaquirks.const import ( - DEVICE_TYPE, - ENDPOINTS, - INPUT_CLUSTERS, - MODELS_INFO, - OUTPUT_CLUSTERS, - PROFILE_ID, +from zigpy.zcl.clusters.general import DeviceTemperature + +( + QuirkBuilder("frient A/S", "SPLZB-141") + .applies_to("Develco Products A/S", "SPLZB-131") + .prevent_default_entity_creation( + endpoint_id=2, cluster_id=DeviceTemperature.cluster_id + ) + .sensor( + attribute_name=DeviceTemperature.AttributeDefs.current_temperature.name, + cluster_id=DeviceTemperature.cluster_id, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + unit=UnitOfTemperature.CELSIUS, + divisor=1, # This should be 100 but the device does not follow the spec + translation_key="device_temperature", + fallback_name="Device temperature", + entity_type=EntityType.DIAGNOSTIC, + unique_id_suffix="2-2", # Replace the ZHA entity + ) + .add_to_registry() ) -from zhaquirks.develco import DEVELCO - -DEV_TEMP_ID = DeviceTemperature.attributes_by_name["current_temperature"].id - - -class DevelcoDeviceTemperature(CustomCluster, DeviceTemperature): - """Custom device temperature cluster to multiply the temperature by 100.""" - - def _update_attribute(self, attrid, value): - if attrid == DEV_TEMP_ID: - value = value * 100 - super()._update_attribute(attrid, value) - - -class SPLZB131(CustomDevice): - """Custom device Develco smart plug device.""" - - signature = { - MODELS_INFO: [(DEVELCO, "SPLZB-131")], - ENDPOINTS: { - 1: { - PROFILE_ID: 0xC0C9, - DEVICE_TYPE: 1, - INPUT_CLUSTERS: [ - Scenes.cluster_id, - OnOff.cluster_id, - ], - OUTPUT_CLUSTERS: [], - }, - 2: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.SMART_PLUG, - INPUT_CLUSTERS: [ - Basic.cluster_id, - DeviceTemperature.cluster_id, - Identify.cluster_id, - Groups.cluster_id, - Scenes.cluster_id, - OnOff.cluster_id, - Alarms.cluster_id, - Metering.cluster_id, - ElectricalMeasurement.cluster_id, - ], - OUTPUT_CLUSTERS: [ - Basic.cluster_id, - Identify.cluster_id, - Time.cluster_id, - Ota.cluster_id, - OccupancySensing.cluster_id, - ], - }, - }, - } - - replacement = { - ENDPOINTS: { - 2: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.SMART_PLUG, - INPUT_CLUSTERS: [ - Basic.cluster_id, - DevelcoDeviceTemperature, - Identify.cluster_id, - Groups.cluster_id, - Scenes.cluster_id, - OnOff.cluster_id, - Alarms.cluster_id, - Metering.cluster_id, - ElectricalMeasurement.cluster_id, - ], - OUTPUT_CLUSTERS: [ - Basic.cluster_id, - Identify.cluster_id, - Time.cluster_id, - Ota.cluster_id, - OccupancySensing.cluster_id, - ], - }, - } - } From 64ab13ee20d68ee24c03bdc61ae9ef8524933468 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 21 Apr 2025 16:47:52 -0400 Subject: [PATCH 05/12] Add power plug --- zhaquirks/develco/power_plug.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/zhaquirks/develco/power_plug.py b/zhaquirks/develco/power_plug.py index 3f08205d35..60a12b16da 100644 --- a/zhaquirks/develco/power_plug.py +++ b/zhaquirks/develco/power_plug.py @@ -5,19 +5,22 @@ QuirkBuilder, SensorDeviceClass, SensorStateClass, - UnitOfTemperature, ) +from zigpy.quirks.v2.homeassistant import UnitOfTemperature from zigpy.zcl.clusters.general import DeviceTemperature ( QuirkBuilder("frient A/S", "SPLZB-141") .applies_to("Develco Products A/S", "SPLZB-131") .prevent_default_entity_creation( - endpoint_id=2, cluster_id=DeviceTemperature.cluster_id + endpoint_id=2, + cluster_id=DeviceTemperature.cluster_id, + function=lambda entity: entity.__class__.__name__ == "DeviceTemperature", ) .sensor( - attribute_name=DeviceTemperature.AttributeDefs.current_temperature.name, + endpoint_id=2, cluster_id=DeviceTemperature.cluster_id, + attribute_name=DeviceTemperature.AttributeDefs.current_temperature.name, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, unit=UnitOfTemperature.CELSIUS, @@ -25,7 +28,7 @@ translation_key="device_temperature", fallback_name="Device temperature", entity_type=EntityType.DIAGNOSTIC, - unique_id_suffix="2-2", # Replace the ZHA entity + unique_id_suffix="2", # Replace the ZHA entity ) .add_to_registry() ) From 16ec20f13f8f37b08d4db198ec3c667d8dba4b89 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 21 Apr 2025 17:49:55 -0400 Subject: [PATCH 06/12] Use automation triggers for button --- zhaquirks/const.py | 1 + zhaquirks/develco/smart_button.py | 21 +++++++++++++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/zhaquirks/const.py b/zhaquirks/const.py index 5d0d81480f..df1727baaf 100644 --- a/zhaquirks/const.py +++ b/zhaquirks/const.py @@ -18,6 +18,7 @@ ATTR_ID = "attr_id" ATTRIBUTE_ID = "attribute_id" ATTRIBUTE_NAME = "attribute_name" +ATTRIBUTE_VALUE = "attribute_value" BUTTON = "button" BUTTON_1 = "button_1" BUTTON_2 = "button_2" diff --git a/zhaquirks/develco/smart_button.py b/zhaquirks/develco/smart_button.py index 6a9389f812..a4c95f4aeb 100644 --- a/zhaquirks/develco/smart_button.py +++ b/zhaquirks/develco/smart_button.py @@ -2,17 +2,30 @@ from zigpy.quirks.v2 import QuirkBuilder from zigpy.zcl import ClusterType -from zigpy.zcl.clusters.general import OnOff +from zigpy.zcl.clusters.general import BinaryInput, OnOff + +from zhaquirks.const import BUTTON, CLUSTER_ID, COMMAND, COMMAND_CLICK, ENDPOINT_ID ( QuirkBuilder("frient A/S", "SBTZB-110") - # The button emits `toggle()` commands in addition to attribute updates. - # The `toggle()` command is unreliable, since the entity state will never match the - # real state of the button if a command is lost. .prevent_default_entity_creation( endpoint_id=32, cluster_id=OnOff.cluster_id, cluster_type=ClusterType.Client, ) + # Don't create a binary entity for a button, instead create automation triggers + .prevent_default_entity_creation( + endpoint_id=32, + cluster_id=BinaryInput.cluster_id, + ) + .device_automation_triggers( + { + (COMMAND_CLICK, BUTTON): { + ENDPOINT_ID: 32, + CLUSTER_ID: int(OnOff.cluster_id), + COMMAND: OnOff.ServerCommandDefs.toggle.name, + }, + } + ) .add_to_registry() ) From 3286c863d9816b9c0e9a8fabf63a7833ae31a957 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 21 Apr 2025 18:23:08 -0400 Subject: [PATCH 07/12] Add smart siren --- zhaquirks/develco/smart_siren.py | 46 ++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 zhaquirks/develco/smart_siren.py diff --git a/zhaquirks/develco/smart_siren.py b/zhaquirks/develco/smart_siren.py new file mode 100644 index 0000000000..9b904496b6 --- /dev/null +++ b/zhaquirks/develco/smart_siren.py @@ -0,0 +1,46 @@ +"""Smart siren.""" + +from zigpy.quirks.v2 import ( + EntityType, + QuirkBuilder, + SensorDeviceClass, + SensorStateClass, +) +from zigpy.quirks.v2.homeassistant import PERCENTAGE +from zigpy.quirks.v2.homeassistant.binary_sensor import BinarySensorDeviceClass +from zigpy.zcl.clusters.general import PowerConfiguration +from zigpy.zcl.clusters.security import IasZone + +( + QuirkBuilder("frient A/S", "SIRZB-111") + # Hide the default `ias_zone` entity + .prevent_default_entity_creation( + endpoint_id=43, + cluster_id=IasZone.cluster_id, + function=lambda entity: entity.translation_key == "ias_zone", + ) + # And instead create a tamper sensor + .binary_sensor( + endpoint_id=43, + cluster_id=IasZone.cluster_id, + attribute_name=IasZone.AttributeDefs.zone_status.name, + device_class=BinarySensorDeviceClass.TAMPER, + attribute_converter=lambda value: bool(value & IasZone.ZoneStatus.Tamper), + unique_id_suffix="tamper", + translation_key="tamper", + fallback_name="Tamper", + ) + .sensor( + attribute_name=PowerConfiguration.AttributeDefs.battery_percentage_remaining.name, + cluster_id=PowerConfiguration.cluster_id, + endpoint_id=43, + device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, + unit=PERCENTAGE, + divisor=2, # ZCL reports battery in units of 0.5%, so 200 => 100% + fallback_name="Battery", + unique_id_suffix="battery", + entity_type=EntityType.DIAGNOSTIC, + ) + .add_to_registry() +) From 4050904a32efecd9500fb4972c59c7d798e94796 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 21 Apr 2025 18:23:59 -0400 Subject: [PATCH 08/12] Apply to UK variant as well --- zhaquirks/develco/smart_siren.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/zhaquirks/develco/smart_siren.py b/zhaquirks/develco/smart_siren.py index 9b904496b6..1e25a598d2 100644 --- a/zhaquirks/develco/smart_siren.py +++ b/zhaquirks/develco/smart_siren.py @@ -13,6 +13,7 @@ ( QuirkBuilder("frient A/S", "SIRZB-111") + .applies_to("frient A/S", "SIRZB-110") # Hide the default `ias_zone` entity .prevent_default_entity_creation( endpoint_id=43, @@ -30,6 +31,7 @@ translation_key="tamper", fallback_name="Tamper", ) + # This is a mains-powered device that has a backup battery .sensor( attribute_name=PowerConfiguration.AttributeDefs.battery_percentage_remaining.name, cluster_id=PowerConfiguration.cluster_id, From 4f26e4f839baeeec7f893527f9ae17c7bb0c39ad Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 21 Apr 2025 18:37:22 -0400 Subject: [PATCH 09/12] Simplify heat alarm --- zhaquirks/develco/heat_alarm.py | 220 ++----------------------------- zhaquirks/develco/smoke_alarm.py | 2 +- 2 files changed, 11 insertions(+), 211 deletions(-) diff --git a/zhaquirks/develco/heat_alarm.py b/zhaquirks/develco/heat_alarm.py index d6334c31b2..0c2addc1f4 100644 --- a/zhaquirks/develco/heat_alarm.py +++ b/zhaquirks/develco/heat_alarm.py @@ -1,213 +1,13 @@ -"""Develco Heat Alarm.""" +"""Frient Heat Detector.""" -import zigpy.profiles.zha -from zigpy.quirks import CustomDevice -from zigpy.zcl.clusters.general import ( - Basic, - BinaryInput, - Identify, - OnOff, - Ota, - PollControl, - PowerConfiguration, - Scenes, - Time, -) -from zigpy.zcl.clusters.measurement import TemperatureMeasurement -from zigpy.zcl.clusters.security import IasWd, IasZone - -from zhaquirks.const import ( - DEVICE_TYPE, - ENDPOINTS, - INPUT_CLUSTERS, - MODELS_INFO, - OUTPUT_CLUSTERS, - PROFILE_ID, -) -from zhaquirks.develco import DEVELCO, FRIENT, DevelcoIasZone, DevelcoPowerConfiguration - -MANUFACTURER = 0x1015 - - -class HESZB120(CustomDevice): - """Custom device heat alarm.""" - - manufacturer_id_override = MANUFACTURER +from zigpy.quirks.v2 import QuirkBuilder - signature = { - # - # - # - MODELS_INFO: [(DEVELCO, "HESZB-120"), (FRIENT, "HESZB-120")], - ENDPOINTS: { - 1: { - PROFILE_ID: 49353, - DEVICE_TYPE: 1, - INPUT_CLUSTERS: [ - Identify.cluster_id, - Scenes.cluster_id, - OnOff.cluster_id, - ], - OUTPUT_CLUSTERS: [], - }, - 35: { - PROFILE_ID: zigpy.profiles.zha.PROFILE_ID, - DEVICE_TYPE: zigpy.profiles.zha.DeviceType.IAS_CONTROL, - INPUT_CLUSTERS: [ - Basic.cluster_id, - PowerConfiguration.cluster_id, - Identify.cluster_id, - BinaryInput.cluster_id, - PollControl.cluster_id, - IasZone.cluster_id, - IasWd.cluster_id, - ], - OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], - }, - 38: { - PROFILE_ID: zigpy.profiles.zha.PROFILE_ID, - DEVICE_TYPE: zigpy.profiles.zha.DeviceType.TEMPERATURE_SENSOR, - INPUT_CLUSTERS: [ - Basic.cluster_id, - Identify.cluster_id, - TemperatureMeasurement.cluster_id, - ], - OUTPUT_CLUSTERS: [Identify.cluster_id], - }, - }, - } +from . import DevelcoIasZone, DevelcoPowerConfiguration - replacement = { - ENDPOINTS: { - 1: { - PROFILE_ID: 49353, - DEVICE_TYPE: 1, - INPUT_CLUSTERS: [ - Identify.cluster_id, - Scenes.cluster_id, - OnOff.cluster_id, - ], - OUTPUT_CLUSTERS: [], - }, - 35: { - PROFILE_ID: zigpy.profiles.zha.PROFILE_ID, - DEVICE_TYPE: zigpy.profiles.zha.DeviceType.IAS_CONTROL, - INPUT_CLUSTERS: [ - Basic.cluster_id, - DevelcoPowerConfiguration, - Identify.cluster_id, - BinaryInput.cluster_id, - PollControl.cluster_id, - DevelcoIasZone, - IasWd.cluster_id, - ], - OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], - }, - 38: { - PROFILE_ID: zigpy.profiles.zha.PROFILE_ID, - DEVICE_TYPE: zigpy.profiles.zha.DeviceType.TEMPERATURE_SENSOR, - INPUT_CLUSTERS: [ - Basic.cluster_id, - Identify.cluster_id, - TemperatureMeasurement.cluster_id, - ], - OUTPUT_CLUSTERS: [Identify.cluster_id], - }, - }, - } - - -class HESZB120F(CustomDevice): - """Frient A/S Heat Alarm.""" - - manufacturer_id_override = MANUFACTURER - - signature = { - # - # - # - MODELS_INFO: [ - (FRIENT, "HESZB-120"), - ], - ENDPOINTS: { - 1: { - PROFILE_ID: 49353, - DEVICE_TYPE: 1, - INPUT_CLUSTERS: [ - Identify.cluster_id, - Scenes.cluster_id, - OnOff.cluster_id, - ], - OUTPUT_CLUSTERS: [], - }, - 35: { - PROFILE_ID: zigpy.profiles.zha.PROFILE_ID, - DEVICE_TYPE: zigpy.profiles.zha.DeviceType.IAS_ZONE, - INPUT_CLUSTERS: [ - Basic.cluster_id, - PowerConfiguration.cluster_id, - Identify.cluster_id, - BinaryInput.cluster_id, - PollControl.cluster_id, - IasZone.cluster_id, - IasWd.cluster_id, - ], - OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], - }, - 38: { - PROFILE_ID: zigpy.profiles.zha.PROFILE_ID, - DEVICE_TYPE: zigpy.profiles.zha.DeviceType.TEMPERATURE_SENSOR, - INPUT_CLUSTERS: [ - Basic.cluster_id, - Identify.cluster_id, - TemperatureMeasurement.cluster_id, - ], - OUTPUT_CLUSTERS: [Identify.cluster_id], - }, - }, - } - - replacement = { - ENDPOINTS: { - 1: { - PROFILE_ID: 49353, - DEVICE_TYPE: 1, - INPUT_CLUSTERS: [ - Identify.cluster_id, - Scenes.cluster_id, - OnOff.cluster_id, - ], - OUTPUT_CLUSTERS: [], - }, - 35: { - PROFILE_ID: zigpy.profiles.zha.PROFILE_ID, - DEVICE_TYPE: zigpy.profiles.zha.DeviceType.IAS_ZONE, - INPUT_CLUSTERS: [ - Basic.cluster_id, - DevelcoPowerConfiguration, - Identify.cluster_id, - BinaryInput.cluster_id, - PollControl.cluster_id, - DevelcoIasZone, - IasWd.cluster_id, - ], - OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], - }, - 38: { - PROFILE_ID: zigpy.profiles.zha.PROFILE_ID, - DEVICE_TYPE: zigpy.profiles.zha.DeviceType.TEMPERATURE_SENSOR, - INPUT_CLUSTERS: [ - Basic.cluster_id, - Identify.cluster_id, - TemperatureMeasurement.cluster_id, - ], - OUTPUT_CLUSTERS: [Identify.cluster_id], - }, - }, - } +( + QuirkBuilder("frient A/S", "HESZB-120") + .applies_to("Develco Products A/S", "HESZB-120") + .replaces(DevelcoIasZone, endpoint_id=35) + .replaces(DevelcoPowerConfiguration, endpoint_id=35) + .add_to_registry() +) diff --git a/zhaquirks/develco/smoke_alarm.py b/zhaquirks/develco/smoke_alarm.py index 30123063b0..1ecd58d82f 100644 --- a/zhaquirks/develco/smoke_alarm.py +++ b/zhaquirks/develco/smoke_alarm.py @@ -1,4 +1,4 @@ -"""Develco Smoke Alarm.""" +"""Frient Smoke Alarm.""" from zigpy.quirks.v2 import QuirkBuilder From bd1652de265427d941ad9df6145c211d8595b460 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 22 Apr 2025 12:22:55 -0400 Subject: [PATCH 10/12] Add a quirk for the Frient EMI devices --- zhaquirks/develco/emi.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 zhaquirks/develco/emi.py diff --git a/zhaquirks/develco/emi.py b/zhaquirks/develco/emi.py new file mode 100644 index 0000000000..2e2a5eb1c7 --- /dev/null +++ b/zhaquirks/develco/emi.py @@ -0,0 +1,15 @@ +"""Frient Electricity Meter Interface.""" + +from zigpy.quirks.v2 import QuirkBuilder + +( + QuirkBuilder("frient A/S", "EMIZB-151") + # These endpoints are duplicates and completely broken: each one is a "mirror" of + # endpoint 2 and will set up duplicate attribute reporting for every attribute, the + # attribute reports will instead be emitted from endpoint 2! + .prevent_default_entity_creation(endpoint_id=64) + .prevent_default_entity_creation(endpoint_id=65) + .prevent_default_entity_creation(endpoint_id=66) + .prevent_default_entity_creation(endpoint_id=67) + .add_to_registry() +) From d3f41cbbb14661a995c9e40a6ef4b1b0546b82ad Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 22 Apr 2025 15:43:26 -0400 Subject: [PATCH 11/12] WIP: power plug --- zhaquirks/develco/power_plug.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/zhaquirks/develco/power_plug.py b/zhaquirks/develco/power_plug.py index 60a12b16da..936331c1fe 100644 --- a/zhaquirks/develco/power_plug.py +++ b/zhaquirks/develco/power_plug.py @@ -8,6 +8,7 @@ ) from zigpy.quirks.v2.homeassistant import UnitOfTemperature from zigpy.zcl.clusters.general import DeviceTemperature +from zigpy.zcl.clusters.smartenergy import Metering ( QuirkBuilder("frient A/S", "SPLZB-141") @@ -17,6 +18,14 @@ cluster_id=DeviceTemperature.cluster_id, function=lambda entity: entity.__class__.__name__ == "DeviceTemperature", ) + # This attribute does not actually work + .prevent_default_entity_creation( + endpoint_id=2, + cluster_id=Metering.cluster_id, + function=lambda entity: ( + entity.info_object.translation_key == "summation_delivered" + ), + ) .sensor( endpoint_id=2, cluster_id=DeviceTemperature.cluster_id, From 9e2ea69a8971996b9ff71e5ecd108c9b82901583 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 29 Apr 2025 15:20:47 -0400 Subject: [PATCH 12/12] WIP: IO module --- zhaquirks/develco/io_module.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 zhaquirks/develco/io_module.py diff --git a/zhaquirks/develco/io_module.py b/zhaquirks/develco/io_module.py new file mode 100644 index 0000000000..9448b3090e --- /dev/null +++ b/zhaquirks/develco/io_module.py @@ -0,0 +1,16 @@ +"""Develco IO Module.""" + +from zigpy.quirks.v2 import QuirkBuilder + +( + QuirkBuilder("frient A/S", "IOMZB-110") + # Name the two outputs + # .add_metadata(unique_id="116-0x0006-on_off", name="COM 1") + # .add_metadata(unique_id="117-0x0006-on_off", name="COM 2") + # And the two inputs + # .add_metadata(unique_id="112-0x000f-binary_input", name="IN1") + # .add_metadata(unique_id="112-0x000f-binary_input", name="IN2") + # .add_metadata(unique_id="112-0x000f-binary_input", name="IN3") + # .add_metadata(unique_id="112-0x000f-binary_input", name="IN4") + .add_to_registry() +)