Add support for climate setpoint thermostats to zwave_js (#45890)
This commit is contained in:
parent
9998fe3684
commit
5615ab4c25
6 changed files with 1699 additions and 4 deletions
|
@ -5,6 +5,7 @@ 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_PROPERTY,
|
||||
THERMOSTAT_MODE_SETPOINT_MAP,
|
||||
THERMOSTAT_MODES,
|
||||
THERMOSTAT_OPERATING_STATE_PROPERTY,
|
||||
|
@ -119,7 +120,9 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
|
|||
self._hvac_presets: Dict[str, Optional[int]] = {}
|
||||
self._unit_value: ZwaveValue = None
|
||||
|
||||
self._current_mode = self.info.primary_value
|
||||
self._current_mode = self.get_zwave_value(
|
||||
THERMOSTAT_MODE_PROPERTY, command_class=CommandClass.THERMOSTAT_MODE
|
||||
)
|
||||
self._setpoint_values: Dict[ThermostatSetpointType, ZwaveValue] = {}
|
||||
for enum in ThermostatSetpointType:
|
||||
self._setpoint_values[enum] = self.get_zwave_value(
|
||||
|
@ -165,10 +168,12 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
|
|||
|
||||
# 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:
|
||||
if self._current_mode is None:
|
||||
self._hvac_modes = {
|
||||
ZW_HVAC_MODE_MAP[ThermostatMode.HEAT]: ThermostatMode.HEAT
|
||||
}
|
||||
return
|
||||
for mode_id, mode_name in current_mode.metadata.states.items():
|
||||
for mode_id, mode_name in self._current_mode.metadata.states.items():
|
||||
mode_id = int(mode_id)
|
||||
if mode_id in THERMOSTAT_MODES:
|
||||
# treat value as hvac mode
|
||||
|
@ -184,6 +189,9 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
|
|||
@property
|
||||
def _current_mode_setpoint_enums(self) -> List[Optional[ThermostatSetpointType]]:
|
||||
"""Return the list of enums that are relevant to the current thermostat mode."""
|
||||
if self._current_mode is None:
|
||||
# Thermostat(valve) with no support for setting a mode is considered heating-only
|
||||
return [ThermostatSetpointType.HEATING]
|
||||
return THERMOSTAT_MODE_SETPOINT_MAP.get(int(self._current_mode.value), []) # type: ignore
|
||||
|
||||
@property
|
||||
|
|
|
@ -56,6 +56,8 @@ class ZWaveDiscoverySchema:
|
|||
type: Optional[Set[str]] = None
|
||||
|
||||
|
||||
# For device class mapping see:
|
||||
# https://github.com/zwave-js/node-zwave-js/blob/master/packages/config/config/deviceClasses.json
|
||||
DISCOVERY_SCHEMAS = [
|
||||
# locks
|
||||
ZWaveDiscoverySchema(
|
||||
|
@ -105,6 +107,18 @@ DISCOVERY_SCHEMAS = [
|
|||
property={"mode"},
|
||||
type={"number"},
|
||||
),
|
||||
# climate
|
||||
# setpoint thermostats
|
||||
ZWaveDiscoverySchema(
|
||||
platform="climate",
|
||||
device_class_generic={"Thermostat"},
|
||||
device_class_specific={
|
||||
"Setpoint Thermostat",
|
||||
},
|
||||
command_class={CommandClass.THERMOSTAT_SETPOINT},
|
||||
property={"setpoint"},
|
||||
type={"number"},
|
||||
),
|
||||
# lights
|
||||
# primary value is the currentValue (brightness)
|
||||
ZWaveDiscoverySchema(
|
||||
|
|
|
@ -128,6 +128,18 @@ def climate_radio_thermostat_ct100_plus_different_endpoints_state_fixture():
|
|||
)
|
||||
|
||||
|
||||
@pytest.fixture(name="climate_danfoss_lc_13_state", scope="session")
|
||||
def climate_danfoss_lc_13_state_fixture():
|
||||
"""Load the climate Danfoss (LC-13) electronic radiator thermostat node state fixture data."""
|
||||
return json.loads(load_fixture("zwave_js/climate_danfoss_lc_13_state.json"))
|
||||
|
||||
|
||||
@pytest.fixture(name="climate_heatit_z_trm3_state", scope="session")
|
||||
def climate_heatit_z_trm3_state_fixture():
|
||||
"""Load the climate HEATIT Z-TRM3 thermostat node state fixture data."""
|
||||
return json.loads(load_fixture("zwave_js/climate_heatit_z_trm3_state.json"))
|
||||
|
||||
|
||||
@pytest.fixture(name="nortek_thermostat_state", scope="session")
|
||||
def nortek_thermostat_state_fixture():
|
||||
"""Load the nortek thermostat node state fixture data."""
|
||||
|
@ -254,6 +266,22 @@ def climate_radio_thermostat_ct100_plus_different_endpoints_fixture(
|
|||
return node
|
||||
|
||||
|
||||
@pytest.fixture(name="climate_danfoss_lc_13")
|
||||
def climate_danfoss_lc_13_fixture(client, climate_danfoss_lc_13_state):
|
||||
"""Mock a climate radio danfoss LC-13 node."""
|
||||
node = Node(client, climate_danfoss_lc_13_state)
|
||||
client.driver.controller.nodes[node.node_id] = node
|
||||
return node
|
||||
|
||||
|
||||
@pytest.fixture(name="climate_heatit_z_trm3")
|
||||
def climate_heatit_z_trm3_fixture(client, climate_heatit_z_trm3_state):
|
||||
"""Mock a climate radio HEATIT Z-TRM3 node."""
|
||||
node = Node(client, climate_heatit_z_trm3_state)
|
||||
client.driver.controller.nodes[node.node_id] = node
|
||||
return node
|
||||
|
||||
|
||||
@pytest.fixture(name="nortek_thermostat")
|
||||
def nortek_thermostat_fixture(client, nortek_thermostat_state):
|
||||
"""Mock a nortek thermostat node."""
|
||||
|
|
|
@ -26,6 +26,8 @@ from homeassistant.components.climate.const import (
|
|||
from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE
|
||||
|
||||
CLIMATE_RADIO_THERMOSTAT_ENTITY = "climate.z_wave_thermostat_thermostat_mode"
|
||||
CLIMATE_DANFOSS_LC13_ENTITY = "climate.living_connect_z_thermostat_heating"
|
||||
CLIMATE_FLOOR_THERMOSTAT_ENTITY = "climate.floor_thermostat_thermostat_mode"
|
||||
|
||||
|
||||
async def test_thermostat_v2(
|
||||
|
@ -335,3 +337,97 @@ async def test_thermostat_different_endpoints(
|
|||
state = hass.states.get(CLIMATE_RADIO_THERMOSTAT_ENTITY)
|
||||
|
||||
assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 22.5
|
||||
|
||||
|
||||
async def test_setpoint_thermostat(hass, client, climate_danfoss_lc_13, integration):
|
||||
"""Test a setpoint thermostat command class entity."""
|
||||
node = climate_danfoss_lc_13
|
||||
state = hass.states.get(CLIMATE_DANFOSS_LC13_ENTITY)
|
||||
|
||||
assert state
|
||||
assert state.state == HVAC_MODE_HEAT
|
||||
assert state.attributes[ATTR_TEMPERATURE] == 25
|
||||
assert state.attributes[ATTR_HVAC_MODES] == [HVAC_MODE_HEAT]
|
||||
assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE
|
||||
|
||||
client.async_send_command.reset_mock()
|
||||
|
||||
# Test setting temperature
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{
|
||||
ATTR_ENTITY_ID: CLIMATE_DANFOSS_LC13_ENTITY,
|
||||
ATTR_TEMPERATURE: 21.5,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert len(client.async_send_command.call_args_list) == 1
|
||||
args = client.async_send_command.call_args_list[0][0][0]
|
||||
assert args["command"] == "node.set_value"
|
||||
assert args["nodeId"] == 5
|
||||
assert args["valueId"] == {
|
||||
"endpoint": 0,
|
||||
"commandClass": 67,
|
||||
"commandClassName": "Thermostat Setpoint",
|
||||
"property": "setpoint",
|
||||
"propertyName": "setpoint",
|
||||
"propertyKeyName": "Heating",
|
||||
"ccVersion": 2,
|
||||
"metadata": {
|
||||
"type": "number",
|
||||
"readable": True,
|
||||
"writeable": True,
|
||||
"unit": "\u00b0C",
|
||||
"ccSpecific": {"setpointType": 1},
|
||||
},
|
||||
"value": 25,
|
||||
}
|
||||
assert args["value"] == 21.5
|
||||
|
||||
client.async_send_command.reset_mock()
|
||||
|
||||
# Test setpoint mode update from value updated event
|
||||
event = Event(
|
||||
type="value updated",
|
||||
data={
|
||||
"source": "node",
|
||||
"event": "value updated",
|
||||
"nodeId": 5,
|
||||
"args": {
|
||||
"commandClassName": "Thermostat Setpoint",
|
||||
"commandClass": 67,
|
||||
"endpoint": 0,
|
||||
"property": "setpoint",
|
||||
"propertyKey": 1,
|
||||
"propertyKeyName": "Heating",
|
||||
"propertyName": "setpoint",
|
||||
"newValue": 23,
|
||||
"prevValue": 21.5,
|
||||
},
|
||||
},
|
||||
)
|
||||
node.receive_event(event)
|
||||
|
||||
state = hass.states.get(CLIMATE_DANFOSS_LC13_ENTITY)
|
||||
assert state.state == HVAC_MODE_HEAT
|
||||
assert state.attributes[ATTR_TEMPERATURE] == 23
|
||||
|
||||
client.async_send_command.reset_mock()
|
||||
|
||||
|
||||
async def test_thermostat_heatit(hass, client, climate_heatit_z_trm3, integration):
|
||||
"""Test a thermostat v2 command class entity."""
|
||||
state = hass.states.get(CLIMATE_FLOOR_THERMOSTAT_ENTITY)
|
||||
|
||||
assert state
|
||||
assert state.state == HVAC_MODE_HEAT
|
||||
assert state.attributes[ATTR_HVAC_MODES] == [
|
||||
HVAC_MODE_OFF,
|
||||
HVAC_MODE_HEAT,
|
||||
]
|
||||
assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 22.9
|
||||
assert state.attributes[ATTR_TEMPERATURE] == 22.5
|
||||
assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE
|
||||
assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE
|
||||
|
|
368
tests/fixtures/zwave_js/climate_danfoss_lc_13_state.json
vendored
Normal file
368
tests/fixtures/zwave_js/climate_danfoss_lc_13_state.json
vendored
Normal file
|
@ -0,0 +1,368 @@
|
|||
{
|
||||
"nodeId": 5,
|
||||
"index": 0,
|
||||
"status": 1,
|
||||
"ready": true,
|
||||
"deviceClass": {
|
||||
"basic": "Routing Slave",
|
||||
"generic": "Thermostat",
|
||||
"specific": "Setpoint Thermostat",
|
||||
"mandatorySupportedCCs": [
|
||||
"Manufacturer Specific",
|
||||
"Multi Command",
|
||||
"Thermostat Setpoint",
|
||||
"Version"
|
||||
],
|
||||
"mandatoryControlCCs": []
|
||||
},
|
||||
"isListening": false,
|
||||
"isFrequentListening": false,
|
||||
"isRouting": true,
|
||||
"maxBaudRate": 40000,
|
||||
"isSecure": false,
|
||||
"version": 4,
|
||||
"isBeaming": true,
|
||||
"manufacturerId": 2,
|
||||
"productId": 4,
|
||||
"productType": 5,
|
||||
"firmwareVersion": "1.1",
|
||||
"deviceConfig": {
|
||||
"manufacturerId": 2,
|
||||
"manufacturer": "Danfoss",
|
||||
"label": "LC-13",
|
||||
"description": "Living Connect Z Thermostat",
|
||||
"devices": [
|
||||
{
|
||||
"productType": "0x0005",
|
||||
"productId": "0x0004"
|
||||
},
|
||||
{
|
||||
"productType": "0x8005",
|
||||
"productId": "0x0001"
|
||||
},
|
||||
{
|
||||
"productType": "0x8005",
|
||||
"productId": "0x0002"
|
||||
}
|
||||
],
|
||||
"firmwareVersion": {
|
||||
"min": "0.0",
|
||||
"max": "255.255"
|
||||
},
|
||||
"associations": {},
|
||||
"compat": {
|
||||
"valueIdRegex": {},
|
||||
"queryOnWakeup": [
|
||||
[
|
||||
"Battery",
|
||||
"get"
|
||||
],
|
||||
[
|
||||
"Thermostat Setpoint",
|
||||
"get",
|
||||
1
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"label": "LC-13",
|
||||
"neighbors": [
|
||||
1,
|
||||
14
|
||||
],
|
||||
"interviewAttempts": 1,
|
||||
"endpoints": [
|
||||
{
|
||||
"nodeId": 5,
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
"values": [
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 67,
|
||||
"commandClassName": "Thermostat Setpoint",
|
||||
"property": "setpoint",
|
||||
"propertyName": "setpoint",
|
||||
"propertyKeyName": "Heating",
|
||||
"ccVersion": 2,
|
||||
"metadata": {
|
||||
"type": "number",
|
||||
"readable": true,
|
||||
"writeable": true,
|
||||
"unit": "\u00b0C",
|
||||
"ccSpecific": {
|
||||
"setpointType": 1
|
||||
}
|
||||
},
|
||||
"value": 25
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 70,
|
||||
"commandClassName": "Climate Control Schedule",
|
||||
"property": "changeCounter",
|
||||
"propertyName": "changeCounter",
|
||||
"ccVersion": 0,
|
||||
"metadata": {
|
||||
"type": "any",
|
||||
"readable": true,
|
||||
"writeable": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 70,
|
||||
"commandClassName": "Climate Control Schedule",
|
||||
"property": "overrideType",
|
||||
"propertyName": "overrideType",
|
||||
"ccVersion": 0,
|
||||
"metadata": {
|
||||
"type": "any",
|
||||
"readable": true,
|
||||
"writeable": true
|
||||
},
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 70,
|
||||
"commandClassName": "Climate Control Schedule",
|
||||
"property": "overrideState",
|
||||
"propertyName": "overrideState",
|
||||
"ccVersion": 0,
|
||||
"metadata": {
|
||||
"type": "any",
|
||||
"readable": true,
|
||||
"writeable": true
|
||||
},
|
||||
"value": "Unused"
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 114,
|
||||
"commandClassName": "Manufacturer Specific",
|
||||
"property": "manufacturerId",
|
||||
"propertyName": "manufacturerId",
|
||||
"ccVersion": 1,
|
||||
"metadata": {
|
||||
"type": "number",
|
||||
"readable": true,
|
||||
"writeable": false,
|
||||
"min": 0,
|
||||
"max": 65535,
|
||||
"label": "Manufacturer ID"
|
||||
},
|
||||
"value": 2
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 114,
|
||||
"commandClassName": "Manufacturer Specific",
|
||||
"property": "productType",
|
||||
"propertyName": "productType",
|
||||
"ccVersion": 1,
|
||||
"metadata": {
|
||||
"type": "number",
|
||||
"readable": true,
|
||||
"writeable": false,
|
||||
"min": 0,
|
||||
"max": 65535,
|
||||
"label": "Product type"
|
||||
},
|
||||
"value": 5
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 114,
|
||||
"commandClassName": "Manufacturer Specific",
|
||||
"property": "productId",
|
||||
"propertyName": "productId",
|
||||
"ccVersion": 1,
|
||||
"metadata": {
|
||||
"type": "number",
|
||||
"readable": true,
|
||||
"writeable": false,
|
||||
"min": 0,
|
||||
"max": 65535,
|
||||
"label": "Product ID"
|
||||
},
|
||||
"value": 4
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 117,
|
||||
"commandClassName": "Protection",
|
||||
"property": "local",
|
||||
"propertyName": "local",
|
||||
"ccVersion": 2,
|
||||
"metadata": {
|
||||
"type": "number",
|
||||
"readable": true,
|
||||
"writeable": true,
|
||||
"label": "Local protection state",
|
||||
"states": {
|
||||
"0": "Unprotected",
|
||||
"2": "NoOperationPossible"
|
||||
}
|
||||
},
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 117,
|
||||
"commandClassName": "Protection",
|
||||
"property": "rf",
|
||||
"propertyName": "rf",
|
||||
"ccVersion": 2,
|
||||
"metadata": {
|
||||
"type": "number",
|
||||
"readable": true,
|
||||
"writeable": true,
|
||||
"label": "RF protection state",
|
||||
"states": {}
|
||||
},
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 117,
|
||||
"commandClassName": "Protection",
|
||||
"property": "exclusiveControlNodeId",
|
||||
"propertyName": "exclusiveControlNodeId",
|
||||
"ccVersion": 2,
|
||||
"metadata": {
|
||||
"type": "any",
|
||||
"readable": true,
|
||||
"writeable": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 117,
|
||||
"commandClassName": "Protection",
|
||||
"property": "timeout",
|
||||
"propertyName": "timeout",
|
||||
"ccVersion": 2,
|
||||
"metadata": {
|
||||
"type": "any",
|
||||
"readable": true,
|
||||
"writeable": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 128,
|
||||
"commandClassName": "Battery",
|
||||
"property": "level",
|
||||
"propertyName": "level",
|
||||
"ccVersion": 1,
|
||||
"metadata": {
|
||||
"type": "number",
|
||||
"readable": true,
|
||||
"writeable": false,
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"unit": "%",
|
||||
"label": "Battery level"
|
||||
},
|
||||
"value": 53
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 128,
|
||||
"commandClassName": "Battery",
|
||||
"property": "isLow",
|
||||
"propertyName": "isLow",
|
||||
"ccVersion": 1,
|
||||
"metadata": {
|
||||
"type": "boolean",
|
||||
"readable": true,
|
||||
"writeable": false,
|
||||
"label": "Low battery level"
|
||||
},
|
||||
"value": false
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 132,
|
||||
"commandClassName": "Wake Up",
|
||||
"property": "wakeUpInterval",
|
||||
"propertyName": "wakeUpInterval",
|
||||
"ccVersion": 1,
|
||||
"metadata": {
|
||||
"type": "number",
|
||||
"readable": false,
|
||||
"writeable": true,
|
||||
"min": 60,
|
||||
"max": 1800,
|
||||
"label": "Wake Up interval",
|
||||
"steps": 60,
|
||||
"default": 300
|
||||
},
|
||||
"value": 300
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 132,
|
||||
"commandClassName": "Wake Up",
|
||||
"property": "controllerNodeId",
|
||||
"propertyName": "controllerNodeId",
|
||||
"ccVersion": 1,
|
||||
"metadata": {
|
||||
"type": "any",
|
||||
"readable": true,
|
||||
"writeable": false,
|
||||
"label": "Node ID of the controller"
|
||||
},
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 134,
|
||||
"commandClassName": "Version",
|
||||
"property": "libraryType",
|
||||
"propertyName": "libraryType",
|
||||
"ccVersion": 1,
|
||||
"metadata": {
|
||||
"type": "any",
|
||||
"readable": true,
|
||||
"writeable": false,
|
||||
"label": "Library type"
|
||||
},
|
||||
"value": 6
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 134,
|
||||
"commandClassName": "Version",
|
||||
"property": "protocolVersion",
|
||||
"propertyName": "protocolVersion",
|
||||
"ccVersion": 1,
|
||||
"metadata": {
|
||||
"type": "any",
|
||||
"readable": true,
|
||||
"writeable": false,
|
||||
"label": "Z-Wave protocol version"
|
||||
},
|
||||
"value": "3.67"
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 134,
|
||||
"commandClassName": "Version",
|
||||
"property": "firmwareVersions",
|
||||
"propertyName": "firmwareVersions",
|
||||
"ccVersion": 1,
|
||||
"metadata": {
|
||||
"type": "any",
|
||||
"readable": true,
|
||||
"writeable": false,
|
||||
"label": "Z-Wave chip firmware versions"
|
||||
},
|
||||
"value": [
|
||||
"1.1"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
1181
tests/fixtures/zwave_js/climate_heatit_z_trm3_state.json
vendored
Normal file
1181
tests/fixtures/zwave_js/climate_heatit_z_trm3_state.json
vendored
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue