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:
parent
7711f9a391
commit
94324cebea
5 changed files with 132 additions and 153 deletions
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
|
|
Loading…
Add table
Reference in a new issue