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 defaultpull/37635/head
parent
2b37cbe079
commit
cbccf011e7
|
@ -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."""
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue