From 9cdab83395cca07d8c1fb96f7c91d1472dae0f23 Mon Sep 17 00:00:00 2001 From: Jiaxin Date: Wed, 22 May 2024 15:24:18 +0800 Subject: [PATCH 1/6] Add quirks for lumi.motion.acn001 lumi.motion.acn001 Aqara E1 motion sensor RTCGQ15LM https://home.miot-spec.com/s/lumi-acn001 --- zhaquirks/xiaomi/aqara/motion_acn001.py | 90 +++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 zhaquirks/xiaomi/aqara/motion_acn001.py diff --git a/zhaquirks/xiaomi/aqara/motion_acn001.py b/zhaquirks/xiaomi/aqara/motion_acn001.py new file mode 100644 index 0000000000..70b865922a --- /dev/null +++ b/zhaquirks/xiaomi/aqara/motion_acn001.py @@ -0,0 +1,90 @@ +"""Xiaomi Aqara T1 motion sensor device lumi.motion.acn001.""" +from zigpy.profiles import zha +from zigpy.zcl.clusters.general import Identify, Ota +from zigpy.zcl.clusters.measurement import OccupancySensing, IlluminanceMeasurement + +from zhaquirks import Bus +from zhaquirks.const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) +from zhaquirks.xiaomi import ( + LUMI, + BasicCluster, + IlluminanceMeasurementCluster, + MotionCluster, + LocalOccupancyCluster, + XiaomiAqaraE1Cluster, + XiaomiCustomDevice, + XiaomiPowerConfiguration, +) + +XIAOMI_CLUSTER_ID = 0xFCC0 + + +class XiaomiManufacturerCluster(XiaomiAqaraE1Cluster): + """Xiaomi manufacturer cluster.""" + + def _update_attribute(self, attrid, value): + super()._update_attribute(attrid, value) + if attrid == 274: + value = value - 65536 + self.endpoint.illuminance.update_attribute( + IlluminanceMeasurement.AttributeDefs.measured_value.id, value + ) + self.endpoint.occupancy.update_attribute( + OccupancySensing.AttributeDefs.occupancy.id, + OccupancySensing.Occupancy.Occupied, + ) + + +class MotionACN001(XiaomiCustomDevice): + """Xiaomi motion sensor device lumi.motion.acn001.""" + + def __init__(self, *args, **kwargs): + """Init.""" + self.battery_size = 11 + self.motion_bus = Bus() + self.illuminance_bus = Bus() + super().__init__(*args, **kwargs) + + signature = { + MODELS_INFO: [(LUMI, "lumi.motion.acn001")], + ENDPOINTS: { + 1: { + PROFILE_ID: 0x0104, + DEVICE_TYPE: 0x0402, + INPUT_CLUSTERS: [ + 0x0000, + 0x0001, + 0x0003, + XIAOMI_CLUSTER_ID, + ], + OUTPUT_CLUSTERS: [ + 0x0003, + 0x0019, + ], + } + }, + } + + replacement = { + ENDPOINTS: { + 1: { + INPUT_CLUSTERS: [ + BasicCluster, + XiaomiPowerConfiguration, + Identify.cluster_id, + LocalOccupancyCluster, + MotionCluster, + IlluminanceMeasurementCluster, + XiaomiManufacturerCluster, + ], + OUTPUT_CLUSTERS: [Identify.cluster_id, Ota.cluster_id], + } + }, + } From 719b68f6bf75f9feb185e4987f09b47fe04ea21b Mon Sep 17 00:00:00 2001 From: Jiaxin Date: Sat, 25 May 2024 11:33:16 +0800 Subject: [PATCH 2/6] Improve code quality for motion_acn001.py --- zhaquirks/xiaomi/aqara/motion_acn001.py | 39 ++++++------------------- 1 file changed, 9 insertions(+), 30 deletions(-) diff --git a/zhaquirks/xiaomi/aqara/motion_acn001.py b/zhaquirks/xiaomi/aqara/motion_acn001.py index 70b865922a..efca629ec4 100644 --- a/zhaquirks/xiaomi/aqara/motion_acn001.py +++ b/zhaquirks/xiaomi/aqara/motion_acn001.py @@ -1,4 +1,4 @@ -"""Xiaomi Aqara T1 motion sensor device lumi.motion.acn001.""" +"""Xiaomi Aqara E1 motion sensor device lumi.motion.acn001.""" from zigpy.profiles import zha from zigpy.zcl.clusters.general import Identify, Ota from zigpy.zcl.clusters.measurement import OccupancySensing, IlluminanceMeasurement @@ -22,24 +22,7 @@ XiaomiCustomDevice, XiaomiPowerConfiguration, ) - -XIAOMI_CLUSTER_ID = 0xFCC0 - - -class XiaomiManufacturerCluster(XiaomiAqaraE1Cluster): - """Xiaomi manufacturer cluster.""" - - def _update_attribute(self, attrid, value): - super()._update_attribute(attrid, value) - if attrid == 274: - value = value - 65536 - self.endpoint.illuminance.update_attribute( - IlluminanceMeasurement.AttributeDefs.measured_value.id, value - ) - self.endpoint.occupancy.update_attribute( - OccupancySensing.AttributeDefs.occupancy.id, - OccupancySensing.Occupancy.Occupied, - ) +from zhaquirks.xiaomi.aqara.motion_agl02 import XiaomiManufacturerCluster class MotionACN001(XiaomiCustomDevice): @@ -49,25 +32,21 @@ def __init__(self, *args, **kwargs): """Init.""" self.battery_size = 11 self.motion_bus = Bus() - self.illuminance_bus = Bus() super().__init__(*args, **kwargs) signature = { MODELS_INFO: [(LUMI, "lumi.motion.acn001")], ENDPOINTS: { 1: { - PROFILE_ID: 0x0104, - DEVICE_TYPE: 0x0402, + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.IAS_ZONE, INPUT_CLUSTERS: [ - 0x0000, - 0x0001, - 0x0003, - XIAOMI_CLUSTER_ID, - ], - OUTPUT_CLUSTERS: [ - 0x0003, - 0x0019, + BasicCluster.cluster_id, + XiaomiPowerConfiguration.cluster_id, + Identify.cluster_id, + XiaomiAqaraE1Cluster.cluster_id, ], + OUTPUT_CLUSTERS: [Identify.cluster_id, Ota.cluster_id], } }, } From e59ab8ce78e0cea7f73c1e266155af85dbbb4fb9 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Tue, 28 May 2024 02:32:53 +0200 Subject: [PATCH 3/6] Fix ruff errors --- zhaquirks/xiaomi/aqara/motion_acn001.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/zhaquirks/xiaomi/aqara/motion_acn001.py b/zhaquirks/xiaomi/aqara/motion_acn001.py index efca629ec4..c84c921be5 100644 --- a/zhaquirks/xiaomi/aqara/motion_acn001.py +++ b/zhaquirks/xiaomi/aqara/motion_acn001.py @@ -1,7 +1,6 @@ """Xiaomi Aqara E1 motion sensor device lumi.motion.acn001.""" from zigpy.profiles import zha from zigpy.zcl.clusters.general import Identify, Ota -from zigpy.zcl.clusters.measurement import OccupancySensing, IlluminanceMeasurement from zhaquirks import Bus from zhaquirks.const import ( @@ -16,8 +15,8 @@ LUMI, BasicCluster, IlluminanceMeasurementCluster, - MotionCluster, LocalOccupancyCluster, + MotionCluster, XiaomiAqaraE1Cluster, XiaomiCustomDevice, XiaomiPowerConfiguration, From a0cd05aa11eb0dc11482ea503ce211e500e11044 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Tue, 28 May 2024 02:49:40 +0200 Subject: [PATCH 4/6] Extract `XiaomiMotionManufacturerCluster` out of motion quirks --- zhaquirks/xiaomi/__init__.py | 17 +++++++++++++++++ zhaquirks/xiaomi/aqara/motion_ac02.py | 23 +++++++---------------- zhaquirks/xiaomi/aqara/motion_acn001.py | 8 ++++---- zhaquirks/xiaomi/aqara/motion_agl02.py | 25 ++++--------------------- 4 files changed, 32 insertions(+), 41 deletions(-) diff --git a/zhaquirks/xiaomi/__init__.py b/zhaquirks/xiaomi/__init__.py index 91607cfa7b..f38efda49f 100644 --- a/zhaquirks/xiaomi/__init__.py +++ b/zhaquirks/xiaomi/__init__.py @@ -24,6 +24,7 @@ from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement from zigpy.zcl.clusters.measurement import ( IlluminanceMeasurement, + OccupancySensing, PressureMeasurement, RelativeHumidity, TemperatureMeasurement, @@ -464,6 +465,22 @@ class XiaomiAqaraE1Cluster(XiaomiCluster): ep_attribute = "opple_cluster" +class XiaomiMotionManufacturerCluster(XiaomiAqaraE1Cluster): + """Xiaomi manufacturer cluster to parse motion and illuminance reports.""" + + def _update_attribute(self, attrid, value): + super()._update_attribute(attrid, value) + if attrid == 274: + value = value - 65536 + self.endpoint.illuminance.update_attribute( + IlluminanceMeasurement.AttributeDefs.measured_value.id, value + ) + self.endpoint.occupancy.update_attribute( + OccupancySensing.AttributeDefs.occupancy.id, + OccupancySensing.Occupancy.Occupied, + ) + + class BinaryOutputInterlock(CustomCluster, BinaryOutput): """Xiaomi binaryoutput cluster with added interlock attribute.""" diff --git a/zhaquirks/xiaomi/aqara/motion_ac02.py b/zhaquirks/xiaomi/aqara/motion_ac02.py index efb52175b6..c372691828 100644 --- a/zhaquirks/xiaomi/aqara/motion_ac02.py +++ b/zhaquirks/xiaomi/aqara/motion_ac02.py @@ -8,7 +8,6 @@ from zigpy.profiles import zha from zigpy.quirks import CustomDevice from zigpy.zcl.clusters.general import Basic, Identify, Ota, PowerConfiguration -from zigpy.zcl.clusters.measurement import IlluminanceMeasurement, OccupancySensing from zhaquirks import Bus, LocalDataCluster from zhaquirks.const import ( @@ -23,7 +22,7 @@ LocalIlluminanceMeasurementCluster, MotionCluster, OccupancyCluster, - XiaomiAqaraE1Cluster, + XiaomiMotionManufacturerCluster, XiaomiPowerConfiguration, ) @@ -34,8 +33,12 @@ _LOGGER = logging.getLogger(__name__) -class OppleCluster(XiaomiAqaraE1Cluster): - """Opple cluster.""" +class OppleCluster(XiaomiMotionManufacturerCluster): + """Xiaomi manufacturer cluster. + + This uses the shared XiaomiMotionManufacturerCluster implementation + which parses motion and illuminance reports from Xiaomi devices. + """ attributes = { DETECTION_INTERVAL: ("detection_interval", types.uint8_t, True), @@ -43,18 +46,6 @@ class OppleCluster(XiaomiAqaraE1Cluster): TRIGGER_INDICATOR: ("trigger_indicator", types.uint8_t, True), } - def _update_attribute(self, attrid: int, value: Any) -> None: - super()._update_attribute(attrid, value) - if attrid == MOTION_ATTRIBUTE: - value = value - 65536 - self.endpoint.illuminance.update_attribute( - IlluminanceMeasurement.AttributeDefs.measured_value.id, value - ) - self.endpoint.occupancy.update_attribute( - OccupancySensing.AttributeDefs.occupancy.id, - OccupancySensing.Occupancy.Occupied, - ) - async def write_attributes( self, attributes: dict[str | int, Any], manufacturer: int | None = None ) -> list: diff --git a/zhaquirks/xiaomi/aqara/motion_acn001.py b/zhaquirks/xiaomi/aqara/motion_acn001.py index c84c921be5..168372aa01 100644 --- a/zhaquirks/xiaomi/aqara/motion_acn001.py +++ b/zhaquirks/xiaomi/aqara/motion_acn001.py @@ -1,4 +1,4 @@ -"""Xiaomi Aqara E1 motion sensor device lumi.motion.acn001.""" +"""Xiaomi Aqara E1 motion sensor device.""" from zigpy.profiles import zha from zigpy.zcl.clusters.general import Identify, Ota @@ -19,12 +19,12 @@ MotionCluster, XiaomiAqaraE1Cluster, XiaomiCustomDevice, + XiaomiMotionManufacturerCluster, XiaomiPowerConfiguration, ) -from zhaquirks.xiaomi.aqara.motion_agl02 import XiaomiManufacturerCluster -class MotionACN001(XiaomiCustomDevice): +class MotionE1(XiaomiCustomDevice): """Xiaomi motion sensor device lumi.motion.acn001.""" def __init__(self, *args, **kwargs): @@ -60,7 +60,7 @@ def __init__(self, *args, **kwargs): LocalOccupancyCluster, MotionCluster, IlluminanceMeasurementCluster, - XiaomiManufacturerCluster, + XiaomiMotionManufacturerCluster, ], OUTPUT_CLUSTERS: [Identify.cluster_id, Ota.cluster_id], } diff --git a/zhaquirks/xiaomi/aqara/motion_agl02.py b/zhaquirks/xiaomi/aqara/motion_agl02.py index e8c0fd1ac0..30223320e5 100644 --- a/zhaquirks/xiaomi/aqara/motion_agl02.py +++ b/zhaquirks/xiaomi/aqara/motion_agl02.py @@ -3,7 +3,7 @@ from zigpy.profiles import zha from zigpy.zcl.clusters.general import Identify, Ota -from zigpy.zcl.clusters.measurement import IlluminanceMeasurement, OccupancySensing +from zigpy.zcl.clusters.measurement import OccupancySensing from zhaquirks import Bus from zhaquirks.const import ( @@ -20,29 +20,11 @@ IlluminanceMeasurementCluster, LocalOccupancyCluster, MotionCluster, - XiaomiAqaraE1Cluster, XiaomiCustomDevice, + XiaomiMotionManufacturerCluster, XiaomiPowerConfiguration, ) -XIAOMI_CLUSTER_ID = 0xFCC0 - - -class XiaomiManufacturerCluster(XiaomiAqaraE1Cluster): - """Xiaomi manufacturer cluster.""" - - def _update_attribute(self, attrid, value): - super()._update_attribute(attrid, value) - if attrid == 274: - value = value - 65536 - self.endpoint.illuminance.update_attribute( - IlluminanceMeasurement.AttributeDefs.measured_value.id, value - ) - self.endpoint.occupancy.update_attribute( - OccupancySensing.AttributeDefs.occupancy.id, - OccupancySensing.Occupancy.Occupied, - ) - class MotionT1(XiaomiCustomDevice): """Xiaomi motion sensor device.""" @@ -73,6 +55,7 @@ def __init__(self, *args, **kwargs): } }, } + replacement = { ENDPOINTS: { 1: { @@ -83,7 +66,7 @@ def __init__(self, *args, **kwargs): LocalOccupancyCluster, MotionCluster, IlluminanceMeasurementCluster, - XiaomiManufacturerCluster, + XiaomiMotionManufacturerCluster, ], OUTPUT_CLUSTERS: [Identify.cluster_id, Ota.cluster_id], } From 040e4c76f005f4b65ff80bb03bbcabc52888421f Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Tue, 28 May 2024 02:50:09 +0200 Subject: [PATCH 5/6] Fix illuminance Aqara attribute reports not being parsed --- zhaquirks/xiaomi/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zhaquirks/xiaomi/__init__.py b/zhaquirks/xiaomi/__init__.py index f38efda49f..251d13742b 100644 --- a/zhaquirks/xiaomi/__init__.py +++ b/zhaquirks/xiaomi/__init__.py @@ -390,7 +390,7 @@ def _parse_aqara_attributes(self, value): attribute_names.update({11: ILLUMINANCE_MEASUREMENT}) elif self.endpoint.device.model == "lumi.curtain.acn002": attribute_names.update({101: BATTERY_PERCENTAGE_REMAINING_ATTRIBUTE}) - elif self.endpoint.device.model in ["lumi.motion.agl02", "lumi.motion.ac02"]: + elif self.endpoint.device.model in ["lumi.motion.agl02", "lumi.motion.ac02", "lumi.motion.acn001"]: attribute_names.update({101: ILLUMINANCE_MEASUREMENT}) if self.endpoint.device.model == "lumi.motion.ac02": attribute_names.update({105: DETECTION_INTERVAL}) From 08618e9ab670be6b84b418645186895bd0b47946 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Tue, 28 May 2024 02:50:26 +0200 Subject: [PATCH 6/6] Add test for E1 sensor --- tests/test_xiaomi.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_xiaomi.py b/tests/test_xiaomi.py index 3836c79c47..f534e0932a 100644 --- a/tests/test_xiaomi.py +++ b/tests/test_xiaomi.py @@ -1135,12 +1135,13 @@ async def test_xiaomi_e1_thermostat_schedule_settings_deserialization( ( (zhaquirks.xiaomi.aqara.motion_ac02.LumiMotionAC02, 0), (zhaquirks.xiaomi.aqara.motion_agl02.MotionT1, -1), + (zhaquirks.xiaomi.aqara.motion_acn001.MotionE1, -1), ), ) async def test_xiaomi_p1_t1_motion_sensor( zigpy_device_from_quirk, quirk, invalid_iilluminance_report ): - """Test Aqara P1 and T1 motion sensors.""" + """Test Aqara P1, T1, and E1 motion sensors.""" device = zigpy_device_from_quirk(quirk) @@ -1192,12 +1193,12 @@ async def test_xiaomi_p1_t1_motion_sensor( opple_cluster.update_attribute(274, 0xFFFF) # confirm invalid illuminance report is interpreted as 0 for P1 sensor, - # and -1 for the T1 sensor, as it doesn't seem like the T1 sensor sends invalid illuminance reports + # and -1 for the T1/E1 sensors, as they don't seem to send invalid illuminance reports assert len(illuminance_listener.attribute_updates) == 2 assert illuminance_listener.attribute_updates[1][0] == zcl_iilluminance_id assert illuminance_listener.attribute_updates[1][1] == invalid_iilluminance_report - # send illuminance report only + # send illuminance report only, parsed via Xiaomi cluster implementation opple_cluster.update_attribute( XIAOMI_AQARA_ATTRIBUTE_E1, create_aqara_attr_report({101: 20}) )