Add climate platform to zwave_js (#45177)

* add zwave_js support for climate

* fix

* add fixture

* rename fixture

* fix variable name error

* add tests

* fix tests and handle set_temp properly based on unit

* update call being tested

* fix tests

* improve coverage

* fix docstring

* address review comments

* fix test

* update enum class name

* bump zwave-js-server-python version and assume primary_value is always set

* add additional coverage

* fix docstrings and move populating modes/presets into initialization

* attempt to address comments

* improve comment

* move mode value into a variable so its easier to iterate in the future

* dont assume mode as a discovery point

* assume all values are available when node is ready

* fix order of operations

* switch to valueerror

* use primary value

* readd property and type to discovery schema
This commit is contained in:
Raman Gupta 2021-01-17 20:45:06 -06:00 committed by GitHub
parent a1b0d6baad
commit b2f914823d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 1351 additions and 1 deletions

View File

@ -0,0 +1,300 @@
"""Representation of Z-Wave thermostats."""
import logging
from typing import Any, Callable, Dict, List, Optional
from zwave_js_server.client import Client as ZwaveClient
from zwave_js_server.const import (
THERMOSTAT_CURRENT_TEMP_PROPERTY,
THERMOSTAT_MODE_SETPOINT_MAP,
THERMOSTAT_MODES,
THERMOSTAT_OPERATING_STATE_PROPERTY,
THERMOSTAT_SETPOINT_PROPERTY,
CommandClass,
ThermostatMode,
ThermostatOperatingState,
ThermostatSetpointType,
)
from zwave_js_server.model.value import Value as ZwaveValue
from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate.const import (
ATTR_HVAC_MODE,
ATTR_TARGET_TEMP_HIGH,
ATTR_TARGET_TEMP_LOW,
CURRENT_HVAC_COOL,
CURRENT_HVAC_FAN,
CURRENT_HVAC_HEAT,
CURRENT_HVAC_IDLE,
DOMAIN as CLIMATE_DOMAIN,
HVAC_MODE_COOL,
HVAC_MODE_DRY,
HVAC_MODE_FAN_ONLY,
HVAC_MODE_HEAT,
HVAC_MODE_HEAT_COOL,
HVAC_MODE_OFF,
PRESET_NONE,
SUPPORT_PRESET_MODE,
SUPPORT_TARGET_TEMPERATURE,
SUPPORT_TARGET_TEMPERATURE_RANGE,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN
from .discovery import ZwaveDiscoveryInfo
from .entity import ZWaveBaseEntity
_LOGGER = logging.getLogger(__name__)
# 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_MAP: Dict[int, str] = {
ThermostatMode.OFF: HVAC_MODE_OFF,
ThermostatMode.HEAT: HVAC_MODE_HEAT,
ThermostatMode.COOL: HVAC_MODE_COOL,
# 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,
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,
}
HVAC_CURRENT_MAP: Dict[int, str] = {
ThermostatOperatingState.IDLE: CURRENT_HVAC_IDLE,
ThermostatOperatingState.PENDING_HEAT: CURRENT_HVAC_IDLE,
ThermostatOperatingState.HEATING: CURRENT_HVAC_HEAT,
ThermostatOperatingState.PENDING_COOL: CURRENT_HVAC_IDLE,
ThermostatOperatingState.COOLING: CURRENT_HVAC_COOL,
ThermostatOperatingState.FAN_ONLY: CURRENT_HVAC_FAN,
ThermostatOperatingState.VENT_ECONOMIZER: CURRENT_HVAC_FAN,
ThermostatOperatingState.AUX_HEATING: CURRENT_HVAC_HEAT,
ThermostatOperatingState.SECOND_STAGE_HEATING: CURRENT_HVAC_HEAT,
ThermostatOperatingState.SECOND_STAGE_COOLING: CURRENT_HVAC_COOL,
ThermostatOperatingState.SECOND_STAGE_AUX_HEAT: CURRENT_HVAC_HEAT,
ThermostatOperatingState.THIRD_STAGE_AUX_HEAT: CURRENT_HVAC_HEAT,
}
async def async_setup_entry(
hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable
) -> None:
"""Set up Z-Wave climate from config entry."""
client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT]
@callback
def async_add_climate(info: ZwaveDiscoveryInfo) -> None:
"""Add Z-Wave Climate."""
entities: List[ZWaveBaseEntity] = []
entities.append(ZWaveClimate(client, info))
async_add_entities(entities)
hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append(
async_dispatcher_connect(
hass, f"{DOMAIN}_add_{CLIMATE_DOMAIN}", async_add_climate
)
)
class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
"""Representation of a Z-Wave climate."""
def __init__(self, client: ZwaveClient, info: ZwaveDiscoveryInfo) -> None:
"""Initialize lock."""
super().__init__(client, info)
self._hvac_modes: Dict[str, Optional[int]] = {}
self._hvac_presets: Dict[str, Optional[int]] = {}
self._current_mode = self.info.primary_value
self._setpoint_values: Dict[ThermostatSetpointType, ZwaveValue] = {}
for enum in ThermostatSetpointType:
self._setpoint_values[enum] = self.get_zwave_value(
THERMOSTAT_SETPOINT_PROPERTY,
command_class=CommandClass.THERMOSTAT_SETPOINT,
value_property_key_name=enum.value,
add_to_watched_value_ids=True,
)
self._operating_state = self.get_zwave_value(
THERMOSTAT_OPERATING_STATE_PROPERTY,
command_class=CommandClass.THERMOSTAT_OPERATING_STATE,
add_to_watched_value_ids=True,
)
self._current_temp = self.get_zwave_value(
THERMOSTAT_CURRENT_TEMP_PROPERTY,
command_class=CommandClass.SENSOR_MULTILEVEL,
add_to_watched_value_ids=True,
)
self._set_modes_and_presets()
def _setpoint_value(self, setpoint_type: ThermostatSetpointType) -> ZwaveValue:
"""Optionally return a ZwaveValue for a setpoint."""
val = self._setpoint_values[setpoint_type]
if val is None:
raise ValueError("Value requested is not available")
return val
def _set_modes_and_presets(self) -> None:
"""Convert Z-Wave Thermostat modes into Home Assistant modes and presets."""
all_modes: Dict[str, Optional[int]] = {}
all_presets: Dict[str, Optional[int]] = {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.
current_mode = self._current_mode
if not current_mode:
return
for mode_id, mode_name in current_mode.metadata.states.items():
mode_id = int(mode_id)
if mode_id in THERMOSTAT_MODES:
# treat value as hvac mode
hass_mode = ZW_HVAC_MODE_MAP.get(mode_id)
if hass_mode:
all_modes[hass_mode] = mode_id
else:
# treat value as hvac preset
all_presets[mode_name] = mode_id
self._hvac_modes = all_modes
self._hvac_presets = all_presets
@property
def _current_mode_setpoint_enums(self) -> List[Optional[ThermostatSetpointType]]:
"""Return the list of enums that are relevant to the current thermostat mode."""
return THERMOSTAT_MODE_SETPOINT_MAP.get(int(self._current_mode.value), []) # type: ignore
@property
def temperature_unit(self) -> str:
"""Return the unit of measurement used by the platform."""
temp: Optional[ZwaveValue] = None
if self._current_mode_setpoint_enums:
temp = self._setpoint_value(self._current_mode_setpoint_enums[0])
if temp is not None and temp.metadata.unit == "°F":
return TEMP_FAHRENHEIT
return TEMP_CELSIUS
@property
def hvac_mode(self) -> str:
"""Return hvac operation ie. heat, cool mode."""
if self._current_mode is None:
# Thermostat(valve) with no support for setting a mode is considered heating-only
return HVAC_MODE_HEAT
return ZW_HVAC_MODE_MAP.get(int(self._current_mode.value), HVAC_MODE_HEAT_COOL)
@property
def hvac_modes(self) -> List[str]:
"""Return the list of available hvac operation modes."""
return list(self._hvac_modes)
@property
def hvac_action(self) -> Optional[str]:
"""Return the current running hvac operation if supported."""
if not self._operating_state:
return None
return HVAC_CURRENT_MAP.get(int(self._operating_state.value))
@property
def current_temperature(self) -> Optional[float]:
"""Return the current temperature."""
return self._current_temp.value if self._current_temp else None
@property
def target_temperature(self) -> Optional[float]:
"""Return the temperature we try to reach."""
temp = self._setpoint_value(self._current_mode_setpoint_enums[0])
return temp.value if temp else None
@property
def target_temperature_high(self) -> Optional[float]:
"""Return the highbound target temperature we try to reach."""
temp = self._setpoint_value(self._current_mode_setpoint_enums[1])
return temp.value if temp else None
@property
def target_temperature_low(self) -> Optional[float]:
"""Return the lowbound target temperature we try to reach."""
return self.target_temperature
@property
def preset_mode(self) -> Optional[str]:
"""Return the current preset mode, e.g., home, away, temp."""
if self._current_mode and int(self._current_mode.value) not in THERMOSTAT_MODES:
return_val: str = self._current_mode.metadata.states.get(
self._current_mode.value
)
return return_val
return PRESET_NONE
@property
def preset_modes(self) -> Optional[List[str]]:
"""Return a list of available preset modes."""
return list(self._hvac_presets)
@property
def supported_features(self) -> int:
"""Return the list of supported features."""
support = SUPPORT_PRESET_MODE
if len(self._current_mode_setpoint_enums) == 1:
support |= SUPPORT_TARGET_TEMPERATURE
if len(self._current_mode_setpoint_enums) > 1:
support |= SUPPORT_TARGET_TEMPERATURE_RANGE
return support
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
assert self.hass
hvac_mode: Optional[str] = kwargs.get(ATTR_HVAC_MODE)
if hvac_mode is not None:
await self.async_set_hvac_mode(hvac_mode)
if len(self._current_mode_setpoint_enums) == 1:
setpoint: ZwaveValue = self._setpoint_value(
self._current_mode_setpoint_enums[0]
)
target_temp: Optional[float] = kwargs.get(ATTR_TEMPERATURE)
if target_temp is not None:
await self.info.node.async_set_value(setpoint, target_temp)
elif len(self._current_mode_setpoint_enums) == 2:
setpoint_low: ZwaveValue = self._setpoint_value(
self._current_mode_setpoint_enums[0]
)
setpoint_high: ZwaveValue = self._setpoint_value(
self._current_mode_setpoint_enums[1]
)
target_temp_low: Optional[float] = kwargs.get(ATTR_TARGET_TEMP_LOW)
target_temp_high: Optional[float] = kwargs.get(ATTR_TARGET_TEMP_HIGH)
if target_temp_low is not None:
await self.info.node.async_set_value(setpoint_low, target_temp_low)
if target_temp_high is not None:
await self.info.node.async_set_value(setpoint_high, target_temp_high)
async def async_set_hvac_mode(self, hvac_mode: str) -> None:
"""Set new target hvac mode."""
if not self._current_mode:
# Thermostat(valve) with no support for setting a mode
raise ValueError(
f"Thermostat {self.entity_id} does not support setting a mode"
)
hvac_mode_value = self._hvac_modes.get(hvac_mode)
if hvac_mode_value is None:
raise ValueError(f"Received an invalid hvac mode: {hvac_mode}")
await self.info.node.async_set_value(self._current_mode, hvac_mode_value)
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""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 = self._hvac_presets.get(preset_mode)
if preset_mode_value is None:
raise ValueError(f"Received an invalid preset mode: {preset_mode}")
await self.info.node.async_set_value(self._current_mode, preset_mode_value)

View File

@ -3,7 +3,7 @@
DOMAIN = "zwave_js"
NAME = "Z-Wave JS"
PLATFORMS = ["binary_sensor", "light", "lock", "sensor", "switch"]
PLATFORMS = ["binary_sensor", "climate", "light", "lock", "sensor", "switch"]
DATA_CLIENT = "client"
DATA_UNSUBSCRIBE = "unsubs"

View File

@ -74,6 +74,19 @@ DISCOVERY_SCHEMAS = [
property={"currentMode", "locked"},
type={"number", "boolean"},
),
# climate
ZWaveDiscoverySchema(
platform="climate",
device_class_generic={"Thermostat"},
device_class_specific={
"Setback Thermostat",
"Thermostat General",
"Thermostat General V2",
},
command_class={CommandClass.THERMOSTAT_MODE},
property={"mode"},
type={"number"},
),
# lights
# primary value is the currentValue (brightness)
ZWaveDiscoverySchema(

View File

@ -67,6 +67,14 @@ def lock_schlage_be469_state_fixture():
return json.loads(load_fixture("zwave_js/lock_schlage_be469_state.json"))
@pytest.fixture(name="climate_radio_thermostat_ct100_plus_state", scope="session")
def climate_radio_thermostat_ct100_plus_state_fixture():
"""Load the climate radio thermostat ct100 plus node state fixture data."""
return json.loads(
load_fixture("zwave_js/climate_radio_thermostat_ct100_plus_state.json")
)
@pytest.fixture(name="client")
def mock_client_fixture(controller_state, version_state):
"""Mock a client."""
@ -122,6 +130,16 @@ def lock_schlage_be469_fixture(client, lock_schlage_be469_state):
return node
@pytest.fixture(name="climate_radio_thermostat_ct100_plus")
def climate_radio_thermostat_ct100_plus_fixture(
client, climate_radio_thermostat_ct100_plus_state
):
"""Mock a climate radio thermostat ct100 plus node."""
node = Node(client, climate_radio_thermostat_ct100_plus_state)
client.driver.controller.nodes[node.node_id] = node
return node
@pytest.fixture(name="integration")
async def integration_fixture(hass, client):
"""Set up the zwave_js integration."""

View File

@ -0,0 +1,326 @@
"""Test the Z-Wave JS climate platform."""
import pytest
from zwave_js_server.event import Event
from homeassistant.components.climate.const import (
ATTR_CURRENT_TEMPERATURE,
ATTR_HVAC_ACTION,
ATTR_HVAC_MODE,
ATTR_HVAC_MODES,
ATTR_PRESET_MODE,
ATTR_TARGET_TEMP_HIGH,
ATTR_TARGET_TEMP_LOW,
CURRENT_HVAC_IDLE,
DOMAIN as CLIMATE_DOMAIN,
HVAC_MODE_COOL,
HVAC_MODE_DRY,
HVAC_MODE_HEAT,
HVAC_MODE_HEAT_COOL,
HVAC_MODE_OFF,
PRESET_NONE,
SERVICE_SET_HVAC_MODE,
SERVICE_SET_PRESET_MODE,
SERVICE_SET_TEMPERATURE,
)
from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE
CLIMATE_RADIO_THERMOSTAT_ENTITY = "climate.z_wave_thermostat_thermostat_mode"
async def test_thermostat_v2(
hass, client, climate_radio_thermostat_ct100_plus, integration
):
"""Test a thermostat v2 command class entity."""
node = climate_radio_thermostat_ct100_plus
state = hass.states.get(CLIMATE_RADIO_THERMOSTAT_ENTITY)
assert state
assert state.state == HVAC_MODE_HEAT
assert state.attributes[ATTR_HVAC_MODES] == [
HVAC_MODE_OFF,
HVAC_MODE_HEAT,
HVAC_MODE_COOL,
HVAC_MODE_HEAT_COOL,
]
assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 22.2
assert state.attributes[ATTR_TEMPERATURE] == 22.2
assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE
assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE
# Test setting preset mode
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_PRESET_MODE,
{
ATTR_ENTITY_ID: CLIMATE_RADIO_THERMOSTAT_ENTITY,
ATTR_PRESET_MODE: PRESET_NONE,
},
blocking=True,
)
assert len(client.async_send_command.call_args_list) == 1
args = client.async_send_command.call_args[0][0]
assert args["command"] == "node.set_value"
assert args["nodeId"] == 13
assert args["valueId"] == {
"commandClassName": "Thermostat Mode",
"commandClass": 64,
"endpoint": 1,
"property": "mode",
"propertyName": "mode",
"metadata": {
"type": "number",
"readable": True,
"writeable": True,
"min": 0,
"max": 31,
"label": "Thermostat mode",
"states": {"0": "Off", "1": "Heat", "2": "Cool", "3": "Auto"},
},
"value": 1,
}
assert args["value"] == 1
client.async_send_command.reset_mock()
# Test setting hvac mode
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE,
{
ATTR_ENTITY_ID: CLIMATE_RADIO_THERMOSTAT_ENTITY,
ATTR_HVAC_MODE: HVAC_MODE_COOL,
},
blocking=True,
)
assert len(client.async_send_command.call_args_list) == 1
args = client.async_send_command.call_args[0][0]
assert args["command"] == "node.set_value"
assert args["nodeId"] == 13
assert args["valueId"] == {
"commandClassName": "Thermostat Mode",
"commandClass": 64,
"endpoint": 1,
"property": "mode",
"propertyName": "mode",
"metadata": {
"type": "number",
"readable": True,
"writeable": True,
"min": 0,
"max": 31,
"label": "Thermostat mode",
"states": {"0": "Off", "1": "Heat", "2": "Cool", "3": "Auto"},
},
"value": 1,
}
assert args["value"] == 2
client.async_send_command.reset_mock()
# Test setting temperature
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_TEMPERATURE,
{
ATTR_ENTITY_ID: CLIMATE_RADIO_THERMOSTAT_ENTITY,
ATTR_HVAC_MODE: HVAC_MODE_COOL,
ATTR_TEMPERATURE: 25,
},
blocking=True,
)
assert len(client.async_send_command.call_args_list) == 2
args = client.async_send_command.call_args_list[0][0][0]
assert args["command"] == "node.set_value"
assert args["nodeId"] == 13
assert args["valueId"] == {
"commandClassName": "Thermostat Mode",
"commandClass": 64,
"endpoint": 1,
"property": "mode",
"propertyName": "mode",
"metadata": {
"type": "number",
"readable": True,
"writeable": True,
"min": 0,
"max": 31,
"label": "Thermostat mode",
"states": {"0": "Off", "1": "Heat", "2": "Cool", "3": "Auto"},
},
"value": 1,
}
assert args["value"] == 2
args = client.async_send_command.call_args_list[1][0][0]
assert args["command"] == "node.set_value"
assert args["nodeId"] == 13
assert args["valueId"] == {
"commandClassName": "Thermostat Setpoint",
"commandClass": 67,
"endpoint": 1,
"property": "setpoint",
"propertyKey": 1,
"propertyName": "setpoint",
"propertyKeyName": "Heating",
"metadata": {
"type": "number",
"readable": True,
"writeable": True,
"unit": "°F",
"ccSpecific": {"setpointType": 1},
},
"value": 72,
}
assert args["value"] == 77
client.async_send_command.reset_mock()
# Test cool mode update from value updated event
event = Event(
type="value updated",
data={
"source": "node",
"event": "value updated",
"nodeId": 13,
"args": {
"commandClassName": "Thermostat Mode",
"commandClass": 64,
"endpoint": 1,
"property": "mode",
"propertyName": "mode",
"newValue": 2,
"prevValue": 1,
},
},
)
node.receive_event(event)
state = hass.states.get(CLIMATE_RADIO_THERMOSTAT_ENTITY)
assert state.state == HVAC_MODE_COOL
assert state.attributes[ATTR_TEMPERATURE] == 22.8
# Test heat_cool mode update from value updated event
event = Event(
type="value updated",
data={
"source": "node",
"event": "value updated",
"nodeId": 13,
"args": {
"commandClassName": "Thermostat Mode",
"commandClass": 64,
"endpoint": 1,
"property": "mode",
"propertyName": "mode",
"newValue": 3,
"prevValue": 1,
},
},
)
node.receive_event(event)
state = hass.states.get(CLIMATE_RADIO_THERMOSTAT_ENTITY)
assert state.state == HVAC_MODE_HEAT_COOL
assert state.attributes[ATTR_TARGET_TEMP_HIGH] == 22.8
assert state.attributes[ATTR_TARGET_TEMP_LOW] == 22.2
client.async_send_command.reset_mock()
# Test setting temperature with heat_cool
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_TEMPERATURE,
{
ATTR_ENTITY_ID: CLIMATE_RADIO_THERMOSTAT_ENTITY,
ATTR_TARGET_TEMP_HIGH: 30,
ATTR_TARGET_TEMP_LOW: 25,
},
blocking=True,
)
assert len(client.async_send_command.call_args_list) == 2
args = client.async_send_command.call_args_list[0][0][0]
assert args["command"] == "node.set_value"
assert args["nodeId"] == 13
assert args["valueId"] == {
"commandClassName": "Thermostat Setpoint",
"commandClass": 67,
"endpoint": 1,
"property": "setpoint",
"propertyKey": 1,
"propertyName": "setpoint",
"propertyKeyName": "Heating",
"metadata": {
"type": "number",
"readable": True,
"writeable": True,
"unit": "°F",
"ccSpecific": {"setpointType": 1},
},
"value": 72,
}
assert args["value"] == 77
args = client.async_send_command.call_args_list[1][0][0]
assert args["command"] == "node.set_value"
assert args["nodeId"] == 13
assert args["valueId"] == {
"commandClassName": "Thermostat Setpoint",
"commandClass": 67,
"endpoint": 1,
"property": "setpoint",
"propertyKey": 2,
"propertyName": "setpoint",
"propertyKeyName": "Cooling",
"metadata": {
"type": "number",
"readable": True,
"writeable": True,
"unit": "°F",
"ccSpecific": {"setpointType": 2},
},
"value": 73,
}
assert args["value"] == 86
client.async_send_command.reset_mock()
with pytest.raises(ValueError):
# Test setting unknown preset mode
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_PRESET_MODE,
{
ATTR_ENTITY_ID: CLIMATE_RADIO_THERMOSTAT_ENTITY,
ATTR_PRESET_MODE: "unknown_preset",
},
blocking=True,
)
assert len(client.async_send_command.call_args_list) == 0
# Test setting invalid hvac mode
with pytest.raises(ValueError):
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE,
{
ATTR_ENTITY_ID: CLIMATE_RADIO_THERMOSTAT_ENTITY,
ATTR_HVAC_MODE: HVAC_MODE_DRY,
},
blocking=True,
)
# Test setting invalid preset mode
with pytest.raises(ValueError):
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_PRESET_MODE,
{
ATTR_ENTITY_ID: CLIMATE_RADIO_THERMOSTAT_ENTITY,
ATTR_PRESET_MODE: "invalid_mode",
},
blocking=True,
)

View File

@ -0,0 +1,693 @@
{
"nodeId": 13,
"index": 0,
"installerIcon": 4608,
"userIcon": 4608,
"status": 4,
"ready": true,
"deviceClass": {
"basic": "Static Controller",
"generic": "Thermostat",
"specific": "Thermostat General V2",
"mandatorySupportedCCs": [
"Basic",
"Manufacturer Specific",
"Thermostat Mode",
"Thermostat Setpoint",
"Version"
],
"mandatoryControlCCs": []
},
"isListening": true,
"isFrequentListening": false,
"isRouting": true,
"maxBaudRate": 40000,
"isSecure": false,
"version": 4,
"isBeaming": true,
"manufacturerId": 152,
"productId": 256,
"productType": 25602,
"firmwareVersion": "10.7",
"zwavePlusVersion": 1,
"nodeType": 0,
"roleType": 5,
"deviceConfig": {
"manufacturerId": 152,
"manufacturer": "Radio Thermostat Company of America (RTC)",
"label": "CT100 Plus",
"description": "Z-Wave Thermostat",
"devices": [{ "productType": "0x6402", "productId": "0x0100" }],
"firmwareVersion": { "min": "0.0", "max": "255.255" },
"paramInformation": { "_map": {} }
},
"label": "CT100 Plus",
"neighbors": [1, 2, 3, 4, 20],
"endpointCountIsDynamic": false,
"endpointsHaveIdenticalCapabilities": false,
"individualEndpointCount": 2,
"aggregatedEndpointCount": 0,
"interviewAttempts": 1,
"endpoints": [
{
"nodeId": 13,
"index": 0,
"installerIcon": 4608,
"userIcon": 4608
},
{
"nodeId": 13,
"index": 1,
"installerIcon": 4608,
"userIcon": 4608
},
{ "nodeId": 13, "index": 2 }
],
"values": [
{
"commandClassName": "Manufacturer Specific",
"commandClass": 114,
"endpoint": 0,
"property": "manufacturerId",
"propertyName": "manufacturerId",
"metadata": {
"type": "number",
"readable": true,
"writeable": false,
"min": 0,
"max": 65535,
"label": "Manufacturer ID"
},
"value": 152
},
{
"commandClassName": "Manufacturer Specific",
"commandClass": 114,
"endpoint": 0,
"property": "productType",
"propertyName": "productType",
"metadata": {
"type": "number",
"readable": true,
"writeable": false,
"min": 0,
"max": 65535,
"label": "Product type"
},
"value": 25602
},
{
"commandClassName": "Manufacturer Specific",
"commandClass": 114,
"endpoint": 0,
"property": "productId",
"propertyName": "productId",
"metadata": {
"type": "number",
"readable": true,
"writeable": false,
"min": 0,
"max": 65535,
"label": "Product ID"
},
"value": 256
},
{
"commandClassName": "Version",
"commandClass": 134,
"endpoint": 0,
"property": "libraryType",
"propertyName": "libraryType",
"metadata": {
"type": "any",
"readable": true,
"writeable": false,
"label": "Library type"
},
"value": 3
},
{
"commandClassName": "Version",
"commandClass": 134,
"endpoint": 0,
"property": "protocolVersion",
"propertyName": "protocolVersion",
"metadata": {
"type": "any",
"readable": true,
"writeable": false,
"label": "Z-Wave protocol version"
},
"value": "4.24"
},
{
"commandClassName": "Version",
"commandClass": 134,
"endpoint": 0,
"property": "firmwareVersions",
"propertyName": "firmwareVersions",
"metadata": {
"type": "any",
"readable": true,
"writeable": false,
"label": "Z-Wave chip firmware versions"
},
"value": ["10.7"]
},
{
"commandClassName": "Version",
"commandClass": 134,
"endpoint": 0,
"property": "hardwareVersion",
"propertyName": "hardwareVersion",
"metadata": {
"type": "any",
"readable": true,
"writeable": false,
"label": "Z-Wave chip hardware version"
}
},
{
"commandClassName": "Indicator",
"commandClass": 135,
"endpoint": 0,
"property": "value",
"propertyName": "value",
"metadata": {
"type": "number",
"readable": true,
"writeable": true,
"min": 0,
"max": 255,
"label": "Indicator value",
"ccSpecific": { "indicatorId": 0 }
},
"value": 0
},
{
"commandClassName": "Configuration",
"commandClass": 112,
"endpoint": 0,
"property": 1,
"propertyName": "Temperature Reporting Threshold",
"metadata": {
"type": "number",
"readable": true,
"writeable": true,
"valueSize": 1,
"min": 0,
"max": 4,
"default": 2,
"format": 0,
"allowManualEntry": false,
"states": {
"0": "Disabled",
"1": "0.5° F",
"2": "1.0° F",
"3": "1.5° F",
"4": "2.0° F"
},
"label": "Temperature Reporting Threshold",
"description": "Reporting threshold for changes in the ambient temperature",
"isFromConfig": true
},
"value": 1
},
{
"commandClassName": "Configuration",
"commandClass": 112,
"endpoint": 0,
"property": 2,
"propertyName": "HVAC Settings",
"metadata": {
"type": "number",
"readable": true,
"writeable": false,
"valueSize": 4,
"min": 0,
"max": 0,
"default": 0,
"format": 0,
"allowManualEntry": true,
"label": "HVAC Settings",
"description": "Configured HVAC settings",
"isFromConfig": true
},
"value": 17891329
},
{
"commandClassName": "Configuration",
"commandClass": 112,
"endpoint": 0,
"property": 4,
"propertyName": "Power Status",
"metadata": {
"type": "number",
"readable": true,
"writeable": false,
"valueSize": 1,
"min": 0,
"max": 0,
"default": 0,
"format": 0,
"allowManualEntry": true,
"label": "Power Status",
"description": "C-Wire / Battery Status",
"isFromConfig": true
},
"value": 1
},
{
"commandClassName": "Configuration",
"commandClass": 112,
"endpoint": 0,
"property": 7,
"propertyName": "Thermostat Swing Temperature",
"metadata": {
"type": "number",
"readable": true,
"writeable": true,
"valueSize": 1,
"min": 1,
"max": 8,
"default": 2,
"format": 0,
"allowManualEntry": false,
"states": {
"1": "0.5° F",
"2": "1.0° F",
"3": "1.5° F",
"4": "2.0° F",
"5": "2.5° F",
"6": "3.0° F",
"7": "3.5° F",
"8": "4.0° F"
},
"label": "Thermostat Swing Temperature",
"description": "Variance allowed from setpoint to engage HVAC",
"isFromConfig": true
},
"value": 1
},
{
"commandClassName": "Configuration",
"commandClass": 112,
"endpoint": 0,
"property": 8,
"propertyName": "Thermostat Diff Temperature",
"metadata": {
"type": "number",
"readable": true,
"writeable": true,
"valueSize": 1,
"min": 4,
"max": 12,
"default": 4,
"format": 0,
"allowManualEntry": false,
"states": { "4": "2.0° F", "8": "4.0° F", "12": "6.0° F" },
"label": "Thermostat Diff Temperature",
"description": "Configures additional stages",
"isFromConfig": true
},
"value": 1028
},
{
"commandClassName": "Configuration",
"commandClass": 112,
"endpoint": 0,
"property": 9,
"propertyName": "Thermostat Recovery Mode",
"metadata": {
"type": "number",
"readable": true,
"writeable": true,
"valueSize": 1,
"min": 1,
"max": 2,
"default": 2,
"format": 0,
"allowManualEntry": false,
"states": {
"1": "Fast recovery mode",
"2": "Economy recovery mode"
},
"label": "Thermostat Recovery Mode",
"description": "Fast or Economy recovery mode",
"isFromConfig": true
},
"value": 2
},
{
"commandClassName": "Configuration",
"commandClass": 112,
"endpoint": 0,
"property": 10,
"propertyName": "Temperature Reporting Filter",
"metadata": {
"type": "number",
"readable": true,
"writeable": true,
"valueSize": 4,
"min": 0,
"max": 124,
"default": 124,
"format": 0,
"allowManualEntry": true,
"label": "Temperature Reporting Filter",
"description": "Upper/Lower bounds for thermostat temperature reporting",
"isFromConfig": true
},
"value": 0
},
{
"commandClassName": "Configuration",
"commandClass": 112,
"endpoint": 0,
"property": 11,
"propertyName": "Simple UI Mode",
"metadata": {
"type": "number",
"readable": true,
"writeable": true,
"valueSize": 1,
"min": 0,
"max": 1,
"default": 1,
"format": 0,
"allowManualEntry": false,
"states": {
"0": "Normal mode enabled",
"1": "Simple mode enabled"
},
"label": "Simple UI Mode",
"description": "Simple mode enable/disable",
"isFromConfig": true
},
"value": 1
},
{
"commandClassName": "Configuration",
"commandClass": 112,
"endpoint": 0,
"property": 12,
"propertyName": "Multicast",
"metadata": {
"type": "number",
"readable": true,
"writeable": true,
"valueSize": 1,
"min": 0,
"max": 1,
"default": 0,
"format": 0,
"allowManualEntry": false,
"states": {
"0": "Multicast disabled",
"1": "Multicast enabled"
},
"label": "Multicast",
"description": "Enable or disables Multicast",
"isFromConfig": true
},
"value": 0
},
{
"commandClassName": "Configuration",
"commandClass": 112,
"endpoint": 0,
"property": 3,
"propertyName": "Utility Lock Enable/Disable",
"metadata": {
"type": "number",
"readable": false,
"writeable": true,
"valueSize": 1,
"min": 0,
"max": 255,
"default": 0,
"format": 1,
"allowManualEntry": false,
"states": {
"0": "Utility lock disabled",
"1": "Utility lock enabled"
},
"label": "Utility Lock Enable/Disable",
"description": "Prevents setpoint changes at thermostat",
"isFromConfig": true
}
},
{
"commandClassName": "Configuration",
"commandClass": 112,
"endpoint": 0,
"property": 5,
"propertyName": "Humidity Reporting Threshold",
"metadata": {
"type": "number",
"readable": true,
"writeable": true,
"valueSize": 1,
"min": 0,
"max": 255,
"default": 0,
"format": 1,
"allowManualEntry": false,
"states": {
"0": "Disabled",
"1": "3% RH",
"2": "5% RH",
"3": "10% RH"
},
"label": "Humidity Reporting Threshold",
"description": "Reporting threshold for changes in the relative humidity",
"isFromConfig": true
}
},
{
"commandClassName": "Configuration",
"commandClass": 112,
"endpoint": 0,
"property": 6,
"propertyName": "Auxiliary/Emergency",
"metadata": {
"type": "number",
"readable": true,
"writeable": true,
"valueSize": 1,
"min": 0,
"max": 255,
"default": 0,
"format": 1,
"allowManualEntry": false,
"states": {
"0": "Auxiliary/Emergency heat disabled",
"1": "Auxiliary/Emergency heat enabled"
},
"label": "Auxiliary/Emergency",
"description": "Enables or disables auxiliary / emergency heating",
"isFromConfig": true
}
},
{
"commandClassName": "Battery",
"commandClass": 128,
"endpoint": 0,
"property": "level",
"propertyName": "level",
"metadata": {
"type": "number",
"readable": true,
"writeable": false,
"min": 0,
"max": 100,
"unit": "%",
"label": "Battery level"
},
"value": 100
},
{
"commandClassName": "Battery",
"commandClass": 128,
"endpoint": 0,
"property": "isLow",
"propertyName": "isLow",
"metadata": {
"type": "boolean",
"readable": true,
"writeable": false,
"label": "Low battery level"
},
"value": false
},
{
"commandClassName": "Indicator",
"commandClass": 135,
"endpoint": 1,
"property": "value",
"propertyName": "value",
"metadata": {
"type": "number",
"readable": true,
"writeable": true,
"min": 0,
"max": 255,
"label": "Indicator value",
"ccSpecific": { "indicatorId": 0 }
},
"value": 0
},
{
"commandClassName": "Multilevel Sensor",
"commandClass": 49,
"endpoint": 1,
"property": "Air temperature",
"propertyName": "Air temperature",
"metadata": {
"type": "number",
"readable": true,
"writeable": false,
"unit": "°F",
"label": "Air temperature",
"ccSpecific": { "sensorType": 1, "scale": 1 }
},
"value": 72
},
{
"commandClassName": "Multilevel Sensor",
"commandClass": 49,
"endpoint": 1,
"property": "Humidity",
"propertyName": "Humidity",
"metadata": {
"type": "number",
"readable": true,
"writeable": false,
"unit": "%",
"label": "Humidity",
"ccSpecific": { "sensorType": 5, "scale": 0 }
},
"value": 30
},
{
"commandClassName": "Thermostat Mode",
"commandClass": 64,
"endpoint": 1,
"property": "mode",
"propertyName": "mode",
"metadata": {
"type": "number",
"readable": true,
"writeable": true,
"min": 0,
"max": 31,
"label": "Thermostat mode",
"states": { "0": "Off", "1": "Heat", "2": "Cool", "3": "Auto" }
},
"value": 1
},
{
"commandClassName": "Thermostat Mode",
"commandClass": 64,
"endpoint": 1,
"property": "manufacturerData",
"propertyName": "manufacturerData",
"metadata": { "type": "any", "readable": true, "writeable": true }
},
{
"commandClassName": "Thermostat Operating State",
"commandClass": 66,
"endpoint": 1,
"property": "state",
"propertyName": "state",
"metadata": {
"type": "number",
"readable": true,
"writeable": false,
"min": 0,
"max": 255,
"label": "Operating state",
"states": {
"0": "Idle",
"1": "Heating",
"2": "Cooling",
"3": "Fan Only",
"4": "Pending Heat",
"5": "Pending Cool",
"6": "Vent/Economizer",
"7": "Aux Heating",
"8": "2nd Stage Heating",
"9": "2nd Stage Cooling",
"10": "2nd Stage Aux Heat",
"11": "3rd Stage Aux Heat"
}
},
"value": 0
},
{
"commandClassName": "Thermostat Setpoint",
"commandClass": 67,
"endpoint": 1,
"property": "setpoint",
"propertyKey": 1,
"propertyName": "setpoint",
"propertyKeyName": "Heating",
"metadata": {
"type": "number",
"readable": true,
"writeable": true,
"unit": "°F",
"ccSpecific": { "setpointType": 1 }
},
"value": 72
},
{
"commandClassName": "Thermostat Setpoint",
"commandClass": 67,
"endpoint": 1,
"property": "setpoint",
"propertyKey": 2,
"propertyName": "setpoint",
"propertyKeyName": "Cooling",
"metadata": {
"type": "number",
"readable": true,
"writeable": true,
"unit": "°F",
"ccSpecific": { "setpointType": 2 }
},
"value": 73
},
{
"commandClassName": "Battery",
"commandClass": 128,
"endpoint": 1,
"property": "level",
"propertyName": "level",
"metadata": {
"type": "number",
"readable": true,
"writeable": false,
"min": 0,
"max": 100,
"unit": "%",
"label": "Battery level"
},
"value": 100
},
{
"commandClassName": "Battery",
"commandClass": 128,
"endpoint": 1,
"property": "isLow",
"propertyName": "isLow",
"metadata": {
"type": "boolean",
"readable": true,
"writeable": false,
"label": "Low battery level"
},
"value": false
}
]
}