-
-
Notifications
You must be signed in to change notification settings - Fork 30k
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
Changes from 3 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 @@ | |
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,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",), | ||
"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, | ||
|
@@ -80,6 +105,7 @@ | |
} | ||
|
||
PRESET_MAPPINGS = { | ||
"away": PRESET_AWAY, | ||
"full power": PRESET_BOOST, | ||
"manufacturer specific": PRESET_MANUFACTURER_SPECIFIC, | ||
} | ||
|
@@ -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] | ||
|
@@ -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: | ||
|
@@ -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()) | ||
|
@@ -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( | ||
( | ||
|
@@ -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.""" | ||
|
@@ -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 | ||
|
||
|
@@ -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 [] | ||
|
||
|
@@ -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 | ||
|
||
|
@@ -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 | ||
|
||
|
@@ -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 [] | ||
|
||
|
@@ -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.""" | ||
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.""" | ||
|
@@ -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.""" | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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