mirror of
https://github.com/home-assistant/core.git
synced 2025-04-25 09:47:52 +00:00
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:
parent
ba31d7d1b4
commit
1b81849271
@ -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
|
||||
|
@ -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."""
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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(),
|
||||
)
|
||||
|
1444
tests/fixtures/zwave_js/climate_heatit_z_trm2fx_state.json
vendored
Normal file
1444
tests/fixtures/zwave_js/climate_heatit_z_trm2fx_state.json
vendored
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user