Refactor ZHA HVAC thermostat channel (#56238)

* Refactor HVAC channel to use zigpy cached attributes

* Allow named attributes in ZHA test attribute reports

* Let attribute write to update cache

* WIP Update tests

* Cleanup
This commit is contained in:
Alexei Chetroi
2021-09-17 15:17:34 -04:00
committed by GitHub
parent 16832bc35b
commit 5b0e00a74b
4 changed files with 136 additions and 162 deletions
+8 -3
View File
@@ -3,6 +3,7 @@ import asyncio
import math
from unittest.mock import AsyncMock, Mock
import zigpy.zcl
import zigpy.zcl.foundation as zcl_f
import homeassistant.components.zha.core.const as zha_const
@@ -47,7 +48,8 @@ def patch_cluster(cluster):
cluster.read_attributes = AsyncMock(wraps=cluster.read_attributes)
cluster.read_attributes_raw = AsyncMock(side_effect=_read_attribute_raw)
cluster.unbind = AsyncMock(return_value=[0])
cluster.write_attributes = AsyncMock(
cluster.write_attributes = AsyncMock(wraps=cluster.write_attributes)
cluster._write_attributes = AsyncMock(
return_value=[zcl_f.WriteAttributesResponse.deserialize(b"\x00")[0]]
)
if cluster.cluster_id == 4:
@@ -76,13 +78,16 @@ def send_attribute_report(hass, cluster, attrid, value):
return send_attributes_report(hass, cluster, {attrid: value})
async def send_attributes_report(hass, cluster: int, attributes: dict):
async def send_attributes_report(hass, cluster: zigpy.zcl.Cluster, attributes: dict):
"""Cause the sensor to receive an attribute report from the network.
This is to simulate the normal device communication that happens when a
device is paired to the zigbee network.
"""
attrs = [make_attribute(attrid, value) for attrid, value in attributes.items()]
attrs = [
make_attribute(cluster.attridx.get(attr, attr), value)
for attr, value in attributes.items()
]
hdr = make_zcl_header(zcl_f.Command.Report_Attributes)
hdr.frame_control.disable_default_response = True
cluster.handle_message(hdr, [attrs])
+14 -5
View File
@@ -1,4 +1,5 @@
"""Test configuration for the ZHA component."""
import itertools
import time
from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch
@@ -116,6 +117,7 @@ def zigpy_device_mock(zigpy_app_controller):
node_descriptor=b"\x02@\x807\x10\x7fd\x00\x00*d\x00\x00",
nwk=0xB79C,
patch_cluster=True,
quirk=None,
):
"""Make a fake device using the specified cluster classes."""
device = zigpy.device.Device(
@@ -133,13 +135,20 @@ def zigpy_device_mock(zigpy_app_controller):
endpoint.request = AsyncMock(return_value=[0])
for cluster_id in ep.get(SIG_EP_INPUT, []):
cluster = endpoint.add_input_cluster(cluster_id)
if patch_cluster:
common.patch_cluster(cluster)
endpoint.add_input_cluster(cluster_id)
for cluster_id in ep.get(SIG_EP_OUTPUT, []):
cluster = endpoint.add_output_cluster(cluster_id)
if patch_cluster:
endpoint.add_output_cluster(cluster_id)
if quirk:
device = quirk(zigpy_app_controller, device.ieee, device.nwk, device)
if patch_cluster:
for endpoint in (ep for epid, ep in device.endpoints.items() if epid):
endpoint.request = AsyncMock(return_value=[0])
for cluster in itertools.chain(
endpoint.in_clusters.values(), endpoint.out_clusters.values()
):
common.patch_cluster(cluster)
return device
+86 -96
View File
@@ -3,6 +3,8 @@
from unittest.mock import patch
import pytest
import zhaquirks.sinope.thermostat
import zhaquirks.tuya.valve
import zigpy.profiles
import zigpy.zcl.clusters
from zigpy.zcl.clusters.hvac import Thermostat
@@ -96,6 +98,12 @@ CLIMATE_SINOPE = {
],
SIG_EP_OUTPUT: [zigpy.zcl.clusters.general.Ota.cluster_id, 65281],
},
196: {
SIG_EP_PROFILE: 0xC25D,
SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.THERMOSTAT,
SIG_EP_INPUT: [zigpy.zcl.clusters.general.PowerConfiguration.cluster_id],
SIG_EP_OUTPUT: [],
},
}
CLIMATE_ZEN = {
@@ -159,13 +167,13 @@ ZCL_ATTR_PLUG = {
def device_climate_mock(hass, zigpy_device_mock, zha_device_joined):
"""Test regular thermostat device."""
async def _dev(clusters, plug=None, manuf=None):
async def _dev(clusters, plug=None, manuf=None, quirk=None):
if plug is None:
plugged_attrs = ZCL_ATTR_PLUG
else:
plugged_attrs = {**ZCL_ATTR_PLUG, **plug}
zigpy_device = zigpy_device_mock(clusters, manufacturer=manuf)
zigpy_device = zigpy_device_mock(clusters, manufacturer=manuf, quirk=quirk)
zigpy_device.endpoints[1].thermostat.PLUGGED_ATTR_READS = plugged_attrs
zha_device = await zha_device_joined(zigpy_device)
await async_enable_traffic(hass, [zha_device])
@@ -198,7 +206,11 @@ async def device_climate_fan(device_climate_mock):
async def device_climate_sinope(device_climate_mock):
"""Sinope thermostat."""
return await device_climate_mock(CLIMATE_SINOPE, manuf=MANUF_SINOPE)
return await device_climate_mock(
CLIMATE_SINOPE,
manuf=MANUF_SINOPE,
quirk=zhaquirks.sinope.thermostat.SinopeTechnologiesThermostat,
)
@pytest.fixture
@@ -212,7 +224,9 @@ async def device_climate_zen(device_climate_mock):
async def device_climate_moes(device_climate_mock):
"""MOES thermostat."""
return await device_climate_mock(CLIMATE_MOES, manuf=MANUF_MOES)
return await device_climate_mock(
CLIMATE_MOES, manuf=MANUF_MOES, quirk=zhaquirks.tuya.valve.MoesHY368_Type1
)
def test_sequence_mappings():
@@ -456,22 +470,18 @@ async def test_target_temperature(
):
"""Test target temperature property."""
with patch.object(
zigpy.zcl.clusters.manufacturer_specific.ManufacturerSpecificCluster,
"ep_attribute",
"sinope_manufacturer_specific",
):
device_climate = await device_climate_mock(
CLIMATE_SINOPE,
{
"occupied_cooling_setpoint": 2500,
"occupied_heating_setpoint": 2200,
"system_mode": sys_mode,
"unoccupied_heating_setpoint": 1600,
"unoccupied_cooling_setpoint": 2700,
},
manuf=MANUF_SINOPE,
)
device_climate = await device_climate_mock(
CLIMATE_SINOPE,
{
"occupied_cooling_setpoint": 2500,
"occupied_heating_setpoint": 2200,
"system_mode": sys_mode,
"unoccupied_heating_setpoint": 1600,
"unoccupied_cooling_setpoint": 2700,
},
manuf=MANUF_SINOPE,
quirk=zhaquirks.sinope.thermostat.SinopeTechnologiesThermostat,
)
entity_id = await find_entity_id(DOMAIN, device_climate, hass)
if preset:
await hass.services.async_call(
@@ -498,20 +508,16 @@ async def test_target_temperature_high(
):
"""Test target temperature high property."""
with patch.object(
zigpy.zcl.clusters.manufacturer_specific.ManufacturerSpecificCluster,
"ep_attribute",
"sinope_manufacturer_specific",
):
device_climate = await device_climate_mock(
CLIMATE_SINOPE,
{
"occupied_cooling_setpoint": 1700,
"system_mode": Thermostat.SystemMode.Auto,
"unoccupied_cooling_setpoint": unoccupied,
},
manuf=MANUF_SINOPE,
)
device_climate = await device_climate_mock(
CLIMATE_SINOPE,
{
"occupied_cooling_setpoint": 1700,
"system_mode": Thermostat.SystemMode.Auto,
"unoccupied_cooling_setpoint": unoccupied,
},
manuf=MANUF_SINOPE,
quirk=zhaquirks.sinope.thermostat.SinopeTechnologiesThermostat,
)
entity_id = await find_entity_id(DOMAIN, device_climate, hass)
if preset:
await hass.services.async_call(
@@ -538,20 +544,16 @@ async def test_target_temperature_low(
):
"""Test target temperature low property."""
with patch.object(
zigpy.zcl.clusters.manufacturer_specific.ManufacturerSpecificCluster,
"ep_attribute",
"sinope_manufacturer_specific",
):
device_climate = await device_climate_mock(
CLIMATE_SINOPE,
{
"occupied_heating_setpoint": 2100,
"system_mode": Thermostat.SystemMode.Auto,
"unoccupied_heating_setpoint": unoccupied,
},
manuf=MANUF_SINOPE,
)
device_climate = await device_climate_mock(
CLIMATE_SINOPE,
{
"occupied_heating_setpoint": 2100,
"system_mode": Thermostat.SystemMode.Auto,
"unoccupied_heating_setpoint": unoccupied,
},
manuf=MANUF_SINOPE,
quirk=zhaquirks.sinope.thermostat.SinopeTechnologiesThermostat,
)
entity_id = await find_entity_id(DOMAIN, device_climate, hass)
if preset:
await hass.services.async_call(
@@ -748,22 +750,18 @@ async def test_set_temperature_hvac_mode(hass, device_climate):
async def test_set_temperature_heat_cool(hass, device_climate_mock):
"""Test setting temperature service call in heating/cooling HVAC mode."""
with patch.object(
zigpy.zcl.clusters.manufacturer_specific.ManufacturerSpecificCluster,
"ep_attribute",
"sinope_manufacturer_specific",
):
device_climate = await device_climate_mock(
CLIMATE_SINOPE,
{
"occupied_cooling_setpoint": 2500,
"occupied_heating_setpoint": 2000,
"system_mode": Thermostat.SystemMode.Auto,
"unoccupied_heating_setpoint": 1600,
"unoccupied_cooling_setpoint": 2700,
},
manuf=MANUF_SINOPE,
)
device_climate = await device_climate_mock(
CLIMATE_SINOPE,
{
"occupied_cooling_setpoint": 2500,
"occupied_heating_setpoint": 2000,
"system_mode": Thermostat.SystemMode.Auto,
"unoccupied_heating_setpoint": 1600,
"unoccupied_cooling_setpoint": 2700,
},
manuf=MANUF_SINOPE,
quirk=zhaquirks.sinope.thermostat.SinopeTechnologiesThermostat,
)
entity_id = await find_entity_id(DOMAIN, device_climate, hass)
thrm_cluster = device_climate.device.endpoints[1].thermostat
@@ -838,22 +836,18 @@ async def test_set_temperature_heat_cool(hass, device_climate_mock):
async def test_set_temperature_heat(hass, device_climate_mock):
"""Test setting temperature service call in heating HVAC mode."""
with patch.object(
zigpy.zcl.clusters.manufacturer_specific.ManufacturerSpecificCluster,
"ep_attribute",
"sinope_manufacturer_specific",
):
device_climate = await device_climate_mock(
CLIMATE_SINOPE,
{
"occupied_cooling_setpoint": 2500,
"occupied_heating_setpoint": 2000,
"system_mode": Thermostat.SystemMode.Heat,
"unoccupied_heating_setpoint": 1600,
"unoccupied_cooling_setpoint": 2700,
},
manuf=MANUF_SINOPE,
)
device_climate = await device_climate_mock(
CLIMATE_SINOPE,
{
"occupied_cooling_setpoint": 2500,
"occupied_heating_setpoint": 2000,
"system_mode": Thermostat.SystemMode.Heat,
"unoccupied_heating_setpoint": 1600,
"unoccupied_cooling_setpoint": 2700,
},
manuf=MANUF_SINOPE,
quirk=zhaquirks.sinope.thermostat.SinopeTechnologiesThermostat,
)
entity_id = await find_entity_id(DOMAIN, device_climate, hass)
thrm_cluster = device_climate.device.endpoints[1].thermostat
@@ -921,22 +915,18 @@ async def test_set_temperature_heat(hass, device_climate_mock):
async def test_set_temperature_cool(hass, device_climate_mock):
"""Test setting temperature service call in cooling HVAC mode."""
with patch.object(
zigpy.zcl.clusters.manufacturer_specific.ManufacturerSpecificCluster,
"ep_attribute",
"sinope_manufacturer_specific",
):
device_climate = await device_climate_mock(
CLIMATE_SINOPE,
{
"occupied_cooling_setpoint": 2500,
"occupied_heating_setpoint": 2000,
"system_mode": Thermostat.SystemMode.Cool,
"unoccupied_cooling_setpoint": 1600,
"unoccupied_heating_setpoint": 2700,
},
manuf=MANUF_SINOPE,
)
device_climate = await device_climate_mock(
CLIMATE_SINOPE,
{
"occupied_cooling_setpoint": 2500,
"occupied_heating_setpoint": 2000,
"system_mode": Thermostat.SystemMode.Cool,
"unoccupied_cooling_setpoint": 1600,
"unoccupied_heating_setpoint": 2700,
},
manuf=MANUF_SINOPE,
quirk=zhaquirks.sinope.thermostat.SinopeTechnologiesThermostat,
)
entity_id = await find_entity_id(DOMAIN, device_climate, hass)
thrm_cluster = device_climate.device.endpoints[1].thermostat