Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Aqara E1 motion sensor support #3163

Merged
merged 7 commits into from
May 28, 2024
Merged
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
7 changes: 4 additions & 3 deletions tests/test_xiaomi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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})
)
Expand Down
19 changes: 18 additions & 1 deletion zhaquirks/xiaomi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement
from zigpy.zcl.clusters.measurement import (
IlluminanceMeasurement,
OccupancySensing,
PressureMeasurement,
RelativeHumidity,
TemperatureMeasurement,
Expand Down Expand Up @@ -389,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})
Expand Down Expand Up @@ -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."""

Expand Down
23 changes: 7 additions & 16 deletions zhaquirks/xiaomi/aqara/motion_ac02.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -23,7 +22,7 @@
LocalIlluminanceMeasurementCluster,
MotionCluster,
OccupancyCluster,
XiaomiAqaraE1Cluster,
XiaomiMotionManufacturerCluster,
XiaomiPowerConfiguration,
)

Expand All @@ -34,27 +33,19 @@
_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),
MOTION_SENSITIVITY: ("motion_sensitivity", types.uint8_t, True),
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:
Expand Down
68 changes: 68 additions & 0 deletions zhaquirks/xiaomi/aqara/motion_acn001.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""Xiaomi Aqara E1 motion sensor device."""
from zigpy.profiles import zha
from zigpy.zcl.clusters.general import Identify, Ota

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,
LocalOccupancyCluster,
MotionCluster,
XiaomiAqaraE1Cluster,
XiaomiCustomDevice,
XiaomiMotionManufacturerCluster,
XiaomiPowerConfiguration,
)


class MotionE1(XiaomiCustomDevice):
"""Xiaomi motion sensor device lumi.motion.acn001."""

def __init__(self, *args, **kwargs):
"""Init."""
self.battery_size = 11
self.motion_bus = Bus()
super().__init__(*args, **kwargs)

signature = {
MODELS_INFO: [(LUMI, "lumi.motion.acn001")],
ENDPOINTS: {
1: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.IAS_ZONE,
INPUT_CLUSTERS: [
BasicCluster.cluster_id,
XiaomiPowerConfiguration.cluster_id,
Identify.cluster_id,
XiaomiAqaraE1Cluster.cluster_id,
],
OUTPUT_CLUSTERS: [Identify.cluster_id, Ota.cluster_id],
}
},
}

replacement = {
ENDPOINTS: {
1: {
INPUT_CLUSTERS: [
BasicCluster,
XiaomiPowerConfiguration,
Identify.cluster_id,
LocalOccupancyCluster,
MotionCluster,
IlluminanceMeasurementCluster,
XiaomiMotionManufacturerCluster,
],
OUTPUT_CLUSTERS: [Identify.cluster_id, Ota.cluster_id],
}
},
}
25 changes: 4 additions & 21 deletions zhaquirks/xiaomi/aqara/motion_agl02.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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."""
Expand Down Expand Up @@ -73,6 +55,7 @@ def __init__(self, *args, **kwargs):
}
},
}

replacement = {
ENDPOINTS: {
1: {
Expand All @@ -83,7 +66,7 @@ def __init__(self, *args, **kwargs):
LocalOccupancyCluster,
MotionCluster,
IlluminanceMeasurementCluster,
XiaomiManufacturerCluster,
XiaomiMotionManufacturerCluster,
],
OUTPUT_CLUSTERS: [Identify.cluster_id, Ota.cluster_id],
}
Expand Down
Loading