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 datetime import datetime, timedelta
import enum
import functools
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.const import (
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)
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 = {
0x00: (HVAC_MODE_OFF, HVAC_MODE_COOL), # cooling only
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
}
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_OFF: SystemMode.OFF,
HVAC_MODE_HEAT_COOL: SystemMode.HEAT_COOL,
HVAC_MODE_COOL: SystemMode.COOL,
HVAC_MODE_HEAT: SystemMode.HEAT,
HVAC_MODE_FAN_ONLY: SystemMode.FAN_ONLY,
HVAC_MODE_DRY: SystemMode.DRY,
HVAC_MODE_OFF: T.SystemMode.Off,
HVAC_MODE_HEAT_COOL: T.SystemMode.Auto,
HVAC_MODE_COOL: T.SystemMode.Cool,
HVAC_MODE_HEAT: T.SystemMode.Heat,
HVAC_MODE_FAN_ONLY: T.SystemMode.Fan_only,
HVAC_MODE_DRY: T.SystemMode.Dry,
}
SYSTEM_MODE_2_HVAC = {
SystemMode.OFF: HVAC_MODE_OFF,
SystemMode.HEAT_COOL: HVAC_MODE_HEAT_COOL,
SystemMode.COOL: HVAC_MODE_COOL,
SystemMode.HEAT: HVAC_MODE_HEAT,
SystemMode.AUX_HEAT: HVAC_MODE_HEAT,
SystemMode.PRE_COOL: HVAC_MODE_COOL, # this is 'precooling'. is it the same?
SystemMode.FAN_ONLY: HVAC_MODE_FAN_ONLY,
SystemMode.DRY: HVAC_MODE_DRY,
SystemMode.SLEEP: HVAC_MODE_OFF,
T.SystemMode.Off: HVAC_MODE_OFF,
T.SystemMode.Auto: HVAC_MODE_HEAT_COOL,
T.SystemMode.Cool: HVAC_MODE_COOL,
T.SystemMode.Heat: HVAC_MODE_HEAT,
T.SystemMode.Emergency_Heating: HVAC_MODE_HEAT,
T.SystemMode.Pre_cooling: HVAC_MODE_COOL, # this is 'precooling'. is it the same?
T.SystemMode.Fan_only: HVAC_MODE_FAN_ONLY,
T.SystemMode.Dry: HVAC_MODE_DRY,
T.SystemMode.Sleep: HVAC_MODE_OFF,
}
ZCL_TEMP = 100
@ -233,7 +198,9 @@ class Thermostat(ZhaEntity, ClimateEntity):
return FAN_AUTO
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_AUTO
@ -259,18 +226,25 @@ class Thermostat(ZhaEntity, ClimateEntity):
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 == SystemMode.HEAT:
if (running_state := self._thrm.running_state) is None:
return None
if running_state & (
T.RunningState.Heat_State_On | T.RunningState.Heat_2nd_Stage_On
):
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
running_state = self._thrm.running_state
if running_state and running_state & (
RunningState.FAN | RunningState.FAN_STAGE_2 | RunningState.FAN_STAGE_3
if 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 == 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_OFF
@ -431,9 +405,9 @@ class Thermostat(ZhaEntity, ClimateEntity):
return
if fan_mode == FAN_ON:
mode = ThermostatFanMode.ON
mode = F.FanMode.On
else:
mode = ThermostatFanMode.AUTO
mode = F.FanMode.Auto
await self._fan.async_set_speed(mode)
@ -545,6 +519,27 @@ class SinopeTechnologiesThermostat(Thermostat):
self._supported_flags |= SUPPORT_PRESET_MODE
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
def _async_update_time(self, timestamp=None) -> None:
"""Update thermostat's time display."""
@ -588,25 +583,6 @@ class SinopeTechnologiesThermostat(Thermostat):
class ZenWithinThermostat(Thermostat):
"""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(
channel_names=CHANNEL_THERMOSTAT,

View file

@ -51,7 +51,6 @@ from .core import discovery
from .core.const import (
CHANNEL_ANALOG_INPUT,
CHANNEL_ELECTRICAL_MEASUREMENT,
CHANNEL_FAN,
CHANNEL_HUMIDITY,
CHANNEL_ILLUMINANCE,
CHANNEL_LEAF_WETNESS,
@ -587,66 +586,6 @@ class ThermostatHVACAction(Sensor, id_suffix="hvac_action"):
return self._rm_rs_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
def _rm_rs_action(self) -> str | None:
"""Return the current HVAC action based on running mode and running state."""
@ -676,6 +615,63 @@ class ZenHVACAction(ThermostatHVACAction):
):
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:
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},
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()
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.
This is used to get the entity id in order to get the state from the state
@ -115,7 +115,12 @@ async def find_entity_id(domain, zha_device, hass):
entities = await find_entity_ids(domain, zha_device, hass)
if not entities:
return None
return entities[0]
if qualifier:
for entity_id in entities:
if qualifier in entity_id:
return entity_id
else:
return entities[0]
async def find_entity_ids(domain, zha_device, hass):

View file

@ -254,12 +254,14 @@ async def test_climate_local_temp(hass, device_climate):
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."""
thrm_cluster = device_climate.device.endpoints[1].thermostat
entity_id = await find_entity_id(Platform.CLIMATE, device_climate, hass)
sensor_entity_id = await find_entity_id(Platform.SENSOR, device_climate, hass)
thrm_cluster = device_climate_sinope.device.endpoints[1].thermostat
entity_id = await find_entity_id(Platform.CLIMATE, device_climate_sinope, hass)
sensor_entity_id = await find_entity_id(
Platform.SENSOR, device_climate_sinope, hass, "hvac"
)
state = hass.states.get(entity_id)
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)
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})
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"): {
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",
},
},
@ -3312,7 +3312,7 @@ DEVICES = [
},
("sensor", "00:11:22:33:44:55:66:77-1-513-hvac_action"): {
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",
},
("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"): {
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",
},
},