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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 136 additions and 162 deletions

View File

@ -125,134 +125,118 @@ class ThermostatChannel(ZigbeeChannel):
"unoccupied_heating_setpoint": False, "unoccupied_heating_setpoint": False,
"unoccupied_cooling_setpoint": False, "unoccupied_cooling_setpoint": False,
} }
self._abs_max_cool_setpoint_limit = 3200 # 32C
self._abs_min_cool_setpoint_limit = 1600 # 16C
self._ctrl_seqe_of_oper = 0xFF
self._abs_max_heat_setpoint_limit = 3000 # 30C
self._abs_min_heat_setpoint_limit = 700 # 7C
self._running_mode = None
self._max_cool_setpoint_limit = None
self._max_heat_setpoint_limit = None
self._min_cool_setpoint_limit = None
self._min_heat_setpoint_limit = None
self._local_temp = None
self._occupancy = None
self._occupied_cooling_setpoint = None
self._occupied_heating_setpoint = None
self._pi_cooling_demand = None
self._pi_heating_demand = None
self._running_state = None
self._system_mode = None
self._unoccupied_cooling_setpoint = None
self._unoccupied_heating_setpoint = None
@property @property
def abs_max_cool_setpoint_limit(self) -> int: def abs_max_cool_setpoint_limit(self) -> int:
"""Absolute maximum cooling setpoint.""" """Absolute maximum cooling setpoint."""
return self._abs_max_cool_setpoint_limit return self.cluster.get("abs_max_cool_setpoint_limit", 3200)
@property @property
def abs_min_cool_setpoint_limit(self) -> int: def abs_min_cool_setpoint_limit(self) -> int:
"""Absolute minimum cooling setpoint.""" """Absolute minimum cooling setpoint."""
return self._abs_min_cool_setpoint_limit return self.cluster.get("abs_min_cool_setpoint_limit", 1600)
@property @property
def abs_max_heat_setpoint_limit(self) -> int: def abs_max_heat_setpoint_limit(self) -> int:
"""Absolute maximum heating setpoint.""" """Absolute maximum heating setpoint."""
return self._abs_max_heat_setpoint_limit return self.cluster.get("abs_max_heat_setpoint_limit", 3000)
@property @property
def abs_min_heat_setpoint_limit(self) -> int: def abs_min_heat_setpoint_limit(self) -> int:
"""Absolute minimum heating setpoint.""" """Absolute minimum heating setpoint."""
return self._abs_min_heat_setpoint_limit return self.cluster.get("abs_min_heat_setpoint_limit", 700)
@property @property
def ctrl_seqe_of_oper(self) -> int: def ctrl_seqe_of_oper(self) -> int:
"""Control Sequence of operations attribute.""" """Control Sequence of operations attribute."""
return self._ctrl_seqe_of_oper return self.cluster.get("ctrl_seqe_of_oper", 0xFF)
@property @property
def max_cool_setpoint_limit(self) -> int: def max_cool_setpoint_limit(self) -> int:
"""Maximum cooling setpoint.""" """Maximum cooling setpoint."""
if self._max_cool_setpoint_limit is None: sp_limit = self.cluster.get("max_cool_setpoint_limit")
if sp_limit is None:
return self.abs_max_cool_setpoint_limit return self.abs_max_cool_setpoint_limit
return self._max_cool_setpoint_limit return sp_limit
@property @property
def min_cool_setpoint_limit(self) -> int: def min_cool_setpoint_limit(self) -> int:
"""Minimum cooling setpoint.""" """Minimum cooling setpoint."""
if self._min_cool_setpoint_limit is None: sp_limit = self.cluster.get("min_cool_setpoint_limit")
if sp_limit is None:
return self.abs_min_cool_setpoint_limit return self.abs_min_cool_setpoint_limit
return self._min_cool_setpoint_limit return sp_limit
@property @property
def max_heat_setpoint_limit(self) -> int: def max_heat_setpoint_limit(self) -> int:
"""Maximum heating setpoint.""" """Maximum heating setpoint."""
if self._max_heat_setpoint_limit is None: sp_limit = self.cluster.get("max_heat_setpoint_limit")
if sp_limit is None:
return self.abs_max_heat_setpoint_limit return self.abs_max_heat_setpoint_limit
return self._max_heat_setpoint_limit return sp_limit
@property @property
def min_heat_setpoint_limit(self) -> int: def min_heat_setpoint_limit(self) -> int:
"""Minimum heating setpoint.""" """Minimum heating setpoint."""
if self._min_heat_setpoint_limit is None: sp_limit = self.cluster.get("min_heat_setpoint_limit")
if sp_limit is None:
return self.abs_min_heat_setpoint_limit return self.abs_min_heat_setpoint_limit
return self._min_heat_setpoint_limit return sp_limit
@property @property
def local_temp(self) -> int | None: def local_temp(self) -> int | None:
"""Thermostat temperature.""" """Thermostat temperature."""
return self._local_temp return self.cluster.get("local_temp")
@property @property
def occupancy(self) -> int | None: def occupancy(self) -> int | None:
"""Is occupancy detected.""" """Is occupancy detected."""
return self._occupancy return self.cluster.get("occupancy")
@property @property
def occupied_cooling_setpoint(self) -> int | None: def occupied_cooling_setpoint(self) -> int | None:
"""Temperature when room is occupied.""" """Temperature when room is occupied."""
return self._occupied_cooling_setpoint return self.cluster.get("occupied_cooling_setpoint")
@property @property
def occupied_heating_setpoint(self) -> int | None: def occupied_heating_setpoint(self) -> int | None:
"""Temperature when room is occupied.""" """Temperature when room is occupied."""
return self._occupied_heating_setpoint return self.cluster.get("occupied_heating_setpoint")
@property @property
def pi_cooling_demand(self) -> int: def pi_cooling_demand(self) -> int:
"""Cooling demand.""" """Cooling demand."""
return self._pi_cooling_demand return self.cluster.get("pi_cooling_demand")
@property @property
def pi_heating_demand(self) -> int: def pi_heating_demand(self) -> int:
"""Heating demand.""" """Heating demand."""
return self._pi_heating_demand return self.cluster.get("pi_heating_demand")
@property @property
def running_mode(self) -> int | None: def running_mode(self) -> int | None:
"""Thermostat running mode.""" """Thermostat running mode."""
return self._running_mode return self.cluster.get("running_mode")
@property @property
def running_state(self) -> int | None: def running_state(self) -> int | None:
"""Thermostat running state, state of heat, cool, fan relays.""" """Thermostat running state, state of heat, cool, fan relays."""
return self._running_state return self.cluster.get("running_state")
@property @property
def system_mode(self) -> int | None: def system_mode(self) -> int | None:
"""System mode.""" """System mode."""
return self._system_mode return self.cluster.get("system_mode")
@property @property
def unoccupied_cooling_setpoint(self) -> int | None: def unoccupied_cooling_setpoint(self) -> int | None:
"""Temperature when room is not occupied.""" """Temperature when room is not occupied."""
return self._unoccupied_cooling_setpoint return self.cluster.get("unoccupied_cooling_setpoint")
@property @property
def unoccupied_heating_setpoint(self) -> int | None: def unoccupied_heating_setpoint(self) -> int | None:
"""Temperature when room is not occupied.""" """Temperature when room is not occupied."""
return self._unoccupied_heating_setpoint return self.cluster.get("unoccupied_heating_setpoint")
@callback @callback
def attribute_updated(self, attrid, value): def attribute_updated(self, attrid, value):
@ -261,7 +245,6 @@ class ThermostatChannel(ZigbeeChannel):
self.debug( self.debug(
"Attribute report '%s'[%s] = %s", self.cluster.name, attr_name, value "Attribute report '%s'[%s] = %s", self.cluster.name, attr_name, value
) )
setattr(self, f"_{attr_name}", value)
self.async_send_signal( self.async_send_signal(
f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}",
AttributeUpdateRecord(attrid, attr_name, value), AttributeUpdateRecord(attrid, attr_name, value),
@ -276,8 +259,6 @@ class ThermostatChannel(ZigbeeChannel):
self._init_attrs.pop(attr, None) self._init_attrs.pop(attr, None)
if attr in fail: if attr in fail:
continue continue
if isinstance(attr, str):
setattr(self, f"_{attr}", res[attr])
self.async_send_signal( self.async_send_signal(
f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}",
AttributeUpdateRecord(None, attr, res[attr]), AttributeUpdateRecord(None, attr, res[attr]),
@ -301,7 +282,6 @@ class ThermostatChannel(ZigbeeChannel):
self.debug("couldn't set '%s' operation mode", mode) self.debug("couldn't set '%s' operation mode", mode)
return False return False
self._system_mode = mode
self.debug("set system to %s", mode) self.debug("set system to %s", mode)
return True return True
@ -317,11 +297,6 @@ class ThermostatChannel(ZigbeeChannel):
self.debug("couldn't set heating setpoint") self.debug("couldn't set heating setpoint")
return False return False
if is_away:
self._unoccupied_heating_setpoint = temperature
else:
self._occupied_heating_setpoint = temperature
self.debug("set heating setpoint to %s", temperature)
return True return True
async def async_set_cooling_setpoint( async def async_set_cooling_setpoint(
@ -335,10 +310,6 @@ class ThermostatChannel(ZigbeeChannel):
if not await self.write_attributes(data): if not await self.write_attributes(data):
self.debug("couldn't set cooling setpoint") self.debug("couldn't set cooling setpoint")
return False return False
if is_away:
self._unoccupied_cooling_setpoint = temperature
else:
self._occupied_cooling_setpoint = temperature
self.debug("set cooling setpoint to %s", temperature) self.debug("set cooling setpoint to %s", temperature)
return True return True
@ -349,7 +320,6 @@ class ThermostatChannel(ZigbeeChannel):
self.debug("read 'occupancy' attr, success: %s, fail: %s", res, fail) self.debug("read 'occupancy' attr, success: %s, fail: %s", res, fail)
if "occupancy" not in res: if "occupancy" not in res:
return None return None
self._occupancy = res["occupancy"]
return bool(self.occupancy) return bool(self.occupancy)
except ZigbeeException as ex: except ZigbeeException as ex:
self.debug("Couldn't read 'occupancy' attribute: %s", ex) self.debug("Couldn't read 'occupancy' attribute: %s", ex)

View File

@ -3,6 +3,7 @@ import asyncio
import math import math
from unittest.mock import AsyncMock, Mock from unittest.mock import AsyncMock, Mock
import zigpy.zcl
import zigpy.zcl.foundation as zcl_f import zigpy.zcl.foundation as zcl_f
import homeassistant.components.zha.core.const as zha_const 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 = AsyncMock(wraps=cluster.read_attributes)
cluster.read_attributes_raw = AsyncMock(side_effect=_read_attribute_raw) cluster.read_attributes_raw = AsyncMock(side_effect=_read_attribute_raw)
cluster.unbind = AsyncMock(return_value=[0]) 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]] return_value=[zcl_f.WriteAttributesResponse.deserialize(b"\x00")[0]]
) )
if cluster.cluster_id == 4: 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}) 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. """Cause the sensor to receive an attribute report from the network.
This is to simulate the normal device communication that happens when a This is to simulate the normal device communication that happens when a
device is paired to the zigbee network. 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 = make_zcl_header(zcl_f.Command.Report_Attributes)
hdr.frame_control.disable_default_response = True hdr.frame_control.disable_default_response = True
cluster.handle_message(hdr, [attrs]) cluster.handle_message(hdr, [attrs])

View File

@ -1,4 +1,5 @@
"""Test configuration for the ZHA component.""" """Test configuration for the ZHA component."""
import itertools
import time import time
from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch 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", node_descriptor=b"\x02@\x807\x10\x7fd\x00\x00*d\x00\x00",
nwk=0xB79C, nwk=0xB79C,
patch_cluster=True, patch_cluster=True,
quirk=None,
): ):
"""Make a fake device using the specified cluster classes.""" """Make a fake device using the specified cluster classes."""
device = zigpy.device.Device( device = zigpy.device.Device(
@ -133,13 +135,20 @@ def zigpy_device_mock(zigpy_app_controller):
endpoint.request = AsyncMock(return_value=[0]) endpoint.request = AsyncMock(return_value=[0])
for cluster_id in ep.get(SIG_EP_INPUT, []): for cluster_id in ep.get(SIG_EP_INPUT, []):
cluster = endpoint.add_input_cluster(cluster_id) endpoint.add_input_cluster(cluster_id)
if patch_cluster:
common.patch_cluster(cluster)
for cluster_id in ep.get(SIG_EP_OUTPUT, []): for cluster_id in ep.get(SIG_EP_OUTPUT, []):
cluster = endpoint.add_output_cluster(cluster_id) endpoint.add_output_cluster(cluster_id)
if patch_cluster:
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) common.patch_cluster(cluster)
return device return device

View File

@ -3,6 +3,8 @@
from unittest.mock import patch from unittest.mock import patch
import pytest import pytest
import zhaquirks.sinope.thermostat
import zhaquirks.tuya.valve
import zigpy.profiles import zigpy.profiles
import zigpy.zcl.clusters import zigpy.zcl.clusters
from zigpy.zcl.clusters.hvac import Thermostat from zigpy.zcl.clusters.hvac import Thermostat
@ -96,6 +98,12 @@ CLIMATE_SINOPE = {
], ],
SIG_EP_OUTPUT: [zigpy.zcl.clusters.general.Ota.cluster_id, 65281], 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 = { CLIMATE_ZEN = {
@ -159,13 +167,13 @@ ZCL_ATTR_PLUG = {
def device_climate_mock(hass, zigpy_device_mock, zha_device_joined): def device_climate_mock(hass, zigpy_device_mock, zha_device_joined):
"""Test regular thermostat device.""" """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: if plug is None:
plugged_attrs = ZCL_ATTR_PLUG plugged_attrs = ZCL_ATTR_PLUG
else: else:
plugged_attrs = {**ZCL_ATTR_PLUG, **plug} 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 zigpy_device.endpoints[1].thermostat.PLUGGED_ATTR_READS = plugged_attrs
zha_device = await zha_device_joined(zigpy_device) zha_device = await zha_device_joined(zigpy_device)
await async_enable_traffic(hass, [zha_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): async def device_climate_sinope(device_climate_mock):
"""Sinope thermostat.""" """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 @pytest.fixture
@ -212,7 +224,9 @@ async def device_climate_zen(device_climate_mock):
async def device_climate_moes(device_climate_mock): async def device_climate_moes(device_climate_mock):
"""MOES thermostat.""" """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(): def test_sequence_mappings():
@ -456,22 +470,18 @@ async def test_target_temperature(
): ):
"""Test target temperature property.""" """Test target temperature property."""
with patch.object( device_climate = await device_climate_mock(
zigpy.zcl.clusters.manufacturer_specific.ManufacturerSpecificCluster, CLIMATE_SINOPE,
"ep_attribute", {
"sinope_manufacturer_specific", "occupied_cooling_setpoint": 2500,
): "occupied_heating_setpoint": 2200,
device_climate = await device_climate_mock( "system_mode": sys_mode,
CLIMATE_SINOPE, "unoccupied_heating_setpoint": 1600,
{ "unoccupied_cooling_setpoint": 2700,
"occupied_cooling_setpoint": 2500, },
"occupied_heating_setpoint": 2200, manuf=MANUF_SINOPE,
"system_mode": sys_mode, quirk=zhaquirks.sinope.thermostat.SinopeTechnologiesThermostat,
"unoccupied_heating_setpoint": 1600, )
"unoccupied_cooling_setpoint": 2700,
},
manuf=MANUF_SINOPE,
)
entity_id = await find_entity_id(DOMAIN, device_climate, hass) entity_id = await find_entity_id(DOMAIN, device_climate, hass)
if preset: if preset:
await hass.services.async_call( await hass.services.async_call(
@ -498,20 +508,16 @@ async def test_target_temperature_high(
): ):
"""Test target temperature high property.""" """Test target temperature high property."""
with patch.object( device_climate = await device_climate_mock(
zigpy.zcl.clusters.manufacturer_specific.ManufacturerSpecificCluster, CLIMATE_SINOPE,
"ep_attribute", {
"sinope_manufacturer_specific", "occupied_cooling_setpoint": 1700,
): "system_mode": Thermostat.SystemMode.Auto,
device_climate = await device_climate_mock( "unoccupied_cooling_setpoint": unoccupied,
CLIMATE_SINOPE, },
{ manuf=MANUF_SINOPE,
"occupied_cooling_setpoint": 1700, quirk=zhaquirks.sinope.thermostat.SinopeTechnologiesThermostat,
"system_mode": Thermostat.SystemMode.Auto, )
"unoccupied_cooling_setpoint": unoccupied,
},
manuf=MANUF_SINOPE,
)
entity_id = await find_entity_id(DOMAIN, device_climate, hass) entity_id = await find_entity_id(DOMAIN, device_climate, hass)
if preset: if preset:
await hass.services.async_call( await hass.services.async_call(
@ -538,20 +544,16 @@ async def test_target_temperature_low(
): ):
"""Test target temperature low property.""" """Test target temperature low property."""
with patch.object( device_climate = await device_climate_mock(
zigpy.zcl.clusters.manufacturer_specific.ManufacturerSpecificCluster, CLIMATE_SINOPE,
"ep_attribute", {
"sinope_manufacturer_specific", "occupied_heating_setpoint": 2100,
): "system_mode": Thermostat.SystemMode.Auto,
device_climate = await device_climate_mock( "unoccupied_heating_setpoint": unoccupied,
CLIMATE_SINOPE, },
{ manuf=MANUF_SINOPE,
"occupied_heating_setpoint": 2100, quirk=zhaquirks.sinope.thermostat.SinopeTechnologiesThermostat,
"system_mode": Thermostat.SystemMode.Auto, )
"unoccupied_heating_setpoint": unoccupied,
},
manuf=MANUF_SINOPE,
)
entity_id = await find_entity_id(DOMAIN, device_climate, hass) entity_id = await find_entity_id(DOMAIN, device_climate, hass)
if preset: if preset:
await hass.services.async_call( 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): async def test_set_temperature_heat_cool(hass, device_climate_mock):
"""Test setting temperature service call in heating/cooling HVAC mode.""" """Test setting temperature service call in heating/cooling HVAC mode."""
with patch.object( device_climate = await device_climate_mock(
zigpy.zcl.clusters.manufacturer_specific.ManufacturerSpecificCluster, CLIMATE_SINOPE,
"ep_attribute", {
"sinope_manufacturer_specific", "occupied_cooling_setpoint": 2500,
): "occupied_heating_setpoint": 2000,
device_climate = await device_climate_mock( "system_mode": Thermostat.SystemMode.Auto,
CLIMATE_SINOPE, "unoccupied_heating_setpoint": 1600,
{ "unoccupied_cooling_setpoint": 2700,
"occupied_cooling_setpoint": 2500, },
"occupied_heating_setpoint": 2000, manuf=MANUF_SINOPE,
"system_mode": Thermostat.SystemMode.Auto, quirk=zhaquirks.sinope.thermostat.SinopeTechnologiesThermostat,
"unoccupied_heating_setpoint": 1600, )
"unoccupied_cooling_setpoint": 2700,
},
manuf=MANUF_SINOPE,
)
entity_id = await find_entity_id(DOMAIN, device_climate, hass) entity_id = await find_entity_id(DOMAIN, device_climate, hass)
thrm_cluster = device_climate.device.endpoints[1].thermostat 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): async def test_set_temperature_heat(hass, device_climate_mock):
"""Test setting temperature service call in heating HVAC mode.""" """Test setting temperature service call in heating HVAC mode."""
with patch.object( device_climate = await device_climate_mock(
zigpy.zcl.clusters.manufacturer_specific.ManufacturerSpecificCluster, CLIMATE_SINOPE,
"ep_attribute", {
"sinope_manufacturer_specific", "occupied_cooling_setpoint": 2500,
): "occupied_heating_setpoint": 2000,
device_climate = await device_climate_mock( "system_mode": Thermostat.SystemMode.Heat,
CLIMATE_SINOPE, "unoccupied_heating_setpoint": 1600,
{ "unoccupied_cooling_setpoint": 2700,
"occupied_cooling_setpoint": 2500, },
"occupied_heating_setpoint": 2000, manuf=MANUF_SINOPE,
"system_mode": Thermostat.SystemMode.Heat, quirk=zhaquirks.sinope.thermostat.SinopeTechnologiesThermostat,
"unoccupied_heating_setpoint": 1600, )
"unoccupied_cooling_setpoint": 2700,
},
manuf=MANUF_SINOPE,
)
entity_id = await find_entity_id(DOMAIN, device_climate, hass) entity_id = await find_entity_id(DOMAIN, device_climate, hass)
thrm_cluster = device_climate.device.endpoints[1].thermostat 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): async def test_set_temperature_cool(hass, device_climate_mock):
"""Test setting temperature service call in cooling HVAC mode.""" """Test setting temperature service call in cooling HVAC mode."""
with patch.object( device_climate = await device_climate_mock(
zigpy.zcl.clusters.manufacturer_specific.ManufacturerSpecificCluster, CLIMATE_SINOPE,
"ep_attribute", {
"sinope_manufacturer_specific", "occupied_cooling_setpoint": 2500,
): "occupied_heating_setpoint": 2000,
device_climate = await device_climate_mock( "system_mode": Thermostat.SystemMode.Cool,
CLIMATE_SINOPE, "unoccupied_cooling_setpoint": 1600,
{ "unoccupied_heating_setpoint": 2700,
"occupied_cooling_setpoint": 2500, },
"occupied_heating_setpoint": 2000, manuf=MANUF_SINOPE,
"system_mode": Thermostat.SystemMode.Cool, quirk=zhaquirks.sinope.thermostat.SinopeTechnologiesThermostat,
"unoccupied_cooling_setpoint": 1600, )
"unoccupied_heating_setpoint": 2700,
},
manuf=MANUF_SINOPE,
)
entity_id = await find_entity_id(DOMAIN, device_climate, hass) entity_id = await find_entity_id(DOMAIN, device_climate, hass)
thrm_cluster = device_climate.device.endpoints[1].thermostat thrm_cluster = device_climate.device.endpoints[1].thermostat