Ozw climate fixes (#37560)

* fix presets and mode conversion

* fix mapping issues in ozw climate

* build mapping table in advance

* Copying a dict to a list copies the keys by default
pull/37635/head
Marcel van der Veldt 2020-07-07 22:20:57 +02:00 committed by GitHub
parent 2b37cbe079
commit cbccf011e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 51 additions and 34 deletions

View File

@ -12,7 +12,6 @@ from homeassistant.components.climate.const import (
CURRENT_HVAC_HEAT, CURRENT_HVAC_HEAT,
CURRENT_HVAC_IDLE, CURRENT_HVAC_IDLE,
CURRENT_HVAC_OFF, CURRENT_HVAC_OFF,
HVAC_MODE_AUTO,
HVAC_MODE_COOL, HVAC_MODE_COOL,
HVAC_MODE_DRY, HVAC_MODE_DRY,
HVAC_MODE_FAN_ONLY, HVAC_MODE_FAN_ONLY,
@ -65,6 +64,16 @@ class ThermostatMode(IntEnum):
MANUFACTURER_SPECIFIC = 31 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 = { MODE_SETPOINT_MAPPINGS = {
ThermostatMode.OFF: (), ThermostatMode.OFF: (),
ThermostatMode.HEAT: ("setpoint_heating",), ThermostatMode.HEAT: ("setpoint_heating",),
@ -99,11 +108,14 @@ HVAC_CURRENT_MAPPINGS = {
# Map Z-Wave HVAC Mode to Home Assistant value # 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 = { ZW_HVAC_MODE_MAPPINGS = {
ThermostatMode.OFF: HVAC_MODE_OFF, ThermostatMode.OFF: HVAC_MODE_OFF,
ThermostatMode.HEAT: HVAC_MODE_HEAT, ThermostatMode.HEAT: HVAC_MODE_HEAT,
ThermostatMode.COOL: HVAC_MODE_COOL, 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.AUXILIARY: HVAC_MODE_HEAT,
ThermostatMode.FAN: HVAC_MODE_FAN_ONLY, ThermostatMode.FAN: HVAC_MODE_FAN_ONLY,
ThermostatMode.FURNANCE: HVAC_MODE_HEAT, ThermostatMode.FURNANCE: HVAC_MODE_HEAT,
@ -120,7 +132,6 @@ HVAC_MODE_ZW_MAPPINGS = {
HVAC_MODE_OFF: ThermostatMode.OFF, HVAC_MODE_OFF: ThermostatMode.OFF,
HVAC_MODE_HEAT: ThermostatMode.HEAT, HVAC_MODE_HEAT: ThermostatMode.HEAT,
HVAC_MODE_COOL: ThermostatMode.COOL, HVAC_MODE_COOL: ThermostatMode.COOL,
HVAC_MODE_AUTO: ThermostatMode.AUTO,
HVAC_MODE_FAN_ONLY: ThermostatMode.FAN, HVAC_MODE_FAN_ONLY: ThermostatMode.FAN,
HVAC_MODE_DRY: ThermostatMode.DRY, HVAC_MODE_DRY: ThermostatMode.DRY,
HVAC_MODE_HEAT_COOL: ThermostatMode.AUTO_CHANGE_OVER, HVAC_MODE_HEAT_COOL: ThermostatMode.AUTO_CHANGE_OVER,
@ -148,12 +159,16 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity):
def __init__(self, values): def __init__(self, values):
"""Initialize the entity.""" """Initialize the entity."""
super().__init__(values) 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 @callback
def on_value_update(self): 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() self._current_mode_setpoint_values = self._get_current_mode_setpoint_values()
if not self._hvac_modes:
self._set_modes_and_presets()
@property @property
def hvac_mode(self): def hvac_mode(self):
@ -161,21 +176,13 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity):
if not self.values.mode: if not self.values.mode:
return None return None
return ZW_HVAC_MODE_MAPPINGS.get( 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 @property
def hvac_modes(self): def hvac_modes(self):
"""Return the list of available hvac operation modes.""" """Return the list of available hvac operation modes."""
if not self.values.mode: return list(self._hvac_modes)
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
@property @property
def fan_mode(self): def fan_mode(self):
@ -212,20 +219,15 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity):
@property @property
def preset_mode(self): def preset_mode(self):
"""Return preset operation ie. eco, away.""" """Return preset operation ie. eco, away."""
# Z-Wave uses mode-values > 10 for presets # A Zwave mode that can not be translated to a hass mode is considered a preset
if self.values.mode.value[VALUE_SELECTED_ID] > 10: if self.values.mode.value[VALUE_SELECTED_ID] not in MODES_LIST:
return self.values.mode.value[VALUE_SELECTED_LABEL] return self.values.mode.value[VALUE_SELECTED_LABEL]
return PRESET_NONE return PRESET_NONE
@property @property
def preset_modes(self): def preset_modes(self):
"""Return the list of available preset operation modes.""" """Return the list of available preset operation modes."""
# Z-Wave uses mode-values > 10 for presets return list(self._hvac_presets)
return [PRESET_NONE] + [
val[VALUE_LABEL]
for val in self.values.mode.value[VALUE_LIST]
if val[VALUE_ID] > 10
]
@property @property
def target_temperature(self): def target_temperature(self):
@ -272,12 +274,10 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity):
async def async_set_hvac_mode(self, hvac_mode): async def async_set_hvac_mode(self, hvac_mode):
"""Set new target hvac mode.""" """Set new target hvac mode."""
if not self.values.mode: hvac_mode_value = self._hvac_modes.get(hvac_mode)
return if not hvac_mode_value:
if hvac_mode not in self.hvac_modes:
_LOGGER.warning("Received an invalid hvac mode: %s", hvac_mode) _LOGGER.warning("Received an invalid hvac mode: %s", hvac_mode)
return return
hvac_mode_value = HVAC_MODE_ZW_MAPPINGS.get(hvac_mode)
self.values.mode.send_value(hvac_mode_value) self.values.mode.send_value(hvac_mode_value)
async def async_set_preset_mode(self, preset_mode): 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 # try to restore to the (translated) main hvac mode
await self.async_set_hvac_mode(self.hvac_mode) await self.async_set_hvac_mode(self.hvac_mode)
return return
preset_mode_value = _get_list_id( preset_mode_value = self._hvac_presets.get(preset_mode)
self.values.mode.value[VALUE_LIST], preset_mode
)
if preset_mode_value is None: if preset_mode_value is None:
_LOGGER.warning("Received an invalid preset mode: %s", preset_mode) _LOGGER.warning("Received an invalid preset mode: %s", preset_mode)
return return
@ -331,6 +329,25 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity):
if getattr(self.values, value_name, None) 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): def _get_list_id(value_lst, value_lbl):
"""Return the id for the value in the list.""" """Return the id for the value in the list."""

View File

@ -10,9 +10,9 @@ from homeassistant.components.climate.const import (
ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_HIGH,
ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_LOW,
CURRENT_HVAC_IDLE, CURRENT_HVAC_IDLE,
HVAC_MODE_AUTO,
HVAC_MODE_COOL, HVAC_MODE_COOL,
HVAC_MODE_HEAT, HVAC_MODE_HEAT,
HVAC_MODE_HEAT_COOL,
HVAC_MODE_OFF, HVAC_MODE_OFF,
) )
@ -32,7 +32,7 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog):
HVAC_MODE_OFF, HVAC_MODE_OFF,
HVAC_MODE_HEAT, HVAC_MODE_HEAT,
HVAC_MODE_COOL, HVAC_MODE_COOL,
HVAC_MODE_AUTO, HVAC_MODE_HEAT_COOL,
] ]
assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE
assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 23.1 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( await hass.services.async_call(
"climate", "climate",
"set_hvac_mode", "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, blocking=True,
) )
assert len(sent_messages) == 2 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() await hass.async_block_till_done()
state = hass.states.get("climate.ct32_thermostat_mode") state = hass.states.get("climate.ct32_thermostat_mode")
assert state is not None 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.get(ATTR_TEMPERATURE) is None
assert state.attributes[ATTR_TARGET_TEMP_LOW] == 21.1 assert state.attributes[ATTR_TARGET_TEMP_LOW] == 21.1
assert state.attributes[ATTR_TARGET_TEMP_HIGH] == 25.6 assert state.attributes[ATTR_TARGET_TEMP_HIGH] == 25.6