diff --git a/homeassistant/components/ozw/climate.py b/homeassistant/components/ozw/climate.py index 92887069a84..8a524805b57 100644 --- a/homeassistant/components/ozw/climate.py +++ b/homeassistant/components/ozw/climate.py @@ -12,7 +12,6 @@ from homeassistant.components.climate.const import ( CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, CURRENT_HVAC_OFF, - HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, @@ -65,6 +64,16 @@ class ThermostatMode(IntEnum): MANUFACTURER_SPECIFIC = 31 +# In Z-Wave the modes and presets are both in ThermostatMode. +# This list contains thermostatmodes we should consider a mode only +MODES_LIST = [ + ThermostatMode.OFF, + ThermostatMode.HEAT, + ThermostatMode.COOL, + ThermostatMode.AUTO, + ThermostatMode.AUTO_CHANGE_OVER, +] + MODE_SETPOINT_MAPPINGS = { ThermostatMode.OFF: (), ThermostatMode.HEAT: ("setpoint_heating",), @@ -99,11 +108,14 @@ HVAC_CURRENT_MAPPINGS = { # Map Z-Wave HVAC Mode to Home Assistant value +# Note: We treat "auto" as "heat_cool" as most Z-Wave devices +# report auto_changeover as auto without schedule support. ZW_HVAC_MODE_MAPPINGS = { ThermostatMode.OFF: HVAC_MODE_OFF, ThermostatMode.HEAT: HVAC_MODE_HEAT, ThermostatMode.COOL: HVAC_MODE_COOL, - ThermostatMode.AUTO: HVAC_MODE_AUTO, + # Z-Wave auto mode is actually heat/cool in the hass world + ThermostatMode.AUTO: HVAC_MODE_HEAT_COOL, ThermostatMode.AUXILIARY: HVAC_MODE_HEAT, ThermostatMode.FAN: HVAC_MODE_FAN_ONLY, ThermostatMode.FURNANCE: HVAC_MODE_HEAT, @@ -120,7 +132,6 @@ HVAC_MODE_ZW_MAPPINGS = { HVAC_MODE_OFF: ThermostatMode.OFF, HVAC_MODE_HEAT: ThermostatMode.HEAT, HVAC_MODE_COOL: ThermostatMode.COOL, - HVAC_MODE_AUTO: ThermostatMode.AUTO, HVAC_MODE_FAN_ONLY: ThermostatMode.FAN, HVAC_MODE_DRY: ThermostatMode.DRY, HVAC_MODE_HEAT_COOL: ThermostatMode.AUTO_CHANGE_OVER, @@ -148,12 +159,16 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity): def __init__(self, values): """Initialize the entity.""" super().__init__(values) - self._current_mode_setpoint_values = self._get_current_mode_setpoint_values() + self._hvac_modes = {} + self._hvac_presets = {} + self.on_value_update() @callback def on_value_update(self): - """Call when the underlying value(s) is added or updated.""" + """Call when the underlying values object changes.""" self._current_mode_setpoint_values = self._get_current_mode_setpoint_values() + if not self._hvac_modes: + self._set_modes_and_presets() @property def hvac_mode(self): @@ -161,21 +176,13 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity): if not self.values.mode: return None return ZW_HVAC_MODE_MAPPINGS.get( - self.values.mode.value[VALUE_SELECTED_ID], HVAC_MODE_AUTO + self.values.mode.value[VALUE_SELECTED_ID], HVAC_MODE_HEAT_COOL ) @property def hvac_modes(self): """Return the list of available hvac operation modes.""" - if not self.values.mode: - return [] - # Z-Wave uses one list for both modes and presets. Extract the unique modes - all_modes = [] - for val in self.values.mode.value[VALUE_LIST]: - hass_mode = ZW_HVAC_MODE_MAPPINGS.get(val[VALUE_ID]) - if hass_mode and hass_mode not in all_modes: - all_modes.append(hass_mode) - return all_modes + return list(self._hvac_modes) @property def fan_mode(self): @@ -212,20 +219,15 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity): @property def preset_mode(self): """Return preset operation ie. eco, away.""" - # Z-Wave uses mode-values > 10 for presets - if self.values.mode.value[VALUE_SELECTED_ID] > 10: + # A Zwave mode that can not be translated to a hass mode is considered a preset + if self.values.mode.value[VALUE_SELECTED_ID] not in MODES_LIST: return self.values.mode.value[VALUE_SELECTED_LABEL] return PRESET_NONE @property def preset_modes(self): """Return the list of available preset operation modes.""" - # Z-Wave uses mode-values > 10 for presets - return [PRESET_NONE] + [ - val[VALUE_LABEL] - for val in self.values.mode.value[VALUE_LIST] - if val[VALUE_ID] > 10 - ] + return list(self._hvac_presets) @property def target_temperature(self): @@ -272,12 +274,10 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity): async def async_set_hvac_mode(self, hvac_mode): """Set new target hvac mode.""" - if not self.values.mode: - return - if hvac_mode not in self.hvac_modes: + hvac_mode_value = self._hvac_modes.get(hvac_mode) + if not hvac_mode_value: _LOGGER.warning("Received an invalid hvac mode: %s", hvac_mode) return - hvac_mode_value = HVAC_MODE_ZW_MAPPINGS.get(hvac_mode) self.values.mode.send_value(hvac_mode_value) async def async_set_preset_mode(self, preset_mode): @@ -286,9 +286,7 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity): # try to restore to the (translated) main hvac mode await self.async_set_hvac_mode(self.hvac_mode) return - preset_mode_value = _get_list_id( - self.values.mode.value[VALUE_LIST], preset_mode - ) + preset_mode_value = self._hvac_presets.get(preset_mode) if preset_mode_value is None: _LOGGER.warning("Received an invalid preset mode: %s", preset_mode) return @@ -331,6 +329,25 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity): if getattr(self.values, value_name, None) ) + def _set_modes_and_presets(self): + """Convert Z-Wave Thermostat modes into Home Assistant modes and presets.""" + if not self.values.mode: + return + all_modes = {} + all_presets = {PRESET_NONE: None} + # Z-Wave uses one list for both modes and presets. + # Iterate over all Z-Wave ThermostatModes and extract the hvac modes and presets. + for val in self.values.mode.value[VALUE_LIST]: + if val[VALUE_ID] in MODES_LIST: + # treat value as hvac mode + hass_mode = ZW_HVAC_MODE_MAPPINGS.get(val[VALUE_ID]) + all_modes[hass_mode] = val[VALUE_ID] + else: + # treat value as hvac preset + all_presets[val[VALUE_LABEL]] = val[VALUE_ID] + self._hvac_modes = all_modes + self._hvac_presets = all_presets + def _get_list_id(value_lst, value_lbl): """Return the id for the value in the list.""" diff --git a/tests/components/ozw/test_climate.py b/tests/components/ozw/test_climate.py index c0f4e362735..13691b49f65 100644 --- a/tests/components/ozw/test_climate.py +++ b/tests/components/ozw/test_climate.py @@ -10,9 +10,9 @@ from homeassistant.components.climate.const import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, CURRENT_HVAC_IDLE, - HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, ) @@ -32,7 +32,7 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog): HVAC_MODE_OFF, HVAC_MODE_HEAT, HVAC_MODE_COOL, - HVAC_MODE_AUTO, + HVAC_MODE_HEAT_COOL, ] assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 23.1 @@ -60,7 +60,7 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog): await hass.services.async_call( "climate", "set_hvac_mode", - {"entity_id": "climate.ct32_thermostat_mode", "hvac_mode": HVAC_MODE_AUTO}, + {"entity_id": "climate.ct32_thermostat_mode", "hvac_mode": HVAC_MODE_HEAT_COOL}, blocking=True, ) assert len(sent_messages) == 2 @@ -106,7 +106,7 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog): await hass.async_block_till_done() state = hass.states.get("climate.ct32_thermostat_mode") assert state is not None - assert state.state == HVAC_MODE_AUTO + assert state.state == HVAC_MODE_HEAT_COOL assert state.attributes.get(ATTR_TEMPERATURE) is None assert state.attributes[ATTR_TARGET_TEMP_LOW] == 21.1 assert state.attributes[ATTR_TARGET_TEMP_HIGH] == 25.6