Add support for climate setpoint thermostats to zwave_js (#45890)

This commit is contained in:
Tobias Sauerwein 2021-02-03 13:59:19 +01:00 committed by GitHub
parent 9998fe3684
commit 5615ab4c25
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 1699 additions and 4 deletions

View file

@ -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

View file

@ -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(

View file

@ -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."""

View file

@ -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

View 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"
]
}
]
}

File diff suppressed because it is too large Load diff