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

Refactor Aqara Roller Driver E1 as v2 quirk to expose configuration and status entities #3686

Open
wants to merge 4 commits into
base: dev
Choose a base branch
from
Open
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
299 changes: 250 additions & 49 deletions tests/test_xiaomi.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import pytest
import zigpy.device
import zigpy.types as t
from zigpy.zcl import foundation
from zigpy.zcl import Cluster, foundation
from zigpy.zcl.clusters.closures import WindowCovering
from zigpy.zcl.clusters.general import (
AnalogInput,
Expand Down Expand Up @@ -1388,15 +1388,12 @@ async def test_xiaomi_power_cluster_not_used(zigpy_device_from_quirk, caplog, qu
)


@pytest.mark.parametrize(
"quirk", (zhaquirks.xiaomi.aqara.roller_curtain_e1.RollerE1AQ,)
)
async def test_xiaomi_e1_roller_curtain_battery(zigpy_device_from_quirk, quirk):
def test_xiaomi_e1_roller_curtain_battery(zigpy_device_from_v2_quirk):
"""Test Aqara E1 roller curtain battery reporting."""
# Ideally, get a real Xiaomi "heartbeat" message to test.
# For now, fake the heartbeat message and check if battery parsing works.

device = zigpy_device_from_quirk(quirk)
device = zigpy_device_from_v2_quirk(LUMI, "lumi.curtain.acn002")

basic_cluster = device.endpoints[1].basic
ClusterListener(basic_cluster)
Expand Down Expand Up @@ -1588,33 +1585,91 @@ async def test_xiaomi_e1_driver_light_level(


@pytest.mark.parametrize(
"command, value",
"command, value, read_current_position",
[
(WindowCovering.ServerCommandDefs.up_open.id, 1),
(WindowCovering.ServerCommandDefs.down_close.id, 0),
(WindowCovering.ServerCommandDefs.stop.id, 2),
(WindowCovering.ServerCommandDefs.up_open.id, 1, False),
(WindowCovering.ServerCommandDefs.down_close.id, 0, False),
(WindowCovering.ServerCommandDefs.stop.id, 2, True),
],
)
async def test_xiaomi_e1_roller_commands_1(zigpy_device_from_quirk, command, value):
async def test_xiaomi_e1_roller_commands_1(
zigpy_device_from_v2_quirk, command, value, read_current_position
):
"""Test Aqara E1 roller commands for basic movement functions using MultistateOutput Cluster."""
device = zigpy_device_from_quirk(
zhaquirks.xiaomi.aqara.roller_curtain_e1.RollerE1AQ
)
device = zigpy_device_from_v2_quirk(LUMI, "lumi.curtain.acn002")

window_covering_cluster = device.endpoints[1].window_covering
window_covering_listener = ClusterListener(window_covering_cluster)
window_covering_attr_id = (
WindowCovering.AttributeDefs.current_position_lift_percentage.id
)

analog_cluster = device.endpoints[1].analog_output
analog_attr_id = AnalogOutput.AttributeDefs.present_value.id

multistate_cluster = device.endpoints[1].multistate_output
multistate_cluster._write_attributes = mock.AsyncMock(
return_value=(
[foundation.WriteAttributesStatusRecord(foundation.Status.SUCCESS)],
)
multistate_attr_id = MultistateOutput.AttributeDefs.present_value.id

# fake read response for attributes: return 1 for all attributes
def mock_read(attributes, manufacturer=None):
records = [
foundation.ReadAttributeRecord(
attr, foundation.Status.SUCCESS, foundation.TypeValue(None, 1)
)
for attr in attributes
]
return (records,)

# patch read commands
patch_window_covering_read = mock.patch.object(
window_covering_cluster,
"_read_attributes",
mock.AsyncMock(side_effect=mock_read),
)
patch_analog_read = mock.patch.object(
analog_cluster, "_read_attributes", mock.AsyncMock(side_effect=mock_read)
)
attr_id = MultistateOutput.AttributeDefs.present_value.id

# test command
await window_covering_cluster.command(command)
assert multistate_cluster._write_attributes.call_count == 1
assert multistate_cluster._write_attributes.call_args[0][0][0].attrid == attr_id
assert multistate_cluster._write_attributes.call_args[0][0][0].value.value == value
# patch write commands
patch_multistate_write = mock.patch.object(
multistate_cluster,
"_write_attributes",
mock.AsyncMock(
return_value=(
[foundation.WriteAttributesStatusRecord(foundation.Status.SUCCESS)],
)
),
)

with (
patch_window_covering_read,
patch_analog_read,
patch_multistate_write,
):
# test command
await window_covering_cluster.command(command)
assert multistate_cluster._write_attributes.call_count == 1
assert (
multistate_cluster._write_attributes.call_args[0][0][0].attrid
== multistate_attr_id
)
assert (
multistate_cluster._write_attributes.call_args[0][0][0].value.value == value
)
if read_current_position:
# confirm the window covering cluster read was redirected
assert len(window_covering_cluster._read_attributes.mock_calls) == 0
# confirm the analog output read occurs
assert len(analog_cluster._read_attributes.mock_calls) == 1
assert analog_cluster._read_attributes.mock_calls[0][1][0] == [
analog_attr_id
]
assert window_covering_listener.attribute_updates[0] == (
window_covering_attr_id,
100 - 1,
) # confirm the position was updated on the ZCL WindowCovering cluster
else:
assert len(analog_cluster._read_attributes.mock_calls) == 0


@pytest.mark.parametrize(
Expand All @@ -1623,27 +1678,173 @@ async def test_xiaomi_e1_roller_commands_1(zigpy_device_from_quirk, command, val
(WindowCovering.ServerCommandDefs.go_to_lift_percentage.id, 60),
],
)
async def test_xiaomi_e1_roller_commands_2(zigpy_device_from_quirk, command, value):
async def test_xiaomi_e1_roller_commands_2(zigpy_device_from_v2_quirk, command, value):
"""Test Aqara E1 roller commands for go to lift percentage using AnalogOutput cluster."""
device = zigpy_device_from_quirk(
zhaquirks.xiaomi.aqara.roller_curtain_e1.RollerE1AQ
)
device = zigpy_device_from_v2_quirk(LUMI, "lumi.curtain.acn002")

window_covering_cluster = device.endpoints[1].window_covering
window_covering_listener = ClusterListener(window_covering_cluster)

analog_cluster = device.endpoints[1].analog_output
analog_cluster._write_attributes = mock.AsyncMock(
return_value=(
[foundation.WriteAttributesStatusRecord(foundation.Status.SUCCESS)],
analog_listener = ClusterListener(analog_cluster)
analog_attr_id = AnalogOutput.AttributeDefs.present_value.id

# patch write commands
patch_analog_write = mock.patch.object(
analog_cluster,
"_write_attributes",
mock.AsyncMock(
return_value=(
[foundation.WriteAttributesStatusRecord(foundation.Status.SUCCESS)],
)
),
)

with (
patch_analog_write,
):
# test go to lift percentage command
await window_covering_cluster.go_to_lift_percentage(value)
assert analog_cluster._write_attributes.call_count == 1
assert (
analog_cluster._write_attributes.call_args[0][0][0].attrid == analog_attr_id
)
assert (
analog_cluster._write_attributes.call_args[0][0][0].value.value
== 100 - value
)
assert (
len(window_covering_listener.attribute_updates) == 0
) # confirm the AnalogOutput write did not update the current WindowCovering position
assert analog_listener.attribute_updates[0] == (
analog_attr_id,
100 - value,
) # confirm the AnalogOutput present_value was updated


@pytest.mark.parametrize(
"attr, expected_value, target_attr, target_cluster",
[
(
WindowCovering.AttributeDefs.current_position_lift_percentage,
99,
AnalogOutput.AttributeDefs.present_value,
AnalogOutput,
), # Redirect with read success
(
WindowCovering.AttributeDefs.current_position_lift_percentage,
None,
AnalogOutput.AttributeDefs.present_value,
AnalogOutput,
), # Redirect with read failure
(
WindowCovering.AttributeDefs.config_status,
1,
None,
None,
), # Regular read success
(
WindowCovering.AttributeDefs.config_status,
None,
None,
None,
), # Regular read failure
],
)
async def test_xiaomi_e1_roller_window_covering_read_redirection(
zigpy_device_from_v2_quirk,
attr: foundation.ZCLAttributeDef,
expected_value: int | None,
target_attr: foundation.ZCLAttributeDef | None,
target_cluster: Cluster | None,
):
"""Test Aqara E1 roller WindowCovering attribute read redirection."""
device = zigpy_device_from_v2_quirk(LUMI, "lumi.curtain.acn002")

window_covering_cluster = device.endpoints[1].window_covering
window_covering_listener = ClusterListener(window_covering_cluster)

redirect = False
if target_attr and target_cluster:
target_cluster = getattr(device.endpoints[1], target_cluster.ep_attribute)
redirect = True

# fake read response for attributes, a value of 1 is returned if expected_value is not None
def mock_read(attributes, manufacturer=None):
records = [
foundation.ReadAttributeRecord(
attr,
foundation.Status.SUCCESS
if expected_value
else foundation.Status.FAILURE,
foundation.TypeValue(None, 1),
)
for attr in attributes
]
return (records,)

# patch window covering read command
patch_window_covering_read = mock.patch.object(
window_covering_cluster,
"_read_attributes",
mock.AsyncMock(side_effect=(mock_read)),
)
attr_id = AnalogOutput.AttributeDefs.present_value.id

# test go to lift percentage command
await window_covering_cluster.go_to_lift_percentage(value)
assert analog_cluster._write_attributes.call_count == 1
assert analog_cluster._write_attributes.call_args[0][0][0].attrid == attr_id
assert (
analog_cluster._write_attributes.call_args[0][0][0].value.value == 100 - value
if redirect:
# patch target cluster read command
patch_target_read = mock.patch.object(
target_cluster,
"_read_attributes",
mock.AsyncMock(side_effect=(mock_read)),
)
with (
patch_window_covering_read,
patch_target_read,
):
# read attribute from WindowCovering cluster using id and name
await window_covering_cluster.read_attributes([attr.id])
await window_covering_cluster.read_attributes([attr.name])

# confirm the reads were redirected to the target cluster
assert len(window_covering_cluster._read_attributes.mock_calls) == 0
assert len(target_cluster._read_attributes.mock_calls) == 2
assert target_cluster._read_attributes.mock_calls[0][1][0] == [
target_attr.id
]
assert target_cluster._read_attributes.mock_calls[1][1][0] == [
target_attr.id
]
else:
with (
patch_window_covering_read,
):
# read attribute from WindowCovering cluster using id and name
await window_covering_cluster.read_attributes([attr.id])
await window_covering_cluster.read_attributes([attr.name])

# confirm the reads occurred normally
assert len(window_covering_cluster._read_attributes.mock_calls) == 2
assert window_covering_cluster._read_attributes.mock_calls[0][1][0] == [
attr.id
]
assert window_covering_cluster._read_attributes.mock_calls[1][1][0] == [
attr.id
]

if not expected_value:
# check read fails do not trigger an attribute update
assert len(window_covering_listener.attribute_updates) == 0
return

# check the WindowCovering attribute was updated by the reads
assert len(window_covering_listener.attribute_updates) == 2
assert window_covering_listener.attribute_updates[0] == (
attr.id,
expected_value,
)
assert window_covering_listener.attribute_updates[1] == (
attr.id,
expected_value,
)


Expand Down Expand Up @@ -1774,11 +1975,11 @@ async def test_aqara_fp1e_sensor(
expected_motion_status,
):
"""Test Aqara FP1E sensor."""
quirk = zigpy_device_from_v2_quirk("aqara", "lumi.sensor_occupy.agl1")
device = zigpy_device_from_v2_quirk("aqara", "lumi.sensor_occupy.agl1")

opple_cluster = quirk.endpoints[1].opple_cluster
ias_cluster = quirk.endpoints[1].ias_zone
occupancy_cluster = quirk.endpoints[1].occupancy
opple_cluster = device.endpoints[1].opple_cluster
ias_cluster = device.endpoints[1].ias_zone
occupancy_cluster = device.endpoints[1].occupancy

opple_listener = ClusterListener(opple_cluster)
ias_listener = ClusterListener(ias_cluster)
Expand Down Expand Up @@ -1809,15 +2010,15 @@ async def test_aqara_fp1e_sensor(
def test_h1_wireless_remotes(zigpy_device_from_v2_quirk):
"""Test Aqara H1 wireless remote quirk adds missing endpoints."""
# create device with endpoint 1 only and verify we don't get a KeyError
quirk = zigpy_device_from_v2_quirk(LUMI, "lumi.remote.b28ac1")
device = zigpy_device_from_v2_quirk(LUMI, "lumi.remote.b28ac1")

# verify the quirk adds endpoints 2 and 3
assert 2 in quirk.endpoints
assert 3 in quirk.endpoints
assert 2 in device.endpoints
assert 3 in device.endpoints

# verify the quirk adds the correct clusters to the new endpoints
assert OnOff.cluster_id in quirk.endpoints[2].out_clusters
assert OnOff.cluster_id in quirk.endpoints[3].out_clusters
assert OnOff.cluster_id in device.endpoints[2].out_clusters
assert OnOff.cluster_id in device.endpoints[3].out_clusters

assert MultistateInput.cluster_id in quirk.endpoints[2].in_clusters
assert MultistateInput.cluster_id in quirk.endpoints[3].in_clusters
assert MultistateInput.cluster_id in device.endpoints[2].in_clusters
assert MultistateInput.cluster_id in device.endpoints[3].in_clusters
Loading
Loading