diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py index 3473a6d8f9e..c6ad3dcc89e 100644 --- a/homeassistant/components/zha/climate.py +++ b/homeassistant/components/zha/climate.py @@ -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, diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 406bcbfa7aa..8a4301b12fb 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -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 diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index b302869d9e4..48772d31fb6 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -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): diff --git a/tests/components/zha/test_climate.py b/tests/components/zha/test_climate.py index 58d5240aad7..4dc72b092e4 100644 --- a/tests/components/zha/test_climate.py +++ b/tests/components/zha/test_climate.py @@ -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) diff --git a/tests/components/zha/zha_devices_list.py b/tests/components/zha/zha_devices_list.py index 06d3f10556c..54f9a35610b 100644 --- a/tests/components/zha/zha_devices_list.py +++ b/tests/components/zha/zha_devices_list.py @@ -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", }, },