Improve z-wave thermostat support (#27040)
* Improve z-wave thermostat support Discover thermostat using COMMAND_CLASS_THERMOSTAT_MODE so that it is a single entitiy in case there are multiple setpoints. z-wave docs mention it is always present. Add support for single/range target temperature depending on the current thermostat mode. * Remove debug print * Refactor Z-Wave dynamic setpoint(s) selection - use explicit mapping between modes and setpoints as defined in Z-Wave specs - add tests for away (2 setpoints) and heat eco (1 setpoint) modes * Add non-standard thermostat mode aliases
This commit is contained in:
parent
807de1aeb3
commit
f5c01cc30d
4 changed files with 479 additions and 115 deletions
|
@ -2,6 +2,8 @@
|
|||
# Because we do not compile openzwave on CI
|
||||
import logging
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from homeassistant.components.climate import ClimateDevice
|
||||
from homeassistant.components.climate.const import (
|
||||
CURRENT_HVAC_COOL,
|
||||
|
@ -17,18 +19,23 @@ from homeassistant.components.climate.const import (
|
|||
HVAC_MODE_DRY,
|
||||
HVAC_MODE_FAN_ONLY,
|
||||
HVAC_MODE_OFF,
|
||||
PRESET_AWAY,
|
||||
PRESET_BOOST,
|
||||
PRESET_NONE,
|
||||
SUPPORT_AUX_HEAT,
|
||||
SUPPORT_FAN_MODE,
|
||||
SUPPORT_SWING_MODE,
|
||||
SUPPORT_TARGET_TEMPERATURE,
|
||||
SUPPORT_TARGET_TEMPERATURE_RANGE,
|
||||
SUPPORT_PRESET_MODE,
|
||||
ATTR_TARGET_TEMP_LOW,
|
||||
ATTR_TARGET_TEMP_HIGH,
|
||||
)
|
||||
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
|
||||
from . import ZWaveDeviceEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -66,6 +73,33 @@ HVAC_STATE_MAPPINGS = {
|
|||
"auto changeover": HVAC_MODE_HEAT_COOL,
|
||||
}
|
||||
|
||||
MODE_SETPOINT_MAPPINGS = {
|
||||
"off": (),
|
||||
"heat": ("setpoint_heating",),
|
||||
"cool": ("setpoint_cooling",),
|
||||
"auto": ("setpoint_heating", "setpoint_cooling"),
|
||||
"aux heat": ("setpoint_heating",),
|
||||
"furnace": ("setpoint_furnace",),
|
||||
"dry air": ("setpoint_dry_air",),
|
||||
"moist air": ("setpoint_moist_air",),
|
||||
"auto changeover": ("setpoint_auto_changeover",),
|
||||
"heat econ": ("setpoint_eco_heating",),
|
||||
"cool econ": ("setpoint_eco_cooling",),
|
||||
"away": ("setpoint_away_heating", "setpoint_away_cooling"),
|
||||
"full power": ("setpoint_full_power",),
|
||||
# aliases found in xml configs
|
||||
"comfort": ("setpoint_heating",),
|
||||
"heat mode": ("setpoint_heating",),
|
||||
"heat (default)": ("setpoint_heating",),
|
||||
"dry floor": ("setpoint_dry_air",),
|
||||
"heat eco": ("setpoint_eco_heating",),
|
||||
"energy saving": ("setpoint_eco_heating",),
|
||||
"energy heat": ("setpoint_eco_heating",),
|
||||
"vacation": ("setpoint_away_heating", "setpoint_away_cooling"),
|
||||
# for tests
|
||||
"heat_cool": ("setpoint_heating", "setpoint_cooling"),
|
||||
}
|
||||
|
||||
HVAC_CURRENT_MAPPINGS = {
|
||||
"idle": CURRENT_HVAC_IDLE,
|
||||
"heat": CURRENT_HVAC_HEAT,
|
||||
|
@ -80,6 +114,7 @@ HVAC_CURRENT_MAPPINGS = {
|
|||
}
|
||||
|
||||
PRESET_MAPPINGS = {
|
||||
"away": PRESET_AWAY,
|
||||
"full power": PRESET_BOOST,
|
||||
"manufacturer specific": PRESET_MANUFACTURER_SPECIFIC,
|
||||
}
|
||||
|
@ -124,6 +159,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||
"""Initialize the Z-Wave climate device."""
|
||||
ZWaveDeviceEntity.__init__(self, values, DOMAIN)
|
||||
self._target_temperature = None
|
||||
self._target_temperature_range = (None, None)
|
||||
self._current_temperature = None
|
||||
self._hvac_action = None
|
||||
self._hvac_list = None # [zwave_mode]
|
||||
|
@ -154,10 +190,20 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||
self._zxt_120 = 1
|
||||
self.update_properties()
|
||||
|
||||
def _current_mode_setpoints(self):
|
||||
current_mode = str(self.values.primary.data).lower()
|
||||
setpoints_names = MODE_SETPOINT_MAPPINGS.get(current_mode, ())
|
||||
return tuple(getattr(self.values, name, None) for name in setpoints_names)
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
support = SUPPORT_TARGET_TEMPERATURE
|
||||
if HVAC_MODE_HEAT_COOL in self._hvac_list:
|
||||
support |= SUPPORT_TARGET_TEMPERATURE_RANGE
|
||||
if PRESET_AWAY in self._preset_list:
|
||||
support |= SUPPORT_TARGET_TEMPERATURE_RANGE
|
||||
|
||||
if self.values.fan_mode:
|
||||
support |= SUPPORT_FAN_MODE
|
||||
if self._zxt_120 == 1 and self.values.zxt_120_swing_mode:
|
||||
|
@ -193,13 +239,13 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||
|
||||
def _update_operation_mode(self):
|
||||
"""Update hvac and preset modes."""
|
||||
if self.values.mode:
|
||||
if self.values.primary:
|
||||
self._hvac_list = []
|
||||
self._hvac_mapping = {}
|
||||
self._preset_list = []
|
||||
self._preset_mapping = {}
|
||||
|
||||
mode_list = self.values.mode.data_items
|
||||
mode_list = self.values.primary.data_items
|
||||
if mode_list:
|
||||
for mode in mode_list:
|
||||
ha_mode = HVAC_STATE_MAPPINGS.get(str(mode).lower())
|
||||
|
@ -227,7 +273,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||
# Presets are supported
|
||||
self._preset_list.append(PRESET_NONE)
|
||||
|
||||
current_mode = self.values.mode.data
|
||||
current_mode = self.values.primary.data
|
||||
_LOGGER.debug("current_mode=%s", current_mode)
|
||||
_hvac_temp = next(
|
||||
(
|
||||
|
@ -313,15 +359,21 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||
|
||||
def _update_target_temp(self):
|
||||
"""Update target temperature."""
|
||||
if self.values.primary.data == 0:
|
||||
_LOGGER.debug(
|
||||
"Setpoint is 0, setting default to " "current_temperature=%s",
|
||||
self._current_temperature,
|
||||
)
|
||||
if self._current_temperature is not None:
|
||||
self._target_temperature = round((float(self._current_temperature)), 1)
|
||||
else:
|
||||
self._target_temperature = round((float(self.values.primary.data)), 1)
|
||||
current_setpoints = self._current_mode_setpoints()
|
||||
self._target_temperature = None
|
||||
self._target_temperature_range = (None, None)
|
||||
if len(current_setpoints) == 1:
|
||||
(setpoint,) = current_setpoints
|
||||
if setpoint is not None:
|
||||
self._target_temperature = round((float(setpoint.data)), 1)
|
||||
elif len(current_setpoints) == 2:
|
||||
(setpoint_low, setpoint_high) = current_setpoints
|
||||
target_low, target_high = None, None
|
||||
if setpoint_low is not None:
|
||||
target_low = round((float(setpoint_low.data)), 1)
|
||||
if setpoint_high is not None:
|
||||
target_high = round((float(setpoint_high.data)), 1)
|
||||
self._target_temperature_range = (target_low, target_high)
|
||||
|
||||
def _update_operating_state(self):
|
||||
"""Update operating state."""
|
||||
|
@ -374,7 +426,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||
|
||||
Need to be one of HVAC_MODE_*.
|
||||
"""
|
||||
if self.values.mode:
|
||||
if self.values.primary:
|
||||
return self._hvac_mode
|
||||
return self._default_hvac_mode
|
||||
|
||||
|
@ -384,7 +436,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||
|
||||
Need to be a subset of HVAC_MODES.
|
||||
"""
|
||||
if self.values.mode:
|
||||
if self.values.primary:
|
||||
return self._hvac_list
|
||||
return []
|
||||
|
||||
|
@ -401,7 +453,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||
"""Return true if aux heater."""
|
||||
if not self._aux_heat:
|
||||
return None
|
||||
if self.values.mode.data == AUX_HEAT_ZWAVE_MODE:
|
||||
if self.values.primary.data == AUX_HEAT_ZWAVE_MODE:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
@ -411,7 +463,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||
|
||||
Need to be one of PRESET_*.
|
||||
"""
|
||||
if self.values.mode:
|
||||
if self.values.primary:
|
||||
return self._preset_mode
|
||||
return PRESET_NONE
|
||||
|
||||
|
@ -421,7 +473,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||
|
||||
Need to be a subset of PRESET_MODES.
|
||||
"""
|
||||
if self.values.mode:
|
||||
if self.values.primary:
|
||||
return self._preset_list
|
||||
return []
|
||||
|
||||
|
@ -430,12 +482,35 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||
"""Return the temperature we try to reach."""
|
||||
return self._target_temperature
|
||||
|
||||
@property
|
||||
def target_temperature_low(self) -> Optional[float]:
|
||||
"""Return the lowbound target temperature we try to reach."""
|
||||
return self._target_temperature_range[0]
|
||||
|
||||
@property
|
||||
def target_temperature_high(self) -> Optional[float]:
|
||||
"""Return the highbound target temperature we try to reach."""
|
||||
return self._target_temperature_range[1]
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
_LOGGER.debug("Set temperature to %s", kwargs.get(ATTR_TEMPERATURE))
|
||||
if kwargs.get(ATTR_TEMPERATURE) is None:
|
||||
return
|
||||
self.values.primary.data = kwargs.get(ATTR_TEMPERATURE)
|
||||
current_setpoints = self._current_mode_setpoints()
|
||||
if len(current_setpoints) == 1:
|
||||
(setpoint,) = current_setpoints
|
||||
target_temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
if setpoint is not None and target_temp is not None:
|
||||
_LOGGER.debug("Set temperature to %s", target_temp)
|
||||
setpoint.data = target_temp
|
||||
elif len(current_setpoints) == 2:
|
||||
(setpoint_low, setpoint_high) = current_setpoints
|
||||
target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
|
||||
target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
||||
if setpoint_low is not None and target_temp_low is not None:
|
||||
_LOGGER.debug("Set low temperature to %s", target_temp_low)
|
||||
setpoint_low.data = target_temp_low
|
||||
if setpoint_high is not None and target_temp_high is not None:
|
||||
_LOGGER.debug("Set high temperature to %s", target_temp_high)
|
||||
setpoint_high.data = target_temp_high
|
||||
|
||||
def set_fan_mode(self, fan_mode):
|
||||
"""Set new target fan mode."""
|
||||
|
@ -447,11 +522,11 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||
def set_hvac_mode(self, hvac_mode):
|
||||
"""Set new target hvac mode."""
|
||||
_LOGGER.debug("Set hvac_mode to %s", hvac_mode)
|
||||
if not self.values.mode:
|
||||
if not self.values.primary:
|
||||
return
|
||||
operation_mode = self._hvac_mapping.get(hvac_mode)
|
||||
_LOGGER.debug("Set operation_mode to %s", operation_mode)
|
||||
self.values.mode.data = operation_mode
|
||||
self.values.primary.data = operation_mode
|
||||
|
||||
def turn_aux_heat_on(self):
|
||||
"""Turn auxillary heater on."""
|
||||
|
@ -459,7 +534,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||
return
|
||||
operation_mode = AUX_HEAT_ZWAVE_MODE
|
||||
_LOGGER.debug("Aux heat on. Set operation mode to %s", operation_mode)
|
||||
self.values.mode.data = operation_mode
|
||||
self.values.primary.data = operation_mode
|
||||
|
||||
def turn_aux_heat_off(self):
|
||||
"""Turn auxillary heater off."""
|
||||
|
@ -470,23 +545,23 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||
else:
|
||||
operation_mode = self._hvac_mapping.get(HVAC_MODE_OFF)
|
||||
_LOGGER.debug("Aux heat off. Set operation mode to %s", operation_mode)
|
||||
self.values.mode.data = operation_mode
|
||||
self.values.primary.data = operation_mode
|
||||
|
||||
def set_preset_mode(self, preset_mode):
|
||||
"""Set new target preset mode."""
|
||||
_LOGGER.debug("Set preset_mode to %s", preset_mode)
|
||||
if not self.values.mode:
|
||||
if not self.values.primary:
|
||||
return
|
||||
if preset_mode == PRESET_NONE:
|
||||
# Activate the current hvac mode
|
||||
self._update_operation_mode()
|
||||
operation_mode = self._hvac_mapping.get(self.hvac_mode)
|
||||
_LOGGER.debug("Set operation_mode to %s", operation_mode)
|
||||
self.values.mode.data = operation_mode
|
||||
self.values.primary.data = operation_mode
|
||||
else:
|
||||
operation_mode = self._preset_mapping.get(preset_mode, preset_mode)
|
||||
_LOGGER.debug("Set operation_mode to %s", operation_mode)
|
||||
self.values.mode.data = operation_mode
|
||||
self.values.primary.data = operation_mode
|
||||
|
||||
def set_swing_mode(self, swing_mode):
|
||||
"""Set new target swing mode."""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue