Add climate platform to ozw (#35566)
parent
f5a326c51e
commit
5bef1c223d
|
@ -0,0 +1,339 @@
|
|||
"""Support for Z-Wave climate devices."""
|
||||
from enum import IntEnum
|
||||
import logging
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN, ClimateEntity
|
||||
from homeassistant.components.climate.const import (
|
||||
ATTR_TARGET_TEMP_HIGH,
|
||||
ATTR_TARGET_TEMP_LOW,
|
||||
CURRENT_HVAC_COOL,
|
||||
CURRENT_HVAC_FAN,
|
||||
CURRENT_HVAC_HEAT,
|
||||
CURRENT_HVAC_IDLE,
|
||||
CURRENT_HVAC_OFF,
|
||||
HVAC_MODE_AUTO,
|
||||
HVAC_MODE_COOL,
|
||||
HVAC_MODE_DRY,
|
||||
HVAC_MODE_FAN_ONLY,
|
||||
HVAC_MODE_HEAT,
|
||||
HVAC_MODE_HEAT_COOL,
|
||||
HVAC_MODE_OFF,
|
||||
PRESET_NONE,
|
||||
SUPPORT_FAN_MODE,
|
||||
SUPPORT_PRESET_MODE,
|
||||
SUPPORT_TARGET_TEMPERATURE,
|
||||
SUPPORT_TARGET_TEMPERATURE_RANGE,
|
||||
)
|
||||
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
from .const import DATA_UNSUBSCRIBE, DOMAIN
|
||||
from .entity import ZWaveDeviceEntity
|
||||
|
||||
VALUE_LIST = "List"
|
||||
VALUE_ID = "Value"
|
||||
VALUE_LABEL = "Label"
|
||||
VALUE_SELECTED_ID = "Selected_id"
|
||||
VALUE_SELECTED_LABEL = "Selected"
|
||||
|
||||
ATTR_FAN_ACTION = "fan_action"
|
||||
ATTR_VALVE_POSITION = "valve_position"
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ThermostatMode(IntEnum):
|
||||
"""Enum with all (known/used) Z-Wave ThermostatModes."""
|
||||
|
||||
# https://github.com/OpenZWave/open-zwave/blob/master/cpp/src/command_classes/ThermostatMode.cpp
|
||||
OFF = 0
|
||||
HEAT = 1
|
||||
COOL = 2
|
||||
AUTO = 3
|
||||
AUXILIARY = 4
|
||||
RESUME_ON = 5
|
||||
FAN = 6
|
||||
FURNANCE = 7
|
||||
DRY = 8
|
||||
MOIST = 9
|
||||
AUTO_CHANGE_OVER = 10
|
||||
HEATING_ECON = 11
|
||||
COOLING_ECON = 12
|
||||
AWAY = 13
|
||||
FULL_POWER = 15
|
||||
MANUFACTURER_SPECIFIC = 31
|
||||
|
||||
|
||||
MODE_SETPOINT_MAPPINGS = {
|
||||
ThermostatMode.OFF: (),
|
||||
ThermostatMode.HEAT: ("setpoint_heating",),
|
||||
ThermostatMode.COOL: ("setpoint_cooling",),
|
||||
ThermostatMode.AUTO: ("setpoint_heating", "setpoint_cooling"),
|
||||
ThermostatMode.AUXILIARY: ("setpoint_heating",),
|
||||
ThermostatMode.FURNANCE: ("setpoint_furnace",),
|
||||
ThermostatMode.DRY: ("setpoint_dry_air",),
|
||||
ThermostatMode.MOIST: ("setpoint_moist_air",),
|
||||
ThermostatMode.AUTO_CHANGE_OVER: ("setpoint_auto_changeover",),
|
||||
ThermostatMode.HEATING_ECON: ("setpoint_eco_heating",),
|
||||
ThermostatMode.COOLING_ECON: ("setpoint_eco_cooling",),
|
||||
ThermostatMode.AWAY: ("setpoint_away_heating", "setpoint_away_cooling"),
|
||||
ThermostatMode.FULL_POWER: ("setpoint_full_power",),
|
||||
}
|
||||
|
||||
|
||||
# strings, OZW and/or qt-ozw does not send numeric values
|
||||
# https://github.com/OpenZWave/open-zwave/blob/master/cpp/src/command_classes/ThermostatOperatingState.cpp
|
||||
HVAC_CURRENT_MAPPINGS = {
|
||||
"idle": CURRENT_HVAC_IDLE,
|
||||
"heat": CURRENT_HVAC_HEAT,
|
||||
"pending heat": CURRENT_HVAC_IDLE,
|
||||
"heating": CURRENT_HVAC_HEAT,
|
||||
"cool": CURRENT_HVAC_COOL,
|
||||
"pending cool": CURRENT_HVAC_IDLE,
|
||||
"cooling": CURRENT_HVAC_COOL,
|
||||
"fan only": CURRENT_HVAC_FAN,
|
||||
"vent / economiser": CURRENT_HVAC_FAN,
|
||||
"off": CURRENT_HVAC_OFF,
|
||||
}
|
||||
|
||||
|
||||
# Map Z-Wave HVAC Mode to Home Assistant value
|
||||
ZW_HVAC_MODE_MAPPINGS = {
|
||||
ThermostatMode.OFF: HVAC_MODE_OFF,
|
||||
ThermostatMode.HEAT: HVAC_MODE_HEAT,
|
||||
ThermostatMode.COOL: HVAC_MODE_COOL,
|
||||
ThermostatMode.AUTO: HVAC_MODE_AUTO,
|
||||
ThermostatMode.AUXILIARY: HVAC_MODE_HEAT,
|
||||
ThermostatMode.FAN: HVAC_MODE_FAN_ONLY,
|
||||
ThermostatMode.FURNANCE: HVAC_MODE_HEAT,
|
||||
ThermostatMode.DRY: HVAC_MODE_DRY,
|
||||
ThermostatMode.AUTO_CHANGE_OVER: HVAC_MODE_HEAT_COOL,
|
||||
ThermostatMode.HEATING_ECON: HVAC_MODE_HEAT,
|
||||
ThermostatMode.COOLING_ECON: HVAC_MODE_COOL,
|
||||
ThermostatMode.AWAY: HVAC_MODE_HEAT_COOL,
|
||||
ThermostatMode.FULL_POWER: HVAC_MODE_HEAT,
|
||||
}
|
||||
|
||||
# Map Home Assistant HVAC Mode to Z-Wave value
|
||||
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,
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up Z-Wave Climate from Config Entry."""
|
||||
|
||||
@callback
|
||||
def async_add_climate(values):
|
||||
"""Add Z-Wave Climate."""
|
||||
async_add_entities([ZWaveClimateEntity(values)])
|
||||
|
||||
hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append(
|
||||
async_dispatcher_connect(
|
||||
hass, f"{DOMAIN}_new_{CLIMATE_DOMAIN}", async_add_climate
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity):
|
||||
"""Representation of a Z-Wave Climate device."""
|
||||
|
||||
def __init__(self, values):
|
||||
"""Initialize the entity."""
|
||||
super().__init__(values)
|
||||
self._current_mode_setpoint_values = self._get_current_mode_setpoint_values()
|
||||
|
||||
@callback
|
||||
def on_value_update(self):
|
||||
"""Call when the underlying value(s) is added or updated."""
|
||||
self._current_mode_setpoint_values = self._get_current_mode_setpoint_values()
|
||||
|
||||
@property
|
||||
def hvac_mode(self):
|
||||
"""Return hvac operation ie. heat, cool mode."""
|
||||
if not self.values.mode:
|
||||
return None
|
||||
return ZW_HVAC_MODE_MAPPINGS.get(
|
||||
self.values.mode.value[VALUE_SELECTED_ID], HVAC_MODE_AUTO
|
||||
)
|
||||
|
||||
@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
|
||||
|
||||
@property
|
||||
def fan_mode(self):
|
||||
"""Return the fan speed set."""
|
||||
return self.values.fan_mode.value[VALUE_SELECTED_LABEL]
|
||||
|
||||
@property
|
||||
def fan_modes(self):
|
||||
"""Return a list of available fan modes."""
|
||||
return [entry[VALUE_LABEL] for entry in self.values.fan_mode.value[VALUE_LIST]]
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
if self.values.temperature and self.values.temperature.units == "F":
|
||||
return TEMP_FAHRENHEIT
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
if not self.values.temperature:
|
||||
return None
|
||||
return self.values.temperature.value
|
||||
|
||||
@property
|
||||
def hvac_action(self):
|
||||
"""Return the current running hvac operation if supported."""
|
||||
if not self.values.operating_state:
|
||||
return None
|
||||
cur_state = self.values.operating_state.value.lower()
|
||||
return HVAC_CURRENT_MAPPINGS.get(cur_state)
|
||||
|
||||
@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:
|
||||
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
|
||||
]
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._current_mode_setpoint_values[0].value
|
||||
|
||||
@property
|
||||
def target_temperature_low(self) -> Optional[float]:
|
||||
"""Return the lowbound target temperature we try to reach."""
|
||||
return self._current_mode_setpoint_values[0].value
|
||||
|
||||
@property
|
||||
def target_temperature_high(self) -> Optional[float]:
|
||||
"""Return the highbound target temperature we try to reach."""
|
||||
return self._current_mode_setpoint_values[1].value
|
||||
|
||||
async def async_set_temperature(self, **kwargs):
|
||||
"""Set new target temperature.
|
||||
|
||||
Must know if single or double setpoint.
|
||||
"""
|
||||
if len(self._current_mode_setpoint_values) == 1:
|
||||
setpoint = self._current_mode_setpoint_values[0]
|
||||
target_temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
if setpoint is not None and target_temp is not None:
|
||||
setpoint.send_value(target_temp)
|
||||
elif len(self._current_mode_setpoint_values) == 2:
|
||||
(setpoint_low, setpoint_high) = self._current_mode_setpoint_values
|
||||
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:
|
||||
setpoint_low.send_value(target_temp_low)
|
||||
if setpoint_high is not None and target_temp_high is not None:
|
||||
setpoint_high.send_value(target_temp_high)
|
||||
|
||||
async def async_set_fan_mode(self, fan_mode):
|
||||
"""Set new target fan mode."""
|
||||
# get id for this fan_mode
|
||||
fan_mode_value = _get_list_id(self.values.fan_mode.value[VALUE_LIST], fan_mode)
|
||||
if fan_mode_value is None:
|
||||
_LOGGER.warning("Received an invalid fan mode: %s", fan_mode)
|
||||
return
|
||||
self.values.fan_mode.send_value(fan_mode_value)
|
||||
|
||||
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:
|
||||
_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):
|
||||
"""Set new target preset mode."""
|
||||
if preset_mode == PRESET_NONE:
|
||||
# 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
|
||||
)
|
||||
if preset_mode_value is None:
|
||||
_LOGGER.warning("Received an invalid preset mode: %s", preset_mode)
|
||||
return
|
||||
self.values.mode.send_value(preset_mode_value)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the optional state attributes."""
|
||||
data = super().device_state_attributes
|
||||
if self.values.fan_action:
|
||||
data[ATTR_FAN_ACTION] = self.values.fan_action.value
|
||||
if self.values.valve_position:
|
||||
data[
|
||||
ATTR_VALVE_POSITION
|
||||
] = f"{self.values.valve_position.value} {self.values.valve_position.units}"
|
||||
return data
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
support = 0
|
||||
if len(self._current_mode_setpoint_values) == 1:
|
||||
support |= SUPPORT_TARGET_TEMPERATURE
|
||||
if len(self._current_mode_setpoint_values) > 1:
|
||||
support |= SUPPORT_TARGET_TEMPERATURE_RANGE
|
||||
if self.values.fan_mode:
|
||||
support |= SUPPORT_FAN_MODE
|
||||
if self.values.mode:
|
||||
support |= SUPPORT_PRESET_MODE
|
||||
return support
|
||||
|
||||
def _get_current_mode_setpoint_values(self) -> Tuple:
|
||||
"""Return a tuple of current setpoint Z-Wave value(s)."""
|
||||
current_mode = self.values.mode.value[VALUE_SELECTED_ID]
|
||||
setpoint_names = MODE_SETPOINT_MAPPINGS.get(current_mode, ())
|
||||
# we do not want None values in our tuple so check if the value exists
|
||||
return tuple(
|
||||
getattr(self.values, value_name)
|
||||
for value_name in setpoint_names
|
||||
if getattr(self.values, value_name, None)
|
||||
)
|
||||
|
||||
|
||||
def _get_list_id(value_lst, value_lbl):
|
||||
"""Return the id for the value in the list."""
|
||||
return next(
|
||||
(val[VALUE_ID] for val in value_lst if val[VALUE_LABEL] == value_lbl), None
|
||||
)
|
|
@ -1,12 +1,19 @@
|
|||
"""Constants for the ozw integration."""
|
||||
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
||||
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
|
||||
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||
|
||||
DOMAIN = "ozw"
|
||||
DATA_UNSUBSCRIBE = "unsubscribe"
|
||||
PLATFORMS = [BINARY_SENSOR_DOMAIN, LIGHT_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN]
|
||||
PLATFORMS = [
|
||||
BINARY_SENSOR_DOMAIN,
|
||||
CLIMATE_DOMAIN,
|
||||
LIGHT_DOMAIN,
|
||||
SENSOR_DOMAIN,
|
||||
SWITCH_DOMAIN,
|
||||
]
|
||||
|
||||
# MQTT Topics
|
||||
TOPIC_OPENZWAVE = "OpenZWave"
|
||||
|
|
|
@ -30,6 +30,107 @@ DISCOVERY_SCHEMAS = (
|
|||
}
|
||||
},
|
||||
},
|
||||
{ # Z-Wave Thermostat device translates to Climate entity
|
||||
const.DISC_COMPONENT: "climate",
|
||||
const.DISC_GENERIC_DEVICE_CLASS: (
|
||||
const_ozw.GENERIC_TYPE_THERMOSTAT,
|
||||
const_ozw.GENERIC_TYPE_SENSOR_MULTILEVEL,
|
||||
),
|
||||
const.DISC_SPECIFIC_DEVICE_CLASS: (
|
||||
const_ozw.SPECIFIC_TYPE_THERMOSTAT_GENERAL,
|
||||
const_ozw.SPECIFIC_TYPE_THERMOSTAT_GENERAL_V2,
|
||||
const_ozw.SPECIFIC_TYPE_SETBACK_THERMOSTAT,
|
||||
const_ozw.SPECIFIC_TYPE_THERMOSTAT_HEATING,
|
||||
const_ozw.SPECIFIC_TYPE_SETPOINT_THERMOSTAT,
|
||||
const_ozw.SPECIFIC_TYPE_NOT_USED,
|
||||
),
|
||||
const.DISC_VALUES: {
|
||||
const.DISC_PRIMARY: {
|
||||
const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_MODE,)
|
||||
},
|
||||
"mode": {
|
||||
const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_MODE,),
|
||||
const.DISC_OPTIONAL: True,
|
||||
},
|
||||
"temperature": {
|
||||
const.DISC_COMMAND_CLASS: (CommandClass.SENSOR_MULTILEVEL,),
|
||||
const.DISC_INDEX: (1,),
|
||||
const.DISC_OPTIONAL: True,
|
||||
},
|
||||
"fan_mode": {
|
||||
const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_FAN_MODE,),
|
||||
const.DISC_OPTIONAL: True,
|
||||
},
|
||||
"operating_state": {
|
||||
const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_OPERATING_STATE,),
|
||||
const.DISC_OPTIONAL: True,
|
||||
},
|
||||
"fan_action": {
|
||||
const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_FAN_STATE,),
|
||||
const.DISC_OPTIONAL: True,
|
||||
},
|
||||
"valve_position": {
|
||||
const.DISC_COMMAND_CLASS: (CommandClass.SWITCH_MULTILEVEL,),
|
||||
const.DISC_INDEX: (0,),
|
||||
const.DISC_OPTIONAL: True,
|
||||
},
|
||||
"setpoint_heating": {
|
||||
const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,),
|
||||
const.DISC_INDEX: (1,),
|
||||
const.DISC_OPTIONAL: True,
|
||||
},
|
||||
"setpoint_cooling": {
|
||||
const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,),
|
||||
const.DISC_INDEX: (2,),
|
||||
const.DISC_OPTIONAL: True,
|
||||
},
|
||||
"setpoint_furnace": {
|
||||
const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,),
|
||||
const.DISC_INDEX: (7,),
|
||||
const.DISC_OPTIONAL: True,
|
||||
},
|
||||
"setpoint_dry_air": {
|
||||
const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,),
|
||||
const.DISC_INDEX: (8,),
|
||||
const.DISC_OPTIONAL: True,
|
||||
},
|
||||
"setpoint_moist_air": {
|
||||
const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,),
|
||||
const.DISC_INDEX: (9,),
|
||||
const.DISC_OPTIONAL: True,
|
||||
},
|
||||
"setpoint_auto_changeover": {
|
||||
const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,),
|
||||
const.DISC_INDEX: (10,),
|
||||
const.DISC_OPTIONAL: True,
|
||||
},
|
||||
"setpoint_eco_heating": {
|
||||
const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,),
|
||||
const.DISC_INDEX: (11,),
|
||||
const.DISC_OPTIONAL: True,
|
||||
},
|
||||
"setpoint_eco_cooling": {
|
||||
const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,),
|
||||
const.DISC_INDEX: (12,),
|
||||
const.DISC_OPTIONAL: True,
|
||||
},
|
||||
"setpoint_away_heating": {
|
||||
const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,),
|
||||
const.DISC_INDEX: (13,),
|
||||
const.DISC_OPTIONAL: True,
|
||||
},
|
||||
"setpoint_away_cooling": {
|
||||
const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,),
|
||||
const.DISC_INDEX: (14,),
|
||||
const.DISC_OPTIONAL: True,
|
||||
},
|
||||
"setpoint_full_power": {
|
||||
const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,),
|
||||
const.DISC_INDEX: (15,),
|
||||
const.DISC_OPTIONAL: True,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ # Light
|
||||
const.DISC_COMPONENT: "light",
|
||||
const.DISC_GENERIC_DEVICE_CLASS: (
|
||||
|
|
|
@ -21,6 +21,12 @@ def light_data_fixture():
|
|||
return load_fixture("ozw/light_network_dump.csv")
|
||||
|
||||
|
||||
@pytest.fixture(name="climate_data", scope="session")
|
||||
def climate_data_fixture():
|
||||
"""Load climate MQTT data and return it."""
|
||||
return load_fixture("ozw/climate_network_dump.csv")
|
||||
|
||||
|
||||
@pytest.fixture(name="sent_messages")
|
||||
def sent_messages_fixture():
|
||||
"""Fixture to capture sent messages."""
|
||||
|
@ -88,3 +94,14 @@ async def binary_sensor_alt_msg_fixture(hass):
|
|||
message = MQTTMessage(topic=sensor_json["topic"], payload=sensor_json["payload"])
|
||||
message.encode()
|
||||
return message
|
||||
|
||||
|
||||
@pytest.fixture(name="climate_msg")
|
||||
async def climate_msg_fixture(hass):
|
||||
"""Return a mock MQTT msg with a climate mode change message."""
|
||||
sensor_json = json.loads(
|
||||
await hass.async_add_executor_job(load_fixture, "ozw/climate.json")
|
||||
)
|
||||
message = MQTTMessage(topic=sensor_json["topic"], payload=sensor_json["payload"])
|
||||
message.encode()
|
||||
return message
|
||||
|
|
|
@ -0,0 +1,220 @@
|
|||
"""Test Z-Wave Multi-setpoint Climate entities."""
|
||||
from homeassistant.components.climate import ATTR_TEMPERATURE
|
||||
from homeassistant.components.climate.const import (
|
||||
ATTR_CURRENT_TEMPERATURE,
|
||||
ATTR_FAN_MODE,
|
||||
ATTR_FAN_MODES,
|
||||
ATTR_HVAC_ACTION,
|
||||
ATTR_HVAC_MODES,
|
||||
ATTR_PRESET_MODES,
|
||||
ATTR_TARGET_TEMP_HIGH,
|
||||
ATTR_TARGET_TEMP_LOW,
|
||||
CURRENT_HVAC_IDLE,
|
||||
HVAC_MODE_AUTO,
|
||||
HVAC_MODE_COOL,
|
||||
HVAC_MODE_HEAT,
|
||||
HVAC_MODE_OFF,
|
||||
)
|
||||
|
||||
from .common import setup_ozw
|
||||
|
||||
|
||||
async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog):
|
||||
"""Test setting up config entry."""
|
||||
receive_message = await setup_ozw(hass, fixture=climate_data)
|
||||
|
||||
# Test multi-setpoint thermostat (node 7 in dump)
|
||||
# mode is heat, this should be single setpoint
|
||||
state = hass.states.get("climate.ct32_thermostat_mode")
|
||||
assert state is not None
|
||||
assert state.state == HVAC_MODE_HEAT
|
||||
assert state.attributes[ATTR_HVAC_MODES] == [
|
||||
HVAC_MODE_OFF,
|
||||
HVAC_MODE_HEAT,
|
||||
HVAC_MODE_COOL,
|
||||
HVAC_MODE_AUTO,
|
||||
]
|
||||
assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE
|
||||
assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 23.1
|
||||
assert state.attributes[ATTR_TEMPERATURE] == 21.1
|
||||
assert state.attributes.get(ATTR_TARGET_TEMP_LOW) is None
|
||||
assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) is None
|
||||
assert state.attributes[ATTR_FAN_MODE] == "Auto Low"
|
||||
assert state.attributes[ATTR_FAN_MODES] == ["Auto Low", "On Low"]
|
||||
|
||||
# Test set target temperature
|
||||
await hass.services.async_call(
|
||||
"climate",
|
||||
"set_temperature",
|
||||
{"entity_id": "climate.ct32_thermostat_mode", "temperature": 26.1},
|
||||
blocking=True,
|
||||
)
|
||||
assert len(sent_messages) == 1
|
||||
msg = sent_messages[-1]
|
||||
assert msg["topic"] == "OpenZWave/1/command/setvalue/"
|
||||
# Celsius is converted to Fahrenheit here!
|
||||
assert round(msg["payload"]["Value"], 2) == 78.98
|
||||
assert msg["payload"]["ValueIDKey"] == 281475099443218
|
||||
|
||||
# Test set mode
|
||||
await hass.services.async_call(
|
||||
"climate",
|
||||
"set_hvac_mode",
|
||||
{"entity_id": "climate.ct32_thermostat_mode", "hvac_mode": HVAC_MODE_AUTO},
|
||||
blocking=True,
|
||||
)
|
||||
assert len(sent_messages) == 2
|
||||
msg = sent_messages[-1]
|
||||
assert msg["topic"] == "OpenZWave/1/command/setvalue/"
|
||||
assert msg["payload"] == {"Value": 3, "ValueIDKey": 122683412}
|
||||
|
||||
# Test set missing mode
|
||||
await hass.services.async_call(
|
||||
"climate",
|
||||
"set_hvac_mode",
|
||||
{"entity_id": "climate.ct32_thermostat_mode", "hvac_mode": "fan_only"},
|
||||
blocking=True,
|
||||
)
|
||||
assert len(sent_messages) == 2
|
||||
assert "Received an invalid hvac mode: fan_only" in caplog.text
|
||||
|
||||
# Test set fan mode
|
||||
await hass.services.async_call(
|
||||
"climate",
|
||||
"set_fan_mode",
|
||||
{"entity_id": "climate.ct32_thermostat_mode", "fan_mode": "On Low"},
|
||||
blocking=True,
|
||||
)
|
||||
assert len(sent_messages) == 3
|
||||
msg = sent_messages[-1]
|
||||
assert msg["topic"] == "OpenZWave/1/command/setvalue/"
|
||||
assert msg["payload"] == {"Value": 1, "ValueIDKey": 122748948}
|
||||
|
||||
# Test set invalid fan mode
|
||||
await hass.services.async_call(
|
||||
"climate",
|
||||
"set_fan_mode",
|
||||
{"entity_id": "climate.ct32_thermostat_mode", "fan_mode": "invalid fan mode"},
|
||||
blocking=True,
|
||||
)
|
||||
assert len(sent_messages) == 3
|
||||
assert "Received an invalid fan mode: invalid fan mode" in caplog.text
|
||||
|
||||
# Test incoming mode change to auto,
|
||||
# resulting in multiple setpoints
|
||||
receive_message(climate_msg)
|
||||
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.attributes.get(ATTR_TEMPERATURE) is None
|
||||
assert state.attributes[ATTR_TARGET_TEMP_LOW] == 21.1
|
||||
assert state.attributes[ATTR_TARGET_TEMP_HIGH] == 25.6
|
||||
|
||||
# Test setting high/low temp on multiple setpoints
|
||||
await hass.services.async_call(
|
||||
"climate",
|
||||
"set_temperature",
|
||||
{
|
||||
"entity_id": "climate.ct32_thermostat_mode",
|
||||
"target_temp_low": 20,
|
||||
"target_temp_high": 25,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
assert len(sent_messages) == 5 # 2 messages !
|
||||
msg = sent_messages[-2] # low setpoint
|
||||
assert msg["topic"] == "OpenZWave/1/command/setvalue/"
|
||||
assert round(msg["payload"]["Value"], 2) == 68.0
|
||||
assert msg["payload"]["ValueIDKey"] == 281475099443218
|
||||
msg = sent_messages[-1] # high setpoint
|
||||
assert msg["topic"] == "OpenZWave/1/command/setvalue/"
|
||||
assert round(msg["payload"]["Value"], 2) == 77.0
|
||||
assert msg["payload"]["ValueIDKey"] == 562950076153874
|
||||
|
||||
# Test basic/single-setpoint thermostat (node 16 in dump)
|
||||
state = hass.states.get("climate.komforthaus_spirit_z_wave_plus_mode")
|
||||
assert state is not None
|
||||
assert state.state == HVAC_MODE_HEAT
|
||||
assert state.attributes[ATTR_HVAC_MODES] == [
|
||||
HVAC_MODE_OFF,
|
||||
HVAC_MODE_HEAT,
|
||||
]
|
||||
assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 17.3
|
||||
assert round(state.attributes[ATTR_TEMPERATURE], 0) == 19
|
||||
assert state.attributes.get(ATTR_TARGET_TEMP_LOW) is None
|
||||
assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) is None
|
||||
assert state.attributes[ATTR_PRESET_MODES] == [
|
||||
"none",
|
||||
"Heat Eco",
|
||||
"Full Power",
|
||||
"Manufacturer Specific",
|
||||
]
|
||||
|
||||
# Test set target temperature
|
||||
await hass.services.async_call(
|
||||
"climate",
|
||||
"set_temperature",
|
||||
{
|
||||
"entity_id": "climate.komforthaus_spirit_z_wave_plus_mode",
|
||||
"temperature": 28.0,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
assert len(sent_messages) == 6
|
||||
msg = sent_messages[-1]
|
||||
assert msg["topic"] == "OpenZWave/1/command/setvalue/"
|
||||
assert msg["payload"] == {
|
||||
"Value": 28.0,
|
||||
"ValueIDKey": 281475250438162,
|
||||
}
|
||||
|
||||
# Test set preset mode
|
||||
await hass.services.async_call(
|
||||
"climate",
|
||||
"set_preset_mode",
|
||||
{
|
||||
"entity_id": "climate.komforthaus_spirit_z_wave_plus_mode",
|
||||
"preset_mode": "Heat Eco",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
assert len(sent_messages) == 7
|
||||
msg = sent_messages[-1]
|
||||
assert msg["topic"] == "OpenZWave/1/command/setvalue/"
|
||||
assert msg["payload"] == {
|
||||
"Value": 11,
|
||||
"ValueIDKey": 273678356,
|
||||
}
|
||||
|
||||
# Test set preset mode None
|
||||
# This preset should set and return to current hvac mode
|
||||
await hass.services.async_call(
|
||||
"climate",
|
||||
"set_preset_mode",
|
||||
{
|
||||
"entity_id": "climate.komforthaus_spirit_z_wave_plus_mode",
|
||||
"preset_mode": "none",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
assert len(sent_messages) == 8
|
||||
msg = sent_messages[-1]
|
||||
assert msg["topic"] == "OpenZWave/1/command/setvalue/"
|
||||
assert msg["payload"] == {
|
||||
"Value": 1,
|
||||
"ValueIDKey": 273678356,
|
||||
}
|
||||
|
||||
# Test set invalid preset mode
|
||||
await hass.services.async_call(
|
||||
"climate",
|
||||
"set_preset_mode",
|
||||
{
|
||||
"entity_id": "climate.komforthaus_spirit_z_wave_plus_mode",
|
||||
"preset_mode": "invalid preset mode",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
assert len(sent_messages) == 8
|
||||
assert "Received an invalid preset mode: invalid preset mode" in caplog.text
|
|
@ -0,0 +1,54 @@
|
|||
{
|
||||
"topic": "OpenZWave/1/node/7/instance/1/commandclass/64/value/122683412/",
|
||||
"payload": {
|
||||
"Label": "Mode",
|
||||
"Value": {
|
||||
"List": [
|
||||
{
|
||||
"Value": 0,
|
||||
"Label": "Off"
|
||||
},
|
||||
{
|
||||
"Value": 1,
|
||||
"Label": "Heat"
|
||||
},
|
||||
{
|
||||
"Value": 2,
|
||||
"Label": "Cool"
|
||||
},
|
||||
{
|
||||
"Value": 3,
|
||||
"Label": "Auto"
|
||||
},
|
||||
{
|
||||
"Value": 11,
|
||||
"Label": "Heat Econ"
|
||||
},
|
||||
{
|
||||
"Value": 12,
|
||||
"Label": "Cool Econ"
|
||||
}
|
||||
],
|
||||
"Selected": "Auto",
|
||||
"Selected_id": 3
|
||||
},
|
||||
"Units": "",
|
||||
"Min": 0,
|
||||
"Max": 0,
|
||||
"Type": "List",
|
||||
"Instance": 1,
|
||||
"CommandClass": "COMMAND_CLASS_THERMOSTAT_MODE",
|
||||
"Index": 0,
|
||||
"Node": 7,
|
||||
"Genre": "User",
|
||||
"Help": "Set the Thermostat Mode",
|
||||
"ValueIDKey": 122683412,
|
||||
"ReadOnly": false,
|
||||
"WriteOnly": false,
|
||||
"ValueSet": false,
|
||||
"ValuePolled": false,
|
||||
"ChangeVerified": false,
|
||||
"Event": "valueAdded",
|
||||
"TimeStamp": 1588264894
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue