Update HVAC action handling in ZHA climate devices (#61460)

* Update HVAC action handling in ZHA climate devices

* fix class name

* align with class name changes

* get the correct sensor entity for state assertions
This commit is contained in:
David F. Mulcahey 2021-12-12 12:11:37 -05:00 committed by GitHub
parent 7711f9a391
commit 94324cebea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 132 additions and 153 deletions

View File

@ -7,10 +7,11 @@ at https://home-assistant.io/components/zha.climate/
from __future__ import annotations from __future__ import annotations
from datetime import datetime, timedelta from datetime import datetime, timedelta
import enum
import functools import functools
from random import randint from random import randint
from zigpy.zcl.clusters.hvac import Fan as F, Thermostat as T
from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate.const import ( from homeassistant.components.climate.const import (
ATTR_HVAC_MODE, ATTR_HVAC_MODE,
@ -82,27 +83,6 @@ STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.CLIMATE)
MULTI_MATCH = functools.partial(ZHA_ENTITIES.multipass_match, Platform.CLIMATE) MULTI_MATCH = functools.partial(ZHA_ENTITIES.multipass_match, Platform.CLIMATE)
RUNNING_MODE = {0x00: HVAC_MODE_OFF, 0x03: HVAC_MODE_COOL, 0x04: HVAC_MODE_HEAT} RUNNING_MODE = {0x00: HVAC_MODE_OFF, 0x03: HVAC_MODE_COOL, 0x04: HVAC_MODE_HEAT}
class ThermostatFanMode(enum.IntEnum):
"""Fan channel enum for thermostat Fans."""
OFF = 0x00
ON = 0x04
AUTO = 0x05
class RunningState(enum.IntFlag):
"""ZCL Running state enum."""
HEAT = 0x0001
COOL = 0x0002
FAN = 0x0004
HEAT_STAGE_2 = 0x0008
COOL_STAGE_2 = 0x0010
FAN_STAGE_2 = 0x0020
FAN_STAGE_3 = 0x0040
SEQ_OF_OPERATION = { SEQ_OF_OPERATION = {
0x00: (HVAC_MODE_OFF, HVAC_MODE_COOL), # cooling only 0x00: (HVAC_MODE_OFF, HVAC_MODE_COOL), # cooling only
0x01: (HVAC_MODE_OFF, HVAC_MODE_COOL), # cooling with reheat 0x01: (HVAC_MODE_OFF, HVAC_MODE_COOL), # cooling with reheat
@ -116,40 +96,25 @@ SEQ_OF_OPERATION = {
0x07: (HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF), # centralite specific 0x07: (HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF), # centralite specific
} }
class SystemMode(enum.IntEnum):
"""ZCL System Mode attribute enum."""
OFF = 0x00
HEAT_COOL = 0x01
COOL = 0x03
HEAT = 0x04
AUX_HEAT = 0x05
PRE_COOL = 0x06
FAN_ONLY = 0x07
DRY = 0x08
SLEEP = 0x09
HVAC_MODE_2_SYSTEM = { HVAC_MODE_2_SYSTEM = {
HVAC_MODE_OFF: SystemMode.OFF, HVAC_MODE_OFF: T.SystemMode.Off,
HVAC_MODE_HEAT_COOL: SystemMode.HEAT_COOL, HVAC_MODE_HEAT_COOL: T.SystemMode.Auto,
HVAC_MODE_COOL: SystemMode.COOL, HVAC_MODE_COOL: T.SystemMode.Cool,
HVAC_MODE_HEAT: SystemMode.HEAT, HVAC_MODE_HEAT: T.SystemMode.Heat,
HVAC_MODE_FAN_ONLY: SystemMode.FAN_ONLY, HVAC_MODE_FAN_ONLY: T.SystemMode.Fan_only,
HVAC_MODE_DRY: SystemMode.DRY, HVAC_MODE_DRY: T.SystemMode.Dry,
} }
SYSTEM_MODE_2_HVAC = { SYSTEM_MODE_2_HVAC = {
SystemMode.OFF: HVAC_MODE_OFF, T.SystemMode.Off: HVAC_MODE_OFF,
SystemMode.HEAT_COOL: HVAC_MODE_HEAT_COOL, T.SystemMode.Auto: HVAC_MODE_HEAT_COOL,
SystemMode.COOL: HVAC_MODE_COOL, T.SystemMode.Cool: HVAC_MODE_COOL,
SystemMode.HEAT: HVAC_MODE_HEAT, T.SystemMode.Heat: HVAC_MODE_HEAT,
SystemMode.AUX_HEAT: HVAC_MODE_HEAT, T.SystemMode.Emergency_Heating: HVAC_MODE_HEAT,
SystemMode.PRE_COOL: HVAC_MODE_COOL, # this is 'precooling'. is it the same? T.SystemMode.Pre_cooling: HVAC_MODE_COOL, # this is 'precooling'. is it the same?
SystemMode.FAN_ONLY: HVAC_MODE_FAN_ONLY, T.SystemMode.Fan_only: HVAC_MODE_FAN_ONLY,
SystemMode.DRY: HVAC_MODE_DRY, T.SystemMode.Dry: HVAC_MODE_DRY,
SystemMode.SLEEP: HVAC_MODE_OFF, T.SystemMode.Sleep: HVAC_MODE_OFF,
} }
ZCL_TEMP = 100 ZCL_TEMP = 100
@ -233,7 +198,9 @@ class Thermostat(ZhaEntity, ClimateEntity):
return FAN_AUTO return FAN_AUTO
if self._thrm.running_state & ( if self._thrm.running_state & (
RunningState.FAN | RunningState.FAN_STAGE_2 | RunningState.FAN_STAGE_3 T.RunningState.Fan_State_On
| T.RunningState.Fan_2nd_Stage_On
| T.RunningState.Fan_3rd_Stage_On
): ):
return FAN_ON return FAN_ON
return FAN_AUTO return FAN_AUTO
@ -259,18 +226,25 @@ class Thermostat(ZhaEntity, ClimateEntity):
def _rm_rs_action(self) -> str | None: def _rm_rs_action(self) -> str | None:
"""Return the current HVAC action based on running mode and running state.""" """Return the current HVAC action based on running mode and running state."""
running_mode = self._thrm.running_mode if (running_state := self._thrm.running_state) is None:
if running_mode == SystemMode.HEAT: return None
if running_state & (
T.RunningState.Heat_State_On | T.RunningState.Heat_2nd_Stage_On
):
return CURRENT_HVAC_HEAT return CURRENT_HVAC_HEAT
if running_mode == SystemMode.COOL: if running_state & (
T.RunningState.Cool_State_On | T.RunningState.Cool_2nd_Stage_On
):
return CURRENT_HVAC_COOL return CURRENT_HVAC_COOL
if running_state & (
running_state = self._thrm.running_state T.RunningState.Fan_State_On
if running_state and running_state & ( | T.RunningState.Fan_2nd_Stage_On
RunningState.FAN | RunningState.FAN_STAGE_2 | RunningState.FAN_STAGE_3 | T.RunningState.Fan_3rd_Stage_On
): ):
return CURRENT_HVAC_FAN return CURRENT_HVAC_FAN
if self.hvac_mode != HVAC_MODE_OFF and running_mode == SystemMode.OFF: if running_state & T.RunningState.Idle:
return CURRENT_HVAC_IDLE
if self.hvac_mode != HVAC_MODE_OFF:
return CURRENT_HVAC_IDLE return CURRENT_HVAC_IDLE
return CURRENT_HVAC_OFF return CURRENT_HVAC_OFF
@ -431,9 +405,9 @@ class Thermostat(ZhaEntity, ClimateEntity):
return return
if fan_mode == FAN_ON: if fan_mode == FAN_ON:
mode = ThermostatFanMode.ON mode = F.FanMode.On
else: else:
mode = ThermostatFanMode.AUTO mode = F.FanMode.Auto
await self._fan.async_set_speed(mode) await self._fan.async_set_speed(mode)
@ -545,6 +519,27 @@ class SinopeTechnologiesThermostat(Thermostat):
self._supported_flags |= SUPPORT_PRESET_MODE self._supported_flags |= SUPPORT_PRESET_MODE
self._manufacturer_ch = self.cluster_channels["sinope_manufacturer_specific"] self._manufacturer_ch = self.cluster_channels["sinope_manufacturer_specific"]
@property
def _rm_rs_action(self) -> str | None:
"""Return the current HVAC action based on running mode and running state."""
running_mode = self._thrm.running_mode
if running_mode == T.SystemMode.Heat:
return CURRENT_HVAC_HEAT
if running_mode == T.SystemMode.Cool:
return CURRENT_HVAC_COOL
running_state = self._thrm.running_state
if running_state and running_state & (
T.RunningState.Fan_State_On
| T.RunningState.Fan_2nd_Stage_On
| T.RunningState.Fan_3rd_Stage_On
):
return CURRENT_HVAC_FAN
if self.hvac_mode != HVAC_MODE_OFF and running_mode == T.SystemMode.Off:
return CURRENT_HVAC_IDLE
return CURRENT_HVAC_OFF
@callback @callback
def _async_update_time(self, timestamp=None) -> None: def _async_update_time(self, timestamp=None) -> None:
"""Update thermostat's time display.""" """Update thermostat's time display."""
@ -588,25 +583,6 @@ class SinopeTechnologiesThermostat(Thermostat):
class ZenWithinThermostat(Thermostat): class ZenWithinThermostat(Thermostat):
"""Zen Within Thermostat implementation.""" """Zen Within Thermostat implementation."""
@property
def _rm_rs_action(self) -> str | None:
"""Return the current HVAC action based on running mode and running state."""
if (running_state := self._thrm.running_state) is None:
return None
if running_state & (RunningState.HEAT | RunningState.HEAT_STAGE_2):
return CURRENT_HVAC_HEAT
if running_state & (RunningState.COOL | RunningState.COOL_STAGE_2):
return CURRENT_HVAC_COOL
if running_state & (
RunningState.FAN | RunningState.FAN_STAGE_2 | RunningState.FAN_STAGE_3
):
return CURRENT_HVAC_FAN
if self.hvac_mode != HVAC_MODE_OFF:
return CURRENT_HVAC_IDLE
return CURRENT_HVAC_OFF
@MULTI_MATCH( @MULTI_MATCH(
channel_names=CHANNEL_THERMOSTAT, channel_names=CHANNEL_THERMOSTAT,

View File

@ -51,7 +51,6 @@ from .core import discovery
from .core.const import ( from .core.const import (
CHANNEL_ANALOG_INPUT, CHANNEL_ANALOG_INPUT,
CHANNEL_ELECTRICAL_MEASUREMENT, CHANNEL_ELECTRICAL_MEASUREMENT,
CHANNEL_FAN,
CHANNEL_HUMIDITY, CHANNEL_HUMIDITY,
CHANNEL_ILLUMINANCE, CHANNEL_ILLUMINANCE,
CHANNEL_LEAF_WETNESS, CHANNEL_LEAF_WETNESS,
@ -587,66 +586,6 @@ class ThermostatHVACAction(Sensor, id_suffix="hvac_action"):
return self._rm_rs_action return self._rm_rs_action
return self._pi_demand_action return self._pi_demand_action
@property
def _rm_rs_action(self) -> str | None:
"""Return the current HVAC action based on running mode and running state."""
running_mode = self._channel.running_mode
if running_mode == self._channel.RunningMode.Heat:
return CURRENT_HVAC_HEAT
if running_mode == self._channel.RunningMode.Cool:
return CURRENT_HVAC_COOL
running_state = self._channel.running_state
if running_state and running_state & (
self._channel.RunningState.Fan_State_On
| self._channel.RunningState.Fan_2nd_Stage_On
| self._channel.RunningState.Fan_3rd_Stage_On
):
return CURRENT_HVAC_FAN
if (
self._channel.system_mode != self._channel.SystemMode.Off
and running_mode == self._channel.SystemMode.Off
):
return CURRENT_HVAC_IDLE
return CURRENT_HVAC_OFF
@property
def _pi_demand_action(self) -> str | None:
"""Return the current HVAC action based on pi_demands."""
heating_demand = self._channel.pi_heating_demand
if heating_demand is not None and heating_demand > 0:
return CURRENT_HVAC_HEAT
cooling_demand = self._channel.pi_cooling_demand
if cooling_demand is not None and cooling_demand > 0:
return CURRENT_HVAC_COOL
if self._channel.system_mode != self._channel.SystemMode.Off:
return CURRENT_HVAC_IDLE
return CURRENT_HVAC_OFF
@callback
def async_set_state(self, *args, **kwargs) -> None:
"""Handle state update from channel."""
self.async_write_ha_state()
@MULTI_MATCH(
channel_names=CHANNEL_THERMOSTAT,
aux_channels=CHANNEL_FAN,
manufacturers="Centralite",
models={"3157100", "3157100-E"},
stop_on_match_group=CHANNEL_THERMOSTAT,
)
@MULTI_MATCH(
channel_names=CHANNEL_THERMOSTAT,
manufacturers="Zen Within",
stop_on_match_group=CHANNEL_THERMOSTAT,
)
class ZenHVACAction(ThermostatHVACAction):
"""Zen Within Thermostat HVAC Action."""
@property @property
def _rm_rs_action(self) -> str | None: def _rm_rs_action(self) -> str | None:
"""Return the current HVAC action based on running mode and running state.""" """Return the current HVAC action based on running mode and running state."""
@ -676,6 +615,63 @@ class ZenHVACAction(ThermostatHVACAction):
): ):
return CURRENT_HVAC_FAN return CURRENT_HVAC_FAN
running_state = self._channel.running_state
if running_state and running_state & self._channel.RunningState.Idle:
return CURRENT_HVAC_IDLE
if self._channel.system_mode != self._channel.SystemMode.Off: if self._channel.system_mode != self._channel.SystemMode.Off:
return CURRENT_HVAC_IDLE return CURRENT_HVAC_IDLE
return CURRENT_HVAC_OFF return CURRENT_HVAC_OFF
@property
def _pi_demand_action(self) -> str | None:
"""Return the current HVAC action based on pi_demands."""
heating_demand = self._channel.pi_heating_demand
if heating_demand is not None and heating_demand > 0:
return CURRENT_HVAC_HEAT
cooling_demand = self._channel.pi_cooling_demand
if cooling_demand is not None and cooling_demand > 0:
return CURRENT_HVAC_COOL
if self._channel.system_mode != self._channel.SystemMode.Off:
return CURRENT_HVAC_IDLE
return CURRENT_HVAC_OFF
@callback
def async_set_state(self, *args, **kwargs) -> None:
"""Handle state update from channel."""
self.async_write_ha_state()
@MULTI_MATCH(
channel_names={CHANNEL_THERMOSTAT},
manufacturers="Sinope Technologies",
stop_on_match_group=CHANNEL_THERMOSTAT,
)
class SinopeHVACAction(ThermostatHVACAction):
"""Sinope Thermostat HVAC action sensor."""
@property
def _rm_rs_action(self) -> str | None:
"""Return the current HVAC action based on running mode and running state."""
running_mode = self._channel.running_mode
if running_mode == self._channel.RunningMode.Heat:
return CURRENT_HVAC_HEAT
if running_mode == self._channel.RunningMode.Cool:
return CURRENT_HVAC_COOL
running_state = self._channel.running_state
if running_state and running_state & (
self._channel.RunningState.Fan_State_On
| self._channel.RunningState.Fan_2nd_Stage_On
| self._channel.RunningState.Fan_3rd_Stage_On
):
return CURRENT_HVAC_FAN
if (
self._channel.system_mode != self._channel.SystemMode.Off
and running_mode == self._channel.SystemMode.Off
):
return CURRENT_HVAC_IDLE
return CURRENT_HVAC_OFF

View File

@ -106,7 +106,7 @@ async def send_attributes_report(hass, cluster: zigpy.zcl.Cluster, attributes: d
await hass.async_block_till_done() await hass.async_block_till_done()
async def find_entity_id(domain, zha_device, hass): async def find_entity_id(domain, zha_device, hass, qualifier=None):
"""Find the entity id under the testing. """Find the entity id under the testing.
This is used to get the entity id in order to get the state from the state This is used to get the entity id in order to get the state from the state
@ -115,6 +115,11 @@ async def find_entity_id(domain, zha_device, hass):
entities = await find_entity_ids(domain, zha_device, hass) entities = await find_entity_ids(domain, zha_device, hass)
if not entities: if not entities:
return None return None
if qualifier:
for entity_id in entities:
if qualifier in entity_id:
return entity_id
else:
return entities[0] return entities[0]

View File

@ -254,12 +254,14 @@ async def test_climate_local_temp(hass, device_climate):
assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 21.0 assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 21.0
async def test_climate_hvac_action_running_state(hass, device_climate): async def test_climate_hvac_action_running_state(hass, device_climate_sinope):
"""Test hvac action via running state.""" """Test hvac action via running state."""
thrm_cluster = device_climate.device.endpoints[1].thermostat thrm_cluster = device_climate_sinope.device.endpoints[1].thermostat
entity_id = await find_entity_id(Platform.CLIMATE, device_climate, hass) entity_id = await find_entity_id(Platform.CLIMATE, device_climate_sinope, hass)
sensor_entity_id = await find_entity_id(Platform.SENSOR, device_climate, hass) sensor_entity_id = await find_entity_id(
Platform.SENSOR, device_climate_sinope, hass, "hvac"
)
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF
@ -407,7 +409,7 @@ async def test_climate_hvac_action_pi_demand(hass, device_climate):
entity_id = await find_entity_id(Platform.CLIMATE, device_climate, hass) entity_id = await find_entity_id(Platform.CLIMATE, device_climate, hass)
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF assert ATTR_HVAC_ACTION not in state.attributes
await send_attributes_report(hass, thrm_cluster, {0x0007: 10}) await send_attributes_report(hass, thrm_cluster, {0x0007: 10})
state = hass.states.get(entity_id) state = hass.states.get(entity_id)

View File

@ -3267,7 +3267,7 @@ DEVICES = [
}, },
("sensor", "00:11:22:33:44:55:66:77-1-513-hvac_action"): { ("sensor", "00:11:22:33:44:55:66:77-1-513-hvac_action"): {
DEV_SIG_CHANNELS: ["thermostat"], DEV_SIG_CHANNELS: ["thermostat"],
DEV_SIG_ENT_MAP_CLASS: "ThermostatHVACAction", DEV_SIG_ENT_MAP_CLASS: "SinopeHVACAction",
DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_thermostat_hvac_action", DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_thermostat_hvac_action",
}, },
}, },
@ -3312,7 +3312,7 @@ DEVICES = [
}, },
("sensor", "00:11:22:33:44:55:66:77-1-513-hvac_action"): { ("sensor", "00:11:22:33:44:55:66:77-1-513-hvac_action"): {
DEV_SIG_CHANNELS: ["thermostat"], DEV_SIG_CHANNELS: ["thermostat"],
DEV_SIG_ENT_MAP_CLASS: "ThermostatHVACAction", DEV_SIG_ENT_MAP_CLASS: "SinopeHVACAction",
DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_thermostat_hvac_action", DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_thermostat_hvac_action",
}, },
("sensor", "00:11:22:33:44:55:66:77-1-1026"): { ("sensor", "00:11:22:33:44:55:66:77-1-1026"): {
@ -3557,7 +3557,7 @@ DEVICES = [
}, },
("sensor", "00:11:22:33:44:55:66:77-1-513-hvac_action"): { ("sensor", "00:11:22:33:44:55:66:77-1-513-hvac_action"): {
DEV_SIG_CHANNELS: ["thermostat"], DEV_SIG_CHANNELS: ["thermostat"],
DEV_SIG_ENT_MAP_CLASS: "ZenHVACAction", DEV_SIG_ENT_MAP_CLASS: "ThermostatHVACAction",
DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_77665544_thermostat_hvac_action", DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_77665544_thermostat_hvac_action",
}, },
}, },