Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve z-wave thermostat support #27040

Merged
merged 4 commits into from
Nov 26, 2019
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 94 additions & 28 deletions homeassistant/components/zwave/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -17,18 +19,23 @@
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__)
Expand Down Expand Up @@ -66,6 +73,24 @@
"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",),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The eco mode is called "Heat Eco" in case of Eurotronic Spirit-Z thermostats. I'm not sure if this makes a difference.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for noticing that.
The labels I used are from https://github.com/OpenZWave/open-zwave/blob/85ca5769a57d13f328e65b415d95e95647994366/cpp/src/command_classes/ThermostatMode.cpp#L56-L94 .
But it turns out open-zwave let's you override the labels in xml as well.
In your case here: https://github.com/OpenZWave/open-zwave/blob/85ca5769a57d13f328e65b415d95e95647994366/config/eurotronic/eur_spiritz.xml#L71
So indexes are the same but labels are different :(
I just ran grep across all XML configs and found various confusing labels for same indexes e.g.
Energy Heat = Energy Saving = Heat Eco = Heat Econ and so on.

I will add the aliases I've found.
Sometime later it makes sense to switch to indexes instead of aliases since they seem to be more stable

"cool econ": ("setpoint_eco_cooling",),
"away": ("setpoint_away_heating", "setpoint_away_cooling"),
"full power": ("setpoint_full_power",),
# for tests
"heat_cool": ("setpoint_heating", "setpoint_cooling"),
}

HVAC_CURRENT_MAPPINGS = {
"idle": CURRENT_HVAC_IDLE,
"heat": CURRENT_HVAC_HEAT,
Expand All @@ -80,6 +105,7 @@
}

PRESET_MAPPINGS = {
"away": PRESET_AWAY,
"full power": PRESET_BOOST,
"manufacturer specific": PRESET_MANUFACTURER_SPECIFIC,
}
Expand Down Expand Up @@ -124,6 +150,7 @@ def __init__(self, values, temp_unit):
"""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]
Expand Down Expand Up @@ -154,10 +181,20 @@ def __init__(self, values, temp_unit):
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:
Expand Down Expand Up @@ -193,13 +230,13 @@ def update_properties(self):

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())
Expand Down Expand Up @@ -227,7 +264,7 @@ def _update_operation_mode(self):
# 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(
(
Expand Down Expand Up @@ -313,15 +350,21 @@ def _update_swing_mode(self):

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."""
Expand Down Expand Up @@ -374,7 +417,7 @@ def hvac_mode(self):

Need to be one of HVAC_MODE_*.
"""
if self.values.mode:
if self.values.primary:
return self._hvac_mode
return self._default_hvac_mode

Expand All @@ -384,7 +427,7 @@ def hvac_modes(self):

Need to be a subset of HVAC_MODES.
"""
if self.values.mode:
if self.values.primary:
return self._hvac_list
return []

Expand All @@ -401,7 +444,7 @@ def is_aux_heat(self):
"""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

Expand All @@ -411,7 +454,7 @@ def preset_mode(self):

Need to be one of PRESET_*.
"""
if self.values.mode:
if self.values.primary:
return self._preset_mode
return PRESET_NONE

Expand All @@ -421,7 +464,7 @@ def preset_modes(self):

Need to be a subset of PRESET_MODES.
"""
if self.values.mode:
if self.values.primary:
return self._preset_list
return []

Expand All @@ -430,12 +473,35 @@ def target_temperature(self):
"""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."""
Expand All @@ -447,19 +513,19 @@ def set_fan_mode(self, fan_mode):
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
Copy link
Member

@Santobert Santobert Oct 5, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please ignore that if it's a bad idea or out of scope of this PR. We can prevent the setting of a mode that is already active with something like the following function:

    def update_mode(self, operation_mode):
        """Update the operation mode only if it is not already set."""
        if self.values.primary.data != operation_mode:
            self.values.primary.data == operation_mode

I noticed that my zwave thermostats move when an active mode is activated again.


def turn_aux_heat_on(self):
"""Turn auxillary heater on."""
if not self._aux_heat:
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."""
Expand All @@ -470,23 +536,23 @@ def turn_aux_heat_off(self):
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."""
Expand Down
61 changes: 56 additions & 5 deletions homeassistant/components/zwave/discovery_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,68 @@
DEFAULT_VALUES_SCHEMA,
**{
const.DISC_PRIMARY: {
const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT]
const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_MODE]
},
"setpoint_heating": {
const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT],
const.DISC_INDEX: [1],
const.DISC_OPTIONAL: True,
},
"setpoint_cooling": {
const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT],
const.DISC_INDEX: [2],
const.DISC_OPTIONAL: True,
},
"setpoint_furnace": {
const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT],
const.DISC_INDEX: [7],
const.DISC_OPTIONAL: True,
},
"setpoint_dry_air": {
const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT],
const.DISC_INDEX: [8],
const.DISC_OPTIONAL: True,
},
"setpoint_moist_air": {
const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT],
const.DISC_INDEX: [9],
const.DISC_OPTIONAL: True,
},
"setpoint_auto_changeover": {
const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT],
const.DISC_INDEX: [10],
const.DISC_OPTIONAL: True,
},
"setpoint_eco_heating": {
const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT],
const.DISC_INDEX: [11],
const.DISC_OPTIONAL: True,
},
"setpoint_eco_cooling": {
const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT],
const.DISC_INDEX: [12],
const.DISC_OPTIONAL: True,
},
"setpoint_away_heating": {
const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT],
const.DISC_INDEX: [13],
const.DISC_OPTIONAL: True,
},
"setpoint_away_cooling": {
const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT],
const.DISC_INDEX: [14],
const.DISC_OPTIONAL: True,
},
"setpoint_full_power": {
const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT],
const.DISC_INDEX: [15],
const.DISC_OPTIONAL: True,
},
"temperature": {
const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_SENSOR_MULTILEVEL],
const.DISC_INDEX: [const.INDEX_SENSOR_MULTILEVEL_TEMPERATURE],
const.DISC_OPTIONAL: True,
},
"mode": {
const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_MODE],
const.DISC_OPTIONAL: True,
},
"fan_mode": {
const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_FAN_MODE],
const.DISC_OPTIONAL: True,
Expand Down
Loading