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

Fix power measurement for Aqara MAEU01 plugs with newer firmware #1656

Merged
merged 22 commits into from
Oct 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
6d61bbb
Add alternative Xiaomi MMEU01 plug signatures
TheJulianJES Jul 11, 2022
f092b60
Improve custom Xiaomi ElectricalMeasurementCluster
TheJulianJES Jul 17, 2022
617ae9b
Use custom Xiaomi ElectricalMeasurementCluster, BasicCluter for MAEU0…
TheJulianJES Jul 17, 2022
949271a
Rename quirk file to plug_eu.py
TheJulianJES Jul 17, 2022
64ced42
Temporary test changes
TheJulianJES Aug 26, 2022
d6d8f9d
Add custom MeteringCluster for "current summation delivered" sensor i…
TheJulianJES Sep 1, 2022
9c14d93
Fix docs
TheJulianJES Sep 1, 2022
a69400c
Add OppleCluster to mmeu01, add alternative mmeu01 signatures, remove…
TheJulianJES Sep 1, 2022
5bdbd44
Simplify common signatures
TheJulianJES Sep 1, 2022
67ca94b
Change endpoint 21 replacement to more likely match actual signature …
TheJulianJES Sep 1, 2022
7bb06ad
Remove stale import
TheJulianJES Sep 1, 2022
413c152
Simplify MAEU01 replacement to use MMEU01 replacement
TheJulianJES Sep 1, 2022
7c74256
Add power_outage_memory
TheJulianJES Sep 5, 2022
78ab39a
Explicitly enable "OppleMode" now (set to 1 instead of 0)
TheJulianJES Sep 13, 2022
ffc8002
Add some comments
TheJulianJES Oct 16, 2022
ccfa93d
Add small test to validate OppleMode gets enabled on binding
TheJulianJES Oct 16, 2022
da5fcef
Remove stale comment
TheJulianJES Oct 16, 2022
f3656bc
Add Xiaomi EU plug current power consumption, total power consumption…
TheJulianJES Oct 16, 2022
34913b6
Also remove Xiaomi MMEU01 plug from group 0
TheJulianJES Oct 16, 2022
bac7e4a
Use create_catching_task from zigpy instead of asyncio.create_task
TheJulianJES Oct 17, 2022
4658eee
Remove plug from group 0 when binding OppleCluster, update tests
TheJulianJES Oct 17, 2022
deeeed6
Use catching task for writing OppleMode attribute, update test
TheJulianJES Oct 17, 2022
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
82 changes: 82 additions & 0 deletions tests/test_xiaomi.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@
ZONE_STATE,
)
from zhaquirks.xiaomi import (
CONSUMPTION_REPORTED,
LUMI,
POWER_REPORTED,
VOLTAGE_REPORTED,
XIAOMI_NODE_DESC,
BasicCluster,
XiaomiCustomDevice,
Expand All @@ -31,6 +34,7 @@
)
import zhaquirks.xiaomi.aqara.motion_aq2
import zhaquirks.xiaomi.aqara.motion_aq2b
import zhaquirks.xiaomi.aqara.plug_eu
import zhaquirks.xiaomi.mija.motion

from tests.common import ZCL_OCC_ATTR_RPT_OCC, ClusterListener
Expand Down Expand Up @@ -426,3 +430,81 @@ def test_attribute_parsing(raw_report):
# The only remaining data should be the data type and the length.
# Everything else is passed through unmodified.
assert len(raw_report) == 2 * len(reports[0])


@mock.patch("zigpy.zcl.Cluster.bind", mock.AsyncMock())
@pytest.mark.parametrize("quirk", (zhaquirks.xiaomi.aqara.plug_eu.PlugMAEU01,))
async def test_xiaomi_eu_plug_binding(zigpy_device_from_quirk, quirk):
"""Test binding Xiaomi EU plug sets OppleMode to True and removes the plug from group 0."""

device = zigpy_device_from_quirk(quirk)
opple_cluster = device.endpoints[1].opple_cluster

p1 = mock.patch.object(opple_cluster, "create_catching_task")
p2 = mock.patch.object(opple_cluster.endpoint, "request", mock.AsyncMock())

with p1 as mock_task, p2 as request_mock:
request_mock.return_value = (foundation.Status.SUCCESS, "done")

await opple_cluster.bind()

# Only removed the plug from group 0 so far
assert len(request_mock.mock_calls) == 1
assert mock_task.call_count == 1

assert request_mock.mock_calls[0][1] == (
4,
1,
b"\x01\x01\x03\x00\x00",
)

# Await call writing OppleMode attribute
await mock_task.call_args[0][0]

assert len(request_mock.mock_calls) == 2
assert request_mock.mock_calls[1][1] == (
64704,
2,
b"\x04_\x11\x02\x02\t\x00 \x01",
)


@pytest.mark.parametrize("quirk", (zhaquirks.xiaomi.aqara.plug_eu.PlugMAEU01,))
async def test_xiaomi_eu_plug_power(zigpy_device_from_quirk, quirk):
"""Test current power consumption, total power consumption, and current voltage on Xiaomi EU plug."""

device = zigpy_device_from_quirk(quirk)

em_cluster = device.endpoints[1].electrical_measurement
em_listener = ClusterListener(em_cluster)

# Test voltage on ElectricalMeasurement cluster
em_cluster.endpoint.device.voltage_bus.listener_event(VOLTAGE_REPORTED, 230)
assert len(em_listener.attribute_updates) == 1
assert em_listener.attribute_updates[0][0] == 1285
assert em_listener.attribute_updates[0][1] == 230

# Test current power consumption on ElectricalMeasurement cluster
em_cluster.endpoint.device.power_bus.listener_event(POWER_REPORTED, 15)
assert len(em_listener.attribute_updates) == 2
assert em_listener.attribute_updates[1][0] == 1291
assert em_listener.attribute_updates[1][1] == 150 # multiplied by 10

# Test total power consumption on ElectricalMeasurement cluster
em_cluster.endpoint.device.consumption_bus.listener_event(
CONSUMPTION_REPORTED, 0.001
)
assert len(em_listener.attribute_updates) == 3
assert em_listener.attribute_updates[2][0] == 772
assert em_listener.attribute_updates[2][1] == 1 # multiplied by 1000

# Test total power consumption on SmartEnergy cluster
se_cluster = device.endpoints[1].smartenergy_metering
se_listener = ClusterListener(se_cluster)

se_cluster.endpoint.device.consumption_bus.listener_event(
CONSUMPTION_REPORTED, 0.001
)
assert len(se_listener.attribute_updates) == 1
assert se_listener.attribute_updates[0][0] == 0
assert se_listener.attribute_updates[0][1] == 1 # multiplied by 1000
45 changes: 41 additions & 4 deletions zhaquirks/xiaomi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,8 @@ def _parse_aqara_attributes(self, value):
)
elif self.endpoint.device.model in [
"lumi.plug.maus01",
"lumi.plug.maeu01",
"lumi.plug.mmeu01",
"lumi.relay.c2acn01",
]:
attribute_names.update({149: CONSUMPTION, 150: VOLTAGE, 152: POWER})
Expand Down Expand Up @@ -552,13 +554,13 @@ class ElectricalMeasurementCluster(LocalDataCluster, ElectricalMeasurement):

cluster_id = ElectricalMeasurement.cluster_id
POWER_ID = 0x050B
VOLTAGE_ID = 0x0500
VOLTAGE_ID = 0x0505
CONSUMPTION_ID = 0x0304
_CONSTANT_ATTRIBUTES = {
0x0402: 1, # power_multiplier
0x0403: 1, # power_divisor
0x0604: 1, # ac_power_multiplier
0x0605: 1, # ac_power_divisor
0x0605: 10, # ac_power_divisor
}

def __init__(self, *args, **kwargs):
Expand All @@ -568,17 +570,52 @@ def __init__(self, *args, **kwargs):
self.endpoint.device.consumption_bus.add_listener(self)
self.endpoint.device.power_bus.add_listener(self)

# put a default value so the sensors are created
if self.POWER_ID not in self._attr_cache:
self._update_attribute(self.POWER_ID, 0)
if self.VOLTAGE_ID not in self._attr_cache:
self._update_attribute(self.VOLTAGE_ID, 0)
if self.CONSUMPTION_ID not in self._attr_cache:
self._update_attribute(self.CONSUMPTION_ID, 0)

def power_reported(self, value):
"""Power reported."""
self._update_attribute(self.POWER_ID, value)
self._update_attribute(self.POWER_ID, value * 10)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change made my another aqara outlet (mmeu01 square one) report 10* more usage than normal

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That should be fixed by either re-pairing the device or reading the "ac_power_divisor" attribute on the ElectricalMeasurement cluster through ZHA -> Devices -> Plug -> Manage Clusters -> ElectricalMeasurement -> ac_power_divisor -> read attribute.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reparing is workaround, I don't think this is good user experience to ask user to re-pair working device,
I think the solution is to remove this multiplication and change ac_power_divisor back to 1, I've changed it locally and both devices shows power consumption correctly for me

Copy link
Collaborator Author

@TheJulianJES TheJulianJES Aug 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. If possible, the "fake" attribute should be re-read automatically. There is another issue with it being left on 1 (different rounding when values like 1.9W are reported which causes a jump from 2W to 1W after some time with unchanged consumption).

If consumption varies a lot when low, it's also nice to see values in between 0W and 1W (and it not constantly jumping around).

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The power measurement on LLKZMK11LM (lumi.relay.c2acn01) is also wrong after this change. It's showing 100W for a 10W device.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hakong No, check the AC power divisor: 0x0605. It's set to 10 instead of 1 now. Both of these changes were made so that there's one decimal.


def voltage_reported(self, value):
"""Voltage reported."""
self._update_attribute(self.VOLTAGE_ID, value)

def consumption_reported(self, value):
"""Consumption reported."""
self._update_attribute(self.CONSUMPTION_ID, value)
self._update_attribute(self.CONSUMPTION_ID, value * 1000)


class MeteringCluster(LocalDataCluster, Metering):
"""Metering cluster to receive reports that are sent to the basic cluster."""

cluster_id = Metering.cluster_id
CURRENT_SUMM_DELIVERED_ID = 0x0000
_CONSTANT_ATTRIBUTES = {
0x0300: 0, # unit_of_measure: kWh
0x0301: 1, # multiplier
0x0302: 1000, # divisor
0x0303: 0b0_0100_011, # summation_formatting (read from plug)
0x0306: 0, # metering_device_type: electric
}

def __init__(self, *args, **kwargs):
"""Init."""
super().__init__(*args, **kwargs)
self.endpoint.device.consumption_bus.add_listener(self)

# put a default value so the sensor is created
if self.CURRENT_SUMM_DELIVERED_ID not in self._attr_cache:
self._update_attribute(self.CURRENT_SUMM_DELIVERED_ID, 0)

def consumption_reported(self, value):
"""Consumption reported."""
self._update_attribute(self.CURRENT_SUMM_DELIVERED_ID, value * 1000)


class IlluminanceMeasurementCluster(CustomCluster, IlluminanceMeasurement):
Expand Down
Loading