Add support for ZHA entities exposed by Zigpy quirks (#111176)

* Add counter entities to the ZHA coordinator device

* rework to prepare for non coordinator device counters

* Initial scaffolding to support quirks v2 entities

* update for zigpy changes

* add assertion error message

* clean up test

* update group entity discovery kwargs

* constants and clearer names

* apply custom device configuration

* quirks switches

* quirks select entities

* quirks sensor entities

* update discovery

* move call to super

* add complex quirks v2 discovery test

* remove duplicate replaces

* add quirks v2 button entity support

* add quirks v2 binary sensor entity support

* fix exception in counter entitiy discovery

* oops

* update formatting

* support custom on and off values

* logging

* don't filter out entities quirks says should be created

* fix type alias warnings

* sync up with zigpy changes and additions

* add a binary sensor test

* button coverage

* switch coverage

* initial select coverage

* number coverage

* sensor coverage

* update discovery after rebase

* coverage

* single line

* line lengths

* fix double underscore

* review comments

* set category from quirks in base entity

* line lengths

* move comment

* imports

* simplify

* simplify
This commit is contained in:
David F. Mulcahey 2024-02-29 10:38:21 -05:00 committed by GitHub
parent f44b759a99
commit 73b6e2bac8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 1340 additions and 54 deletions

View file

@ -11,7 +11,8 @@ from zhaquirks.const import (
)
from zigpy.exceptions import ZigbeeException
import zigpy.profiles.zha as zha
from zigpy.quirks import CustomCluster, CustomDevice
from zigpy.quirks import _DEVICE_REGISTRY, CustomCluster, CustomDevice
from zigpy.quirks.v2 import CustomDeviceV2, add_to_registry_v2
import zigpy.types as t
import zigpy.zcl.clusters.closures as closures
import zigpy.zcl.clusters.general as general
@ -564,6 +565,272 @@ async def test_switch_configurable(
await async_test_rejoin(hass, zigpy_device_tuya, [cluster], (0,))
async def test_switch_configurable_custom_on_off_values(
hass: HomeAssistant, zha_device_joined_restored, zigpy_device_mock
) -> None:
"""Test ZHA configurable switch platform."""
zigpy_device = zigpy_device_mock(
{
1: {
SIG_EP_INPUT: [general.Basic.cluster_id],
SIG_EP_OUTPUT: [],
SIG_EP_TYPE: zha.DeviceType.ON_OFF_SWITCH,
}
},
manufacturer="manufacturer",
model="model",
)
(
add_to_registry_v2(zigpy_device.manufacturer, zigpy_device.model)
.adds(WindowDetectionFunctionQuirk.TuyaManufCluster)
.switch(
"window_detection_function",
WindowDetectionFunctionQuirk.TuyaManufCluster.cluster_id,
on_value=3,
off_value=5,
)
)
zigpy_device = _DEVICE_REGISTRY.get_device(zigpy_device)
assert isinstance(zigpy_device, CustomDeviceV2)
cluster = zigpy_device.endpoints[1].tuya_manufacturer
cluster.PLUGGED_ATTR_READS = {"window_detection_function": 5}
update_attribute_cache(cluster)
zha_device = await zha_device_joined_restored(zigpy_device)
entity_id = find_entity_id(Platform.SWITCH, zha_device, hass)
assert entity_id is not None
assert hass.states.get(entity_id).state == STATE_OFF
await async_enable_traffic(hass, [zha_device], enabled=False)
# test that the switch was created and that its state is unavailable
assert hass.states.get(entity_id).state == STATE_UNAVAILABLE
# allow traffic to flow through the gateway and device
await async_enable_traffic(hass, [zha_device])
# test that the state has changed from unavailable to off
assert hass.states.get(entity_id).state == STATE_OFF
# turn on at switch
await send_attributes_report(hass, cluster, {"window_detection_function": 3})
assert hass.states.get(entity_id).state == STATE_ON
# turn off at switch
await send_attributes_report(hass, cluster, {"window_detection_function": 5})
assert hass.states.get(entity_id).state == STATE_OFF
# turn on from HA
with patch(
"zigpy.zcl.Cluster.write_attributes",
return_value=[zcl_f.WriteAttributesResponse.deserialize(b"\x00")[0]],
):
# turn on via UI
await hass.services.async_call(
SWITCH_DOMAIN, "turn_on", {"entity_id": entity_id}, blocking=True
)
assert cluster.write_attributes.mock_calls == [
call({"window_detection_function": 3}, manufacturer=None)
]
cluster.write_attributes.reset_mock()
# turn off from HA
with patch(
"zigpy.zcl.Cluster.write_attributes",
return_value=[zcl_f.WriteAttributesResponse.deserialize(b"\x00")[0]],
):
# turn off via UI
await hass.services.async_call(
SWITCH_DOMAIN, "turn_off", {"entity_id": entity_id}, blocking=True
)
assert cluster.write_attributes.mock_calls == [
call({"window_detection_function": 5}, manufacturer=None)
]
async def test_switch_configurable_custom_on_off_values_force_inverted(
hass: HomeAssistant, zha_device_joined_restored, zigpy_device_mock
) -> None:
"""Test ZHA configurable switch platform."""
zigpy_device = zigpy_device_mock(
{
1: {
SIG_EP_INPUT: [general.Basic.cluster_id],
SIG_EP_OUTPUT: [],
SIG_EP_TYPE: zha.DeviceType.ON_OFF_SWITCH,
}
},
manufacturer="manufacturer2",
model="model2",
)
(
add_to_registry_v2(zigpy_device.manufacturer, zigpy_device.model)
.adds(WindowDetectionFunctionQuirk.TuyaManufCluster)
.switch(
"window_detection_function",
WindowDetectionFunctionQuirk.TuyaManufCluster.cluster_id,
on_value=3,
off_value=5,
force_inverted=True,
)
)
zigpy_device = _DEVICE_REGISTRY.get_device(zigpy_device)
assert isinstance(zigpy_device, CustomDeviceV2)
cluster = zigpy_device.endpoints[1].tuya_manufacturer
cluster.PLUGGED_ATTR_READS = {"window_detection_function": 5}
update_attribute_cache(cluster)
zha_device = await zha_device_joined_restored(zigpy_device)
entity_id = find_entity_id(Platform.SWITCH, zha_device, hass)
assert entity_id is not None
assert hass.states.get(entity_id).state == STATE_ON
await async_enable_traffic(hass, [zha_device], enabled=False)
# test that the switch was created and that its state is unavailable
assert hass.states.get(entity_id).state == STATE_UNAVAILABLE
# allow traffic to flow through the gateway and device
await async_enable_traffic(hass, [zha_device])
# test that the state has changed from unavailable to off
assert hass.states.get(entity_id).state == STATE_ON
# turn on at switch
await send_attributes_report(hass, cluster, {"window_detection_function": 3})
assert hass.states.get(entity_id).state == STATE_OFF
# turn off at switch
await send_attributes_report(hass, cluster, {"window_detection_function": 5})
assert hass.states.get(entity_id).state == STATE_ON
# turn on from HA
with patch(
"zigpy.zcl.Cluster.write_attributes",
return_value=[zcl_f.WriteAttributesResponse.deserialize(b"\x00")[0]],
):
# turn on via UI
await hass.services.async_call(
SWITCH_DOMAIN, "turn_on", {"entity_id": entity_id}, blocking=True
)
assert cluster.write_attributes.mock_calls == [
call({"window_detection_function": 5}, manufacturer=None)
]
cluster.write_attributes.reset_mock()
# turn off from HA
with patch(
"zigpy.zcl.Cluster.write_attributes",
return_value=[zcl_f.WriteAttributesResponse.deserialize(b"\x00")[0]],
):
# turn off via UI
await hass.services.async_call(
SWITCH_DOMAIN, "turn_off", {"entity_id": entity_id}, blocking=True
)
assert cluster.write_attributes.mock_calls == [
call({"window_detection_function": 3}, manufacturer=None)
]
async def test_switch_configurable_custom_on_off_values_inverter_attribute(
hass: HomeAssistant, zha_device_joined_restored, zigpy_device_mock
) -> None:
"""Test ZHA configurable switch platform."""
zigpy_device = zigpy_device_mock(
{
1: {
SIG_EP_INPUT: [general.Basic.cluster_id],
SIG_EP_OUTPUT: [],
SIG_EP_TYPE: zha.DeviceType.ON_OFF_SWITCH,
}
},
manufacturer="manufacturer3",
model="model3",
)
(
add_to_registry_v2(zigpy_device.manufacturer, zigpy_device.model)
.adds(WindowDetectionFunctionQuirk.TuyaManufCluster)
.switch(
"window_detection_function",
WindowDetectionFunctionQuirk.TuyaManufCluster.cluster_id,
on_value=3,
off_value=5,
invert_attribute_name="window_detection_function_inverter",
)
)
zigpy_device = _DEVICE_REGISTRY.get_device(zigpy_device)
assert isinstance(zigpy_device, CustomDeviceV2)
cluster = zigpy_device.endpoints[1].tuya_manufacturer
cluster.PLUGGED_ATTR_READS = {
"window_detection_function": 5,
"window_detection_function_inverter": t.Bool(True),
}
update_attribute_cache(cluster)
zha_device = await zha_device_joined_restored(zigpy_device)
entity_id = find_entity_id(Platform.SWITCH, zha_device, hass)
assert entity_id is not None
assert hass.states.get(entity_id).state == STATE_ON
await async_enable_traffic(hass, [zha_device], enabled=False)
# test that the switch was created and that its state is unavailable
assert hass.states.get(entity_id).state == STATE_UNAVAILABLE
# allow traffic to flow through the gateway and device
await async_enable_traffic(hass, [zha_device])
# test that the state has changed from unavailable to off
assert hass.states.get(entity_id).state == STATE_ON
# turn on at switch
await send_attributes_report(hass, cluster, {"window_detection_function": 3})
assert hass.states.get(entity_id).state == STATE_OFF
# turn off at switch
await send_attributes_report(hass, cluster, {"window_detection_function": 5})
assert hass.states.get(entity_id).state == STATE_ON
# turn on from HA
with patch(
"zigpy.zcl.Cluster.write_attributes",
return_value=[zcl_f.WriteAttributesResponse.deserialize(b"\x00")[0]],
):
# turn on via UI
await hass.services.async_call(
SWITCH_DOMAIN, "turn_on", {"entity_id": entity_id}, blocking=True
)
assert cluster.write_attributes.mock_calls == [
call({"window_detection_function": 5}, manufacturer=None)
]
cluster.write_attributes.reset_mock()
# turn off from HA
with patch(
"zigpy.zcl.Cluster.write_attributes",
return_value=[zcl_f.WriteAttributesResponse.deserialize(b"\x00")[0]],
):
# turn off via UI
await hass.services.async_call(
SWITCH_DOMAIN, "turn_off", {"entity_id": entity_id}, blocking=True
)
assert cluster.write_attributes.mock_calls == [
call({"window_detection_function": 3}, manufacturer=None)
]
WCAttrs = closures.WindowCovering.AttributeDefs
WCT = closures.WindowCovering.WindowCoveringType
WCCS = closures.WindowCovering.ConfigStatus