Skip to content

Commit 3acf032

Browse files
Add Aqara FP1E presence sensor v2 quirk (#3521)
Co-authored-by: TheJulianJES <[email protected]>
1 parent 213ce10 commit 3acf032

File tree

2 files changed

+240
-0
lines changed

2 files changed

+240
-0
lines changed

tests/test_xiaomi.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1756,3 +1756,51 @@ def test_aqara_acn014_signature_match(assert_signature_matches_quirk):
17561756
assert_signature_matches_quirk(
17571757
zhaquirks.xiaomi.aqara.light_acn.LumiLightAcn014, signature
17581758
)
1759+
1760+
1761+
@pytest.mark.parametrize(
1762+
"occupancy_value, expected_occ_status, motion_value, expected_motion_status",
1763+
[
1764+
(0, OccupancySensing.Occupancy.Unoccupied, 2, 0),
1765+
(1, OccupancySensing.Occupancy.Occupied, 3, IasZone.ZoneStatus.Alarm_1),
1766+
(1, OccupancySensing.Occupancy.Occupied, 4, 0),
1767+
],
1768+
)
1769+
async def test_aqara_fp1e_sensor(
1770+
zigpy_device_from_v2_quirk,
1771+
occupancy_value,
1772+
expected_occ_status,
1773+
motion_value,
1774+
expected_motion_status,
1775+
):
1776+
"""Test Aqara FP1E sensor."""
1777+
quirk = zigpy_device_from_v2_quirk("aqara", "lumi.sensor_occupy.agl1")
1778+
1779+
opple_cluster = quirk.endpoints[1].opple_cluster
1780+
ias_cluster = quirk.endpoints[1].ias_zone
1781+
occupancy_cluster = quirk.endpoints[1].occupancy
1782+
1783+
opple_listener = ClusterListener(opple_cluster)
1784+
ias_listener = ClusterListener(ias_cluster)
1785+
occupancy_listener = ClusterListener(occupancy_cluster)
1786+
1787+
# update custom occupancy attribute id
1788+
opple_cluster.update_attribute(0x0142, occupancy_value)
1789+
assert len(opple_listener.attribute_updates) == 1
1790+
1791+
# confirm occupancy cluster is updated
1792+
assert len(occupancy_listener.attribute_updates) == 1
1793+
assert (
1794+
occupancy_listener.attribute_updates[0][0]
1795+
== OccupancySensing.AttributeDefs.occupancy.id
1796+
)
1797+
assert occupancy_listener.attribute_updates[0][1] == expected_occ_status
1798+
1799+
# update custom motion attribute id
1800+
opple_cluster.update_attribute(0x0160, motion_value)
1801+
assert len(opple_listener.attribute_updates) == 2
1802+
1803+
# confirm ias cluster is updated
1804+
assert len(ias_listener.attribute_updates) == 1
1805+
assert ias_listener.attribute_updates[0][0] == IasZone.AttributeDefs.zone_status.id
1806+
assert ias_listener.attribute_updates[0][1] == expected_motion_status
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
"""Quirk for aqara lumi.sensor_occupy.agl1."""
2+
3+
from __future__ import annotations
4+
5+
from typing import Any
6+
7+
from zigpy import types
8+
from zigpy.quirks.v2 import (
9+
NumberDeviceClass,
10+
QuirkBuilder,
11+
SensorDeviceClass,
12+
SensorStateClass,
13+
)
14+
from zigpy.quirks.v2.homeassistant import EntityType, UnitOfLength
15+
from zigpy.zcl.clusters.general import DeviceTemperature
16+
from zigpy.zcl.clusters.measurement import OccupancySensing
17+
from zigpy.zcl.clusters.security import IasZone
18+
from zigpy.zcl.foundation import BaseAttributeDefs, ZCLAttributeDef
19+
20+
from zhaquirks import LocalDataCluster
21+
from zhaquirks.xiaomi import XiaomiAqaraE1Cluster
22+
23+
24+
class AqaraMotion(types.enum8):
25+
"""Aqara motion attribute values."""
26+
27+
Idle = 0x02
28+
Moving = 0x03
29+
Still = 0x04
30+
31+
32+
class AqaraMotionSensitivity(types.enum8):
33+
"""Aqara motion sensitivity attribute values."""
34+
35+
Low = 0x01
36+
Medium = 0x02
37+
High = 0x03
38+
39+
40+
class AqaraOccupancy(types.enum8):
41+
"""Aqara occupancy attribute values."""
42+
43+
Unoccupied = 0x00
44+
Occupied = 0x01
45+
46+
47+
class IasZoneLocal(LocalDataCluster, IasZone):
48+
"""Virtual cluster for IasZone."""
49+
50+
_CONSTANT_ATTRIBUTES = {
51+
IasZone.AttributeDefs.zone_type.id: IasZone.ZoneType.Motion_Sensor
52+
}
53+
_VALID_ATTRIBUTES = {IasZone.AttributeDefs.zone_status.id}
54+
55+
56+
class OccupancySensingLocal(LocalDataCluster, OccupancySensing):
57+
"""Virtual cluster for OccupancySensing."""
58+
59+
_VALID_ATTRIBUTES = {OccupancySensing.AttributeDefs.occupancy.id}
60+
61+
62+
class OppleCluster(XiaomiAqaraE1Cluster):
63+
"""Aqara manufacturer cluster for the presence sensor FP1E."""
64+
65+
class AttributeDefs(BaseAttributeDefs):
66+
"""Manufacturer specific attributes."""
67+
68+
# The configurable maximum detection distance in millimeters (default 600 = 6 meters).
69+
approach_distance = ZCLAttributeDef(
70+
id=0x015B,
71+
type=types.uint32_t,
72+
access="rw",
73+
is_manufacturer_specific=True,
74+
)
75+
76+
# Detected motion
77+
motion = ZCLAttributeDef(
78+
id=0x0160,
79+
type=AqaraMotion,
80+
access="rp",
81+
is_manufacturer_specific=True,
82+
)
83+
84+
# Distance to the detected motion in millimeters
85+
motion_distance = ZCLAttributeDef(
86+
id=0x015F,
87+
type=types.uint32_t,
88+
access="rp",
89+
is_manufacturer_specific=True,
90+
)
91+
92+
# The configurable detection sensitivity
93+
motion_sensitivity = ZCLAttributeDef(
94+
id=0x010C,
95+
type=AqaraMotionSensitivity,
96+
access="rw",
97+
is_manufacturer_specific=True,
98+
)
99+
100+
# Detected occupancy
101+
occupancy = ZCLAttributeDef(
102+
id=0x0142,
103+
type=AqaraOccupancy,
104+
access="rp",
105+
is_manufacturer_specific=True,
106+
)
107+
108+
# Trigger AI spatial learning (write 1)
109+
reset_no_presence_status = ZCLAttributeDef(
110+
id=0x0157,
111+
type=types.uint8_t,
112+
access="w",
113+
is_manufacturer_specific=True,
114+
)
115+
116+
# Trigger device restart (write 0)
117+
restart_device = ZCLAttributeDef(
118+
id=0x00E8,
119+
type=types.Bool,
120+
access="w",
121+
is_manufacturer_specific=True,
122+
)
123+
124+
def _update_attribute(self, attrid: int, value: Any) -> None:
125+
super()._update_attribute(attrid, value)
126+
if attrid == self.AttributeDefs.occupancy.id:
127+
self.endpoint.occupancy.update_attribute(
128+
OccupancySensing.AttributeDefs.occupancy.id,
129+
OccupancySensing.Occupancy.Occupied
130+
if value == AqaraOccupancy.Occupied
131+
else OccupancySensing.Occupancy.Unoccupied,
132+
)
133+
elif attrid == self.AttributeDefs.motion.id:
134+
self.endpoint.ias_zone.update_attribute(
135+
IasZone.AttributeDefs.zone_status.id,
136+
IasZone.ZoneStatus.Alarm_1 if value == AqaraMotion.Moving else 0,
137+
)
138+
139+
140+
(
141+
QuirkBuilder("aqara", "lumi.sensor_occupy.agl1")
142+
.friendly_name(manufacturer="Aqara", model="Presence Sensor FP1E")
143+
.adds(DeviceTemperature)
144+
.adds(OccupancySensingLocal)
145+
.adds(IasZoneLocal)
146+
.replaces(OppleCluster)
147+
.number(
148+
OppleCluster.AttributeDefs.approach_distance.name,
149+
OppleCluster.cluster_id,
150+
min_value=0,
151+
max_value=6,
152+
step=0.1,
153+
unit=UnitOfLength.METERS,
154+
multiplier=0.01,
155+
device_class=NumberDeviceClass.DISTANCE,
156+
translation_key="approach_distance",
157+
fallback_name="Approach distance",
158+
)
159+
.sensor(
160+
OppleCluster.AttributeDefs.motion_distance.name,
161+
OppleCluster.cluster_id,
162+
unit=UnitOfLength.METERS,
163+
multiplier=0.01,
164+
device_class=SensorDeviceClass.DISTANCE,
165+
state_class=SensorStateClass.MEASUREMENT,
166+
translation_key="motion_distance",
167+
fallback_name="Motion distance",
168+
)
169+
.enum(
170+
OppleCluster.AttributeDefs.motion_sensitivity.name,
171+
AqaraMotionSensitivity,
172+
OppleCluster.cluster_id,
173+
translation_key="motion_sensitivity",
174+
fallback_name="Motion sensitivity",
175+
)
176+
.write_attr_button(
177+
OppleCluster.AttributeDefs.reset_no_presence_status.name,
178+
1,
179+
OppleCluster.cluster_id,
180+
translation_key="reset_no_presence_status",
181+
fallback_name="Presence status reset",
182+
)
183+
.write_attr_button(
184+
OppleCluster.AttributeDefs.restart_device.name,
185+
0,
186+
OppleCluster.cluster_id,
187+
entity_type=EntityType.DIAGNOSTIC,
188+
translation_key="restart_device",
189+
fallback_name="Restart device",
190+
)
191+
.add_to_registry()
192+
)

0 commit comments

Comments
 (0)