From f5c01cc30da96b5e03ab07b0811dbb42bffc93e2 Mon Sep 17 00:00:00 2001 From: Andrew Onyshchuk Date: Mon, 25 Nov 2019 18:32:37 -0600 Subject: [PATCH] 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 --- homeassistant/components/zwave/climate.py | 131 ++++-- .../components/zwave/discovery_schemas.py | 61 ++- tests/components/zwave/test_climate.py | 373 ++++++++++++++---- tests/components/zwave/test_init.py | 29 +- 4 files changed, 479 insertions(+), 115 deletions(-) diff --git a/homeassistant/components/zwave/climate.py b/homeassistant/components/zwave/climate.py index b40fff66958..e5090878328 100644 --- a/homeassistant/components/zwave/climate.py +++ b/homeassistant/components/zwave/climate.py @@ -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.""" diff --git a/homeassistant/components/zwave/discovery_schemas.py b/homeassistant/components/zwave/discovery_schemas.py index e2254073290..2d6f08169ea 100644 --- a/homeassistant/components/zwave/discovery_schemas.py +++ b/homeassistant/components/zwave/discovery_schemas.py @@ -57,17 +57,68 @@ DISCOVERY_SCHEMAS = [ 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, diff --git a/tests/components/zwave/test_climate.py b/tests/components/zwave/test_climate.py index 2f13d95fb9f..c9fe123af82 100644 --- a/tests/components/zwave/test_climate.py +++ b/tests/components/zwave/test_climate.py @@ -9,6 +9,7 @@ from homeassistant.components.climate.const import ( HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, + PRESET_AWAY, PRESET_BOOST, PRESET_ECO, PRESET_NONE, @@ -16,6 +17,9 @@ from homeassistant.components.climate.const import ( SUPPORT_PRESET_MODE, SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_RANGE, + ATTR_TARGET_TEMP_LOW, + ATTR_TARGET_TEMP_HIGH, ) from homeassistant.components.zwave import climate from homeassistant.components.zwave.climate import DEFAULT_HVAC_MODES @@ -29,9 +33,7 @@ def device(hass, mock_openzwave): """Fixture to provide a precreated climate device.""" node = MockNode() values = MockEntityValues( - primary=MockValue(data=1, node=node), - temperature=MockValue(data=5, node=node, units=None), - mode=MockValue( + primary=MockValue( data=HVAC_MODE_HEAT, data_items=[ HVAC_MODE_OFF, @@ -41,6 +43,9 @@ def device(hass, mock_openzwave): ], node=node, ), + setpoint_heating=MockValue(data=1, node=node), + setpoint_cooling=MockValue(data=10, node=node), + temperature=MockValue(data=5, node=node, units=None), fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), operating_state=MockValue(data=CURRENT_HVAC_HEAT, node=node), fan_action=MockValue(data=7, node=node), @@ -56,9 +61,7 @@ def device_zxt_120(hass, mock_openzwave): node = MockNode(manufacturer_id="5254", product_id="8377") values = MockEntityValues( - primary=MockValue(data=1, node=node), - temperature=MockValue(data=5, node=node, units=None), - mode=MockValue( + primary=MockValue( data=HVAC_MODE_HEAT, data_items=[ HVAC_MODE_OFF, @@ -68,6 +71,9 @@ def device_zxt_120(hass, mock_openzwave): ], node=node, ), + setpoint_heating=MockValue(data=1, node=node), + setpoint_cooling=MockValue(data=10, node=node), + temperature=MockValue(data=5, node=node, units=None), fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), operating_state=MockValue(data=CURRENT_HVAC_HEAT, node=node), fan_action=MockValue(data=7, node=node), @@ -83,13 +89,14 @@ def device_mapping(hass, mock_openzwave): """Fixture to provide a precreated climate device. Test state mapping.""" node = MockNode() values = MockEntityValues( - primary=MockValue(data=1, node=node), - temperature=MockValue(data=5, node=node, units=None), - mode=MockValue( + primary=MockValue( data="Heat", - data_items=["Off", "Cool", "Heat", "Full Power", "heat_cool"], + data_items=["Off", "Cool", "Heat", "Full Power", "Auto"], node=node, ), + setpoint_heating=MockValue(data=1, node=node), + setpoint_cooling=MockValue(data=10, node=node), + temperature=MockValue(data=5, node=node, units=None), fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), operating_state=MockValue(data="heating", node=node), fan_action=MockValue(data=7, node=node), @@ -104,13 +111,14 @@ def device_unknown(hass, mock_openzwave): """Fixture to provide a precreated climate device. Test state unknown.""" node = MockNode() values = MockEntityValues( - primary=MockValue(data=1, node=node), - temperature=MockValue(data=5, node=node, units=None), - mode=MockValue( + primary=MockValue( data="Heat", data_items=["Off", "Cool", "Heat", "heat_cool", "Abcdefg"], node=node, ), + setpoint_heating=MockValue(data=1, node=node), + setpoint_cooling=MockValue(data=10, node=node), + temperature=MockValue(data=5, node=node, units=None), fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), operating_state=MockValue(data="test4", node=node), fan_action=MockValue(data=7, node=node), @@ -125,9 +133,7 @@ def device_heat_cool(hass, mock_openzwave): """Fixture to provide a precreated climate device. Test state heat only.""" node = MockNode() values = MockEntityValues( - primary=MockValue(data=1, node=node), - temperature=MockValue(data=5, node=node, units=None), - mode=MockValue( + primary=MockValue( data=HVAC_MODE_HEAT, data_items=[ HVAC_MODE_OFF, @@ -138,6 +144,88 @@ def device_heat_cool(hass, mock_openzwave): ], node=node, ), + setpoint_heating=MockValue(data=1, node=node), + setpoint_cooling=MockValue(data=10, node=node), + temperature=MockValue(data=5, node=node, units=None), + fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), + operating_state=MockValue(data="test4", node=node), + fan_action=MockValue(data=7, node=node), + ) + device = climate.get_device(hass, node=node, values=values, node_config={}) + + yield device + + +@pytest.fixture +def device_heat_cool_range(hass, mock_openzwave): + """Fixture to provide a precreated climate device. Target range mode.""" + node = MockNode() + values = MockEntityValues( + primary=MockValue( + data=HVAC_MODE_HEAT_COOL, + data_items=[ + HVAC_MODE_OFF, + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_HEAT_COOL, + ], + node=node, + ), + setpoint_heating=MockValue(data=1, node=node), + setpoint_cooling=MockValue(data=10, node=node), + temperature=MockValue(data=5, node=node, units=None), + fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), + operating_state=MockValue(data="test4", node=node), + fan_action=MockValue(data=7, node=node), + ) + device = climate.get_device(hass, node=node, values=values, node_config={}) + + yield device + + +@pytest.fixture +def device_heat_cool_away(hass, mock_openzwave): + """Fixture to provide a precreated climate device. Target range mode.""" + node = MockNode() + values = MockEntityValues( + primary=MockValue( + data=HVAC_MODE_HEAT_COOL, + data_items=[ + HVAC_MODE_OFF, + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_HEAT_COOL, + PRESET_AWAY, + ], + node=node, + ), + setpoint_heating=MockValue(data=2, node=node), + setpoint_cooling=MockValue(data=9, node=node), + setpoint_away_heating=MockValue(data=1, node=node), + setpoint_away_cooling=MockValue(data=10, node=node), + temperature=MockValue(data=5, node=node, units=None), + fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), + operating_state=MockValue(data="test4", node=node), + fan_action=MockValue(data=7, node=node), + ) + device = climate.get_device(hass, node=node, values=values, node_config={}) + + yield device + + +@pytest.fixture +def device_heat_eco(hass, mock_openzwave): + """Fixture to provide a precreated climate device. heat/heat eco.""" + node = MockNode() + values = MockEntityValues( + primary=MockValue( + data=HVAC_MODE_HEAT, + data_items=[HVAC_MODE_OFF, HVAC_MODE_HEAT, "heat econ"], + node=node, + ), + setpoint_heating=MockValue(data=2, node=node), + setpoint_eco_heating=MockValue(data=1, node=node), + temperature=MockValue(data=5, node=node, units=None), fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), operating_state=MockValue(data="test4", node=node), fan_action=MockValue(data=7, node=node), @@ -155,7 +243,23 @@ def test_default_hvac_modes(): def test_supported_features(device): """Test supported features flags.""" - assert device.supported_features == SUPPORT_FAN_MODE + SUPPORT_TARGET_TEMPERATURE + assert ( + device.supported_features + == SUPPORT_FAN_MODE + + SUPPORT_TARGET_TEMPERATURE + + SUPPORT_TARGET_TEMPERATURE_RANGE + ) + + +def test_supported_features_temp_range(device_heat_cool_range): + """Test supported features flags with target temp range.""" + device = device_heat_cool_range + assert ( + device.supported_features + == SUPPORT_FAN_MODE + + SUPPORT_TARGET_TEMPERATURE + + SUPPORT_TARGET_TEMPERATURE_RANGE + ) def test_supported_features_preset_mode(device_mapping): @@ -163,7 +267,10 @@ def test_supported_features_preset_mode(device_mapping): device = device_mapping assert ( device.supported_features - == SUPPORT_FAN_MODE + SUPPORT_TARGET_TEMPERATURE + SUPPORT_PRESET_MODE + == SUPPORT_FAN_MODE + + SUPPORT_TARGET_TEMPERATURE + + SUPPORT_TARGET_TEMPERATURE_RANGE + + SUPPORT_PRESET_MODE ) @@ -172,7 +279,10 @@ def test_supported_features_swing_mode(device_zxt_120): device = device_zxt_120 assert ( device.supported_features - == SUPPORT_FAN_MODE + SUPPORT_TARGET_TEMPERATURE + SUPPORT_SWING_MODE + == SUPPORT_FAN_MODE + + SUPPORT_TARGET_TEMPERATURE + + SUPPORT_TARGET_TEMPERATURE_RANGE + + SUPPORT_SWING_MODE ) @@ -207,14 +317,6 @@ def test_temperature_unit(device): assert device.temperature_unit == TEMP_CELSIUS -def test_default_target_temperature(device): - """Test default setting of target temperature.""" - assert device.target_temperature == 1 - device.values.primary.data = 0 - value_changed(device.values.primary) - assert device.target_temperature == 5 # Current Temperature - - def test_data_lists(device): """Test data lists from zwave value items.""" assert device.fan_modes == [3, 4, 5] @@ -225,7 +327,7 @@ def test_data_lists(device): HVAC_MODE_HEAT_COOL, ] assert device.preset_modes == [] - device.values.mode = None + device.values.primary = None assert device.preset_modes == [] @@ -234,71 +336,126 @@ def test_data_lists_mapping(device_mapping): device = device_mapping assert device.hvac_modes == ["off", "cool", "heat", "heat_cool"] assert device.preset_modes == ["boost", "none"] - device.values.mode = None + device.values.primary = None assert device.preset_modes == [] def test_target_value_set(device): """Test values changed for climate device.""" - assert device.values.primary.data == 1 + assert device.values.setpoint_heating.data == 1 + assert device.values.setpoint_cooling.data == 10 device.set_temperature() - assert device.values.primary.data == 1 + assert device.values.setpoint_heating.data == 1 + assert device.values.setpoint_cooling.data == 10 device.set_temperature(**{ATTR_TEMPERATURE: 2}) - assert device.values.primary.data == 2 + assert device.values.setpoint_heating.data == 2 + assert device.values.setpoint_cooling.data == 10 + device.set_hvac_mode(HVAC_MODE_COOL) + value_changed(device.values.primary) + assert device.values.setpoint_heating.data == 2 + assert device.values.setpoint_cooling.data == 10 + device.set_temperature(**{ATTR_TEMPERATURE: 9}) + assert device.values.setpoint_heating.data == 2 + assert device.values.setpoint_cooling.data == 9 + + +def test_target_value_set_range(device_heat_cool_range): + """Test values changed for climate device.""" + device = device_heat_cool_range + assert device.values.setpoint_heating.data == 1 + assert device.values.setpoint_cooling.data == 10 + device.set_temperature() + assert device.values.setpoint_heating.data == 1 + assert device.values.setpoint_cooling.data == 10 + device.set_temperature(**{ATTR_TARGET_TEMP_LOW: 2}) + assert device.values.setpoint_heating.data == 2 + assert device.values.setpoint_cooling.data == 10 + device.set_temperature(**{ATTR_TARGET_TEMP_HIGH: 9}) + assert device.values.setpoint_heating.data == 2 + assert device.values.setpoint_cooling.data == 9 + device.set_temperature(**{ATTR_TARGET_TEMP_LOW: 3, ATTR_TARGET_TEMP_HIGH: 8}) + assert device.values.setpoint_heating.data == 3 + assert device.values.setpoint_cooling.data == 8 + + +def test_target_value_set_range_away(device_heat_cool_away): + """Test values changed for climate device.""" + device = device_heat_cool_away + assert device.values.setpoint_heating.data == 2 + assert device.values.setpoint_cooling.data == 9 + assert device.values.setpoint_away_heating.data == 1 + assert device.values.setpoint_away_cooling.data == 10 + device.set_preset_mode(PRESET_AWAY) + device.set_temperature(**{ATTR_TARGET_TEMP_LOW: 0, ATTR_TARGET_TEMP_HIGH: 11}) + assert device.values.setpoint_heating.data == 2 + assert device.values.setpoint_cooling.data == 9 + assert device.values.setpoint_away_heating.data == 0 + assert device.values.setpoint_away_cooling.data == 11 + + +def test_target_value_set_eco(device_heat_eco): + """Test values changed for climate device.""" + device = device_heat_eco + assert device.values.setpoint_heating.data == 2 + assert device.values.setpoint_eco_heating.data == 1 + device.set_preset_mode("heat econ") + device.set_temperature(**{ATTR_TEMPERATURE: 0}) + assert device.values.setpoint_heating.data == 2 + assert device.values.setpoint_eco_heating.data == 0 def test_operation_value_set(device): """Test values changed for climate device.""" - assert device.values.mode.data == HVAC_MODE_HEAT + assert device.values.primary.data == HVAC_MODE_HEAT device.set_hvac_mode(HVAC_MODE_COOL) - assert device.values.mode.data == HVAC_MODE_COOL + assert device.values.primary.data == HVAC_MODE_COOL device.set_preset_mode(PRESET_ECO) - assert device.values.mode.data == PRESET_ECO + assert device.values.primary.data == PRESET_ECO device.set_preset_mode(PRESET_NONE) - assert device.values.mode.data == HVAC_MODE_HEAT_COOL - device.values.mode = None + assert device.values.primary.data == HVAC_MODE_HEAT_COOL + device.values.primary = None device.set_hvac_mode("test_set_failes") - assert device.values.mode is None + assert device.values.primary is None device.set_preset_mode("test_set_failes") - assert device.values.mode is None + assert device.values.primary is None def test_operation_value_set_mapping(device_mapping): """Test values changed for climate device. Mapping.""" device = device_mapping - assert device.values.mode.data == "Heat" + assert device.values.primary.data == "Heat" device.set_hvac_mode(HVAC_MODE_COOL) - assert device.values.mode.data == "Cool" + assert device.values.primary.data == "Cool" device.set_hvac_mode(HVAC_MODE_OFF) - assert device.values.mode.data == "Off" + assert device.values.primary.data == "Off" device.set_preset_mode(PRESET_BOOST) - assert device.values.mode.data == "Full Power" + assert device.values.primary.data == "Full Power" device.set_preset_mode(PRESET_ECO) - assert device.values.mode.data == "eco" + assert device.values.primary.data == "eco" def test_operation_value_set_unknown(device_unknown): """Test values changed for climate device. Unknown.""" device = device_unknown - assert device.values.mode.data == "Heat" + assert device.values.primary.data == "Heat" device.set_preset_mode("Abcdefg") - assert device.values.mode.data == "Abcdefg" + assert device.values.primary.data == "Abcdefg" device.set_preset_mode(PRESET_NONE) - assert device.values.mode.data == HVAC_MODE_HEAT_COOL + assert device.values.primary.data == HVAC_MODE_HEAT_COOL def test_operation_value_set_heat_cool(device_heat_cool): """Test values changed for climate device. Heat/Cool only.""" device = device_heat_cool - assert device.values.mode.data == HVAC_MODE_HEAT + assert device.values.primary.data == HVAC_MODE_HEAT device.set_preset_mode("Heat Eco") - assert device.values.mode.data == "Heat Eco" + assert device.values.primary.data == "Heat Eco" device.set_preset_mode(PRESET_NONE) - assert device.values.mode.data == HVAC_MODE_HEAT + assert device.values.primary.data == HVAC_MODE_HEAT device.set_preset_mode("Cool Eco") - assert device.values.mode.data == "Cool Eco" + assert device.values.primary.data == "Cool Eco" device.set_preset_mode(PRESET_NONE) - assert device.values.mode.data == HVAC_MODE_COOL + assert device.values.primary.data == HVAC_MODE_COOL def test_fan_mode_value_set(device): @@ -314,11 +471,81 @@ def test_fan_mode_value_set(device): def test_target_value_changed(device): """Test values changed for climate device.""" assert device.target_temperature == 1 - device.values.primary.data = 2 + device.values.setpoint_heating.data = 2 + value_changed(device.values.setpoint_heating) + assert device.target_temperature == 2 + device.values.primary.data = HVAC_MODE_COOL + value_changed(device.values.primary) + assert device.target_temperature == 10 + device.values.setpoint_cooling.data = 9 + value_changed(device.values.setpoint_cooling) + assert device.target_temperature == 9 + + +def test_target_range_changed(device_heat_cool_range): + """Test values changed for climate device.""" + device = device_heat_cool_range + assert device.target_temperature_low == 1 + assert device.target_temperature_high == 10 + device.values.setpoint_heating.data = 2 + value_changed(device.values.setpoint_heating) + assert device.target_temperature_low == 2 + assert device.target_temperature_high == 10 + device.values.setpoint_cooling.data = 9 + value_changed(device.values.setpoint_cooling) + assert device.target_temperature_low == 2 + assert device.target_temperature_high == 9 + + +def test_target_changed_preset_range(device_heat_cool_away): + """Test values changed for climate device.""" + device = device_heat_cool_away + assert device.target_temperature_low == 2 + assert device.target_temperature_high == 9 + device.values.primary.data = PRESET_AWAY + value_changed(device.values.primary) + assert device.target_temperature_low == 1 + assert device.target_temperature_high == 10 + device.values.setpoint_away_heating.data = 0 + value_changed(device.values.setpoint_away_heating) + device.values.setpoint_away_cooling.data = 11 + value_changed(device.values.setpoint_away_cooling) + assert device.target_temperature_low == 0 + assert device.target_temperature_high == 11 + device.values.primary.data = HVAC_MODE_HEAT_COOL + value_changed(device.values.primary) + assert device.target_temperature_low == 2 + assert device.target_temperature_high == 9 + + +def test_target_changed_eco(device_heat_eco): + """Test values changed for climate device.""" + device = device_heat_eco + assert device.target_temperature == 2 + device.values.primary.data = "heat econ" + value_changed(device.values.primary) + assert device.target_temperature == 1 + device.values.setpoint_eco_heating.data = 0 + value_changed(device.values.setpoint_eco_heating) + assert device.target_temperature == 0 + device.values.primary.data = HVAC_MODE_HEAT value_changed(device.values.primary) assert device.target_temperature == 2 +def test_target_changed_with_mode(device): + """Test values changed for climate device.""" + assert device.hvac_mode == HVAC_MODE_HEAT + assert device.target_temperature == 1 + device.values.primary.data = HVAC_MODE_COOL + value_changed(device.values.primary) + assert device.target_temperature == 10 + device.values.primary.data = HVAC_MODE_HEAT_COOL + value_changed(device.values.primary) + assert device.target_temperature_low == 1 + assert device.target_temperature_high == 10 + + def test_temperature_value_changed(device): """Test values changed for climate device.""" assert device.current_temperature == 5 @@ -331,15 +558,15 @@ def test_operation_value_changed(device): """Test values changed for climate device.""" assert device.hvac_mode == HVAC_MODE_HEAT assert device.preset_mode == PRESET_NONE - device.values.mode.data = HVAC_MODE_COOL - value_changed(device.values.mode) + device.values.primary.data = HVAC_MODE_COOL + value_changed(device.values.primary) assert device.hvac_mode == HVAC_MODE_COOL assert device.preset_mode == PRESET_NONE - device.values.mode.data = HVAC_MODE_OFF - value_changed(device.values.mode) + device.values.primary.data = HVAC_MODE_OFF + value_changed(device.values.primary) assert device.hvac_mode == HVAC_MODE_OFF assert device.preset_mode == PRESET_NONE - device.values.mode = None + device.values.primary = None assert device.hvac_mode == HVAC_MODE_HEAT_COOL assert device.preset_mode == PRESET_NONE @@ -349,8 +576,8 @@ def test_operation_value_changed_preset(device_mapping): device = device_mapping assert device.hvac_mode == HVAC_MODE_HEAT assert device.preset_mode == PRESET_NONE - device.values.mode.data = PRESET_ECO - value_changed(device.values.mode) + device.values.primary.data = PRESET_ECO + value_changed(device.values.primary) assert device.hvac_mode == HVAC_MODE_HEAT_COOL assert device.preset_mode == PRESET_ECO @@ -360,12 +587,12 @@ def test_operation_value_changed_mapping(device_mapping): device = device_mapping assert device.hvac_mode == HVAC_MODE_HEAT assert device.preset_mode == PRESET_NONE - device.values.mode.data = "Off" - value_changed(device.values.mode) + device.values.primary.data = "Off" + value_changed(device.values.primary) assert device.hvac_mode == HVAC_MODE_OFF assert device.preset_mode == PRESET_NONE - device.values.mode.data = "Cool" - value_changed(device.values.mode) + device.values.primary.data = "Cool" + value_changed(device.values.primary) assert device.hvac_mode == HVAC_MODE_COOL assert device.preset_mode == PRESET_NONE @@ -375,11 +602,11 @@ def test_operation_value_changed_mapping_preset(device_mapping): device = device_mapping assert device.hvac_mode == HVAC_MODE_HEAT assert device.preset_mode == PRESET_NONE - device.values.mode.data = "Full Power" - value_changed(device.values.mode) + device.values.primary.data = "Full Power" + value_changed(device.values.primary) assert device.hvac_mode == HVAC_MODE_HEAT_COOL assert device.preset_mode == PRESET_BOOST - device.values.mode = None + device.values.primary = None assert device.hvac_mode == HVAC_MODE_HEAT_COOL assert device.preset_mode == PRESET_NONE @@ -389,8 +616,8 @@ def test_operation_value_changed_unknown(device_unknown): device = device_unknown assert device.hvac_mode == HVAC_MODE_HEAT assert device.preset_mode == PRESET_NONE - device.values.mode.data = "Abcdefg" - value_changed(device.values.mode) + device.values.primary.data = "Abcdefg" + value_changed(device.values.primary) assert device.hvac_mode == HVAC_MODE_HEAT_COOL assert device.preset_mode == "Abcdefg" @@ -400,12 +627,12 @@ def test_operation_value_changed_heat_cool(device_heat_cool): device = device_heat_cool assert device.hvac_mode == HVAC_MODE_HEAT assert device.preset_mode == PRESET_NONE - device.values.mode.data = "Cool Eco" - value_changed(device.values.mode) + device.values.primary.data = "Cool Eco" + value_changed(device.values.primary) assert device.hvac_mode == HVAC_MODE_COOL assert device.preset_mode == "Cool Eco" - device.values.mode.data = "Heat Eco" - value_changed(device.values.mode) + device.values.primary.data = "Heat Eco" + value_changed(device.values.primary) assert device.hvac_mode == HVAC_MODE_HEAT assert device.preset_mode == "Heat Eco" diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index 1de69249bfe..7038d6b6114 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -574,18 +574,33 @@ async def test_value_discovery_existing_entity(hass, mock_openzwave): assert len(mock_receivers) == 1 node = MockNode(node_id=11, generic=const.GENERIC_TYPE_THERMOSTAT) - setpoint = MockValue( + thermostat_mode = MockValue( + data="Heat", + data_items=["Off", "Heat"], + node=node, + command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, + genre=const.GENRE_USER, + ) + setpoint_heating = MockValue( data=22.0, node=node, - index=12, - instance=13, command_class=const.COMMAND_CLASS_THERMOSTAT_SETPOINT, + index=1, genre=const.GENRE_USER, - units="C", ) - hass.async_add_job(mock_receivers[0], node, setpoint) + + hass.async_add_job(mock_receivers[0], node, thermostat_mode) await hass.async_block_till_done() + def mock_update(self): + self.hass.add_job(self.async_update_ha_state) + + with patch.object( + zwave.node_entity.ZWaveBaseEntity, "maybe_schedule_update", new=mock_update + ): + hass.async_add_job(mock_receivers[0], node, setpoint_heating) + await hass.async_block_till_done() + assert ( hass.states.get("climate.mock_node_mock_value").attributes["temperature"] == 22.0 @@ -597,9 +612,6 @@ async def test_value_discovery_existing_entity(hass, mock_openzwave): is None ) - def mock_update(self): - self.hass.add_job(self.async_update_ha_state) - with patch.object( zwave.node_entity.ZWaveBaseEntity, "maybe_schedule_update", new=mock_update ): @@ -607,7 +619,6 @@ async def test_value_discovery_existing_entity(hass, mock_openzwave): data=23.5, node=node, index=1, - instance=13, command_class=const.COMMAND_CLASS_SENSOR_MULTILEVEL, genre=const.GENRE_USER, units="C",