Add zwave_js support for HeatIt Z-TRM2fx (#50317)

* Add zwave_js support for HeatIt Z-TRM2fx

* fix docstring

* use AwesomeVersion to support firmware version ranges

* add guard against empty firmware range

* switch guard approach to raise exception sooner

* make post init more generic

* Set up firmware range schema as AwesomeVersion during initialization

* Update homeassistant/components/zwave_js/discovery.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Allow min_ver and max_ver to be None

* fix docstring

* reduce import scope

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Raman Gupta 2021-05-09 15:28:35 -04:00 committed by GitHub
parent ba31d7d1b4
commit 1b81849271
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 1610 additions and 4 deletions

View File

@ -2,9 +2,10 @@
from __future__ import annotations
from collections.abc import Generator
from dataclasses import dataclass
from dataclasses import asdict, dataclass, field
from typing import Any
from awesomeversion import AwesomeVersion
from zwave_js_server.const import THERMOSTAT_CURRENT_TEMP_PROPERTY, CommandClass
from zwave_js_server.model.device_class import DeviceClassItem
from zwave_js_server.model.node import Node as ZwaveNode
@ -19,6 +20,33 @@ from .discovery_data_template import (
)
class DataclassMustHaveAtLeastOne:
"""A dataclass that must have at least one input parameter that is not None."""
def __post_init__(self) -> None:
"""Post dataclass initialization."""
if all(val is None for val in asdict(self).values()):
raise ValueError("At least one input parameter must not be None")
@dataclass
class FirmwareVersionRange(DataclassMustHaveAtLeastOne):
"""Firmware version range dictionary."""
min: str | None = None
max: str | None = None
min_ver: AwesomeVersion | None = field(default=None, init=False)
max_ver: AwesomeVersion | None = field(default=None, init=False)
def __post_init__(self) -> None:
"""Post dataclass initialization."""
super().__post_init__()
if self.min:
self.min_ver = AwesomeVersion(self.min)
if self.max:
self.max_ver = AwesomeVersion(self.max)
@dataclass
class ZwaveDiscoveryInfo:
"""Info discovered from (primary) ZWave Value to create entity."""
@ -42,7 +70,7 @@ class ZwaveDiscoveryInfo:
@dataclass
class ZWaveValueDiscoverySchema:
class ZWaveValueDiscoverySchema(DataclassMustHaveAtLeastOne):
"""Z-Wave Value discovery schema.
The Z-Wave Value must match these conditions.
@ -89,6 +117,8 @@ class ZWaveDiscoverySchema:
product_id: set[int] | None = None
# [optional] the node's product_type must match ANY of these values
product_type: set[int] | None = None
# [optional] the node's firmware_version must be within this range
firmware_version_range: FirmwareVersionRange | None = None
# [optional] the node's firmware_version must match ANY of these values
firmware_version: set[str] | None = None
# [optional] the node's basic device class must match ANY of these values
@ -274,6 +304,42 @@ DISCOVERY_SCHEMAS = [
ZwaveValueID(2, CommandClass.CONFIGURATION, endpoint=0),
),
),
# Heatit Z-TRM2fx
ZWaveDiscoverySchema(
platform="climate",
hint="dynamic_current_temp",
manufacturer_id={0x019B},
product_id={0x0202},
product_type={0x0003},
firmware_version_range=FirmwareVersionRange(min="3.0"),
primary_value=ZWaveValueDiscoverySchema(
command_class={CommandClass.THERMOSTAT_MODE},
property={"mode"},
type={"number"},
),
data_template=DynamicCurrentTempClimateDataTemplate(
{
# External Sensor
"A2": ZwaveValueID(
THERMOSTAT_CURRENT_TEMP_PROPERTY,
CommandClass.SENSOR_MULTILEVEL,
endpoint=2,
),
"A2F": ZwaveValueID(
THERMOSTAT_CURRENT_TEMP_PROPERTY,
CommandClass.SENSOR_MULTILEVEL,
endpoint=2,
),
# Floor sensor
"F": ZwaveValueID(
THERMOSTAT_CURRENT_TEMP_PROPERTY,
CommandClass.SENSOR_MULTILEVEL,
endpoint=3,
),
},
ZwaveValueID(2, CommandClass.CONFIGURATION, endpoint=0),
),
),
# ====== START OF CONFIG PARAMETER SPECIFIC MAPPING SCHEMAS =======
# Door lock mode config parameter. Functionality equivalent to Notification CC
# list sensors.
@ -541,6 +607,21 @@ def async_discover_values(node: ZwaveNode) -> Generator[ZwaveDiscoveryInfo, None
):
continue
# check firmware_version_range
if schema.firmware_version_range is not None and (
(
schema.firmware_version_range.min is not None
and schema.firmware_version_range.min_ver
> AwesomeVersion(value.node.firmware_version)
)
or (
schema.firmware_version_range.max is not None
and schema.firmware_version_range.max_ver
< AwesomeVersion(value.node.firmware_version)
)
):
continue
# check firmware_version
if (
schema.firmware_version is not None

View File

@ -240,6 +240,12 @@ def climate_heatit_z_trm3_state_fixture():
return json.loads(load_fixture("zwave_js/climate_heatit_z_trm3_state.json"))
@pytest.fixture(name="climate_heatit_z_trm2fx_state", scope="session")
def climate_heatit_z_trm2fx_state_fixture():
"""Load the climate HEATIT Z-TRM2fx thermostat node state fixture data."""
return json.loads(load_fixture("zwave_js/climate_heatit_z_trm2fx_state.json"))
@pytest.fixture(name="nortek_thermostat_state", scope="session")
def nortek_thermostat_state_fixture():
"""Load the nortek thermostat node state fixture data."""
@ -484,6 +490,14 @@ def climate_heatit_z_trm3_fixture(client, climate_heatit_z_trm3_state):
return node
@pytest.fixture(name="climate_heatit_z_trm2fx")
def climate_heatit_z_trm2fx_fixture(client, climate_heatit_z_trm2fx_state):
"""Mock a climate radio HEATIT Z-TRM2fx node."""
node = Node(client, copy.deepcopy(climate_heatit_z_trm2fx_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

@ -28,6 +28,7 @@ from homeassistant.components.climate.const import (
SERVICE_SET_PRESET_MODE,
SERVICE_SET_TEMPERATURE,
SUPPORT_FAN_MODE,
SUPPORT_PRESET_MODE,
SUPPORT_TARGET_TEMPERATURE,
SUPPORT_TARGET_TEMPERATURE_RANGE,
)
@ -436,8 +437,10 @@ async def test_setpoint_thermostat(hass, client, climate_danfoss_lc_13, integrat
client.async_send_command_no_wait.reset_mock()
async def test_thermostat_heatit(hass, client, climate_heatit_z_trm3, integration):
"""Test a thermostat v2 command class entity."""
async def test_thermostat_heatit_z_trm3(
hass, client, climate_heatit_z_trm3, integration
):
"""Test a heatit Z-TRM3 entity."""
node = climate_heatit_z_trm3
state = hass.states.get(CLIMATE_FLOOR_THERMOSTAT_ENTITY)
@ -501,6 +504,53 @@ async def test_thermostat_heatit(hass, client, climate_heatit_z_trm3, integratio
assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 25.5
async def test_thermostat_heatit_z_trm2fx(
hass, client, climate_heatit_z_trm2fx, integration
):
"""Test a heatit Z-TRM2fx entity."""
node = climate_heatit_z_trm2fx
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,
HVAC_MODE_COOL,
]
assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 28.8
assert state.attributes[ATTR_TEMPERATURE] == 29
assert (
state.attributes[ATTR_SUPPORTED_FEATURES]
== SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE
)
assert state.attributes[ATTR_MIN_TEMP] == 7
assert state.attributes[ATTR_MAX_TEMP] == 35
# Try switching to external sensor
event = Event(
type="value updated",
data={
"source": "node",
"event": "value updated",
"nodeId": 24,
"args": {
"commandClassName": "Configuration",
"commandClass": 112,
"endpoint": 0,
"property": 2,
"propertyName": "Sensor mode",
"newValue": 4,
"prevValue": 2,
},
},
)
node.receive_event(event)
state = hass.states.get(CLIMATE_FLOOR_THERMOSTAT_ENTITY)
assert state
assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 0
async def test_thermostat_srt321_hrt4_zw(hass, client, srt321_hrt4_zw, integration):
"""Test a climate entity from a HRT4-ZW / SRT321 thermostat device.

View File

@ -1,4 +1,11 @@
"""Test discovery of entities for device-specific schemas for the Z-Wave JS integration."""
import pytest
from homeassistant.components.zwave_js.discovery import (
FirmwareVersionRange,
ZWaveDiscoverySchema,
ZWaveValueDiscoverySchema,
)
async def test_iblinds_v2(hass, client, iblinds_v2, integration):
@ -48,3 +55,13 @@ async def test_vision_security_zl7432(
state = hass.states.get(entity_id)
assert state
assert state.attributes["assumed_state"]
async def test_firmware_version_range_exception(hass):
"""Test FirmwareVersionRange exception."""
with pytest.raises(ValueError):
ZWaveDiscoverySchema(
"test",
ZWaveValueDiscoverySchema(command_class=1),
firmware_version_range=FirmwareVersionRange(),
)

File diff suppressed because it is too large Load Diff