Reduce coverage gaps for zwave_js (#79520)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
f3007b22c4
commit
9b7eb6b5a1
7 changed files with 170 additions and 53 deletions
|
@ -142,7 +142,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
await client.connect()
|
||||
except InvalidServerVersion as err:
|
||||
if use_addon:
|
||||
async_ensure_addon_updated(hass)
|
||||
addon_manager = _get_addon_manager(hass)
|
||||
addon_manager.async_schedule_update_addon(catch_error=True)
|
||||
else:
|
||||
async_create_issue(
|
||||
hass,
|
||||
|
@ -205,8 +206,7 @@ async def start_client(
|
|||
|
||||
LOGGER.info("Connection to Zwave JS Server initialized")
|
||||
|
||||
if client.driver is None:
|
||||
raise RuntimeError("Driver not ready.")
|
||||
assert client.driver
|
||||
|
||||
await driver_events.setup(client.driver)
|
||||
|
||||
|
@ -789,17 +789,13 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
info = hass.data[DOMAIN][entry.entry_id]
|
||||
driver_events: DriverEvents = info[DATA_DRIVER_EVENTS]
|
||||
|
||||
tasks: list[asyncio.Task | Coroutine] = []
|
||||
for platform, task in driver_events.platform_setup_tasks.items():
|
||||
if task.done():
|
||||
tasks.append(
|
||||
tasks: list[Coroutine] = [
|
||||
hass.config_entries.async_forward_entry_unload(entry, platform)
|
||||
)
|
||||
else:
|
||||
task.cancel()
|
||||
tasks.append(task)
|
||||
for platform, task in driver_events.platform_setup_tasks.items()
|
||||
if not task.cancel()
|
||||
]
|
||||
|
||||
unload_ok = all(await asyncio.gather(*tasks))
|
||||
unload_ok = all(await asyncio.gather(*tasks)) if tasks else True
|
||||
|
||||
if DATA_CLIENT_LISTEN_TASK in info:
|
||||
await disconnect_client(hass, entry)
|
||||
|
@ -842,9 +838,7 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
|||
|
||||
async def async_ensure_addon_running(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Ensure that Z-Wave JS add-on is installed and running."""
|
||||
addon_manager: AddonManager = get_addon_manager(hass)
|
||||
if addon_manager.task_in_progress():
|
||||
raise ConfigEntryNotReady
|
||||
addon_manager = _get_addon_manager(hass)
|
||||
try:
|
||||
addon_info = await addon_manager.async_get_addon_info()
|
||||
except AddonError as err:
|
||||
|
@ -911,9 +905,9 @@ async def async_ensure_addon_running(hass: HomeAssistant, entry: ConfigEntry) ->
|
|||
|
||||
|
||||
@callback
|
||||
def async_ensure_addon_updated(hass: HomeAssistant) -> None:
|
||||
def _get_addon_manager(hass: HomeAssistant) -> AddonManager:
|
||||
"""Ensure that Z-Wave JS add-on is updated and running."""
|
||||
addon_manager: AddonManager = get_addon_manager(hass)
|
||||
if addon_manager.task_in_progress():
|
||||
raise ConfigEntryNotReady
|
||||
addon_manager.async_schedule_update_addon(catch_error=True)
|
||||
return addon_manager
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
from dataclasses import astuple, dataclass
|
||||
from typing import Any
|
||||
|
||||
from zwave_js_server.client import Client
|
||||
|
@ -21,27 +20,13 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|||
|
||||
from .const import DATA_CLIENT, DOMAIN, USER_AGENT
|
||||
from .helpers import (
|
||||
ZwaveValueMatcher,
|
||||
get_home_and_node_id_from_device_entry,
|
||||
get_state_key_from_unique_id,
|
||||
get_value_id_from_unique_id,
|
||||
value_matches_matcher,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ZwaveValueMatcher:
|
||||
"""Class to allow matching a Z-Wave Value."""
|
||||
|
||||
property_: str | int | None = None
|
||||
command_class: int | None = None
|
||||
endpoint: int | None = None
|
||||
property_key: str | int | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""Post initialization check."""
|
||||
if all(val is None for val in astuple(self)):
|
||||
raise ValueError("At least one of the fields must be set.")
|
||||
|
||||
|
||||
KEYS_TO_REDACT = {"homeId", "location"}
|
||||
|
||||
VALUES_TO_REDACT = (
|
||||
|
@ -55,21 +40,7 @@ def redact_value_of_zwave_value(zwave_value: ValueDataType) -> ValueDataType:
|
|||
if zwave_value.get("value") in (None, ""):
|
||||
return zwave_value
|
||||
for value_to_redact in VALUES_TO_REDACT:
|
||||
command_class = None
|
||||
if "commandClass" in zwave_value:
|
||||
command_class = CommandClass(zwave_value["commandClass"])
|
||||
zwave_value_id = ZwaveValueMatcher(
|
||||
property_=zwave_value.get("property"),
|
||||
command_class=command_class,
|
||||
endpoint=zwave_value.get("endpoint"),
|
||||
property_key=zwave_value.get("propertyKey"),
|
||||
)
|
||||
if all(
|
||||
redacted_field_val is None or redacted_field_val == zwave_value_field_val
|
||||
for redacted_field_val, zwave_value_field_val in zip(
|
||||
astuple(value_to_redact), astuple(zwave_value_id)
|
||||
)
|
||||
):
|
||||
if value_matches_matcher(value_to_redact, zwave_value):
|
||||
redacted_value: ValueDataType = deepcopy(zwave_value)
|
||||
redacted_value["value"] = REDACTED
|
||||
return redacted_value
|
||||
|
|
|
@ -2,18 +2,19 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import astuple, dataclass
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
import voluptuous as vol
|
||||
from zwave_js_server.client import Client as ZwaveClient
|
||||
from zwave_js_server.const import ConfigurationValueType
|
||||
from zwave_js_server.const import CommandClass, ConfigurationValueType
|
||||
from zwave_js_server.model.driver import Driver
|
||||
from zwave_js_server.model.node import Node as ZwaveNode
|
||||
from zwave_js_server.model.value import (
|
||||
ConfigurationValue,
|
||||
Value as ZwaveValue,
|
||||
ValueDataType,
|
||||
get_value_id_str,
|
||||
)
|
||||
|
||||
|
@ -55,6 +56,42 @@ class ZwaveValueID:
|
|||
property_key: str | int | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ZwaveValueMatcher:
|
||||
"""Class to allow matching a Z-Wave Value."""
|
||||
|
||||
property_: str | int | None = None
|
||||
command_class: int | None = None
|
||||
endpoint: int | None = None
|
||||
property_key: str | int | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""Post initialization check."""
|
||||
if all(val is None for val in astuple(self)):
|
||||
raise ValueError("At least one of the fields must be set.")
|
||||
|
||||
|
||||
def value_matches_matcher(
|
||||
matcher: ZwaveValueMatcher, value_data: ValueDataType
|
||||
) -> bool:
|
||||
"""Return whether value matches matcher."""
|
||||
command_class = None
|
||||
if "commandClass" in value_data:
|
||||
command_class = CommandClass(value_data["commandClass"])
|
||||
zwave_value_id = ZwaveValueMatcher(
|
||||
property_=value_data.get("property"),
|
||||
command_class=command_class,
|
||||
endpoint=value_data.get("endpoint"),
|
||||
property_key=value_data.get("propertyKey"),
|
||||
)
|
||||
return all(
|
||||
redacted_field_val is None or redacted_field_val == zwave_value_field_val
|
||||
for redacted_field_val, zwave_value_field_val in zip(
|
||||
astuple(matcher), astuple(zwave_value_id)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def get_value_id_from_unique_id(unique_id: str) -> str | None:
|
||||
"""
|
||||
|
|
|
@ -1,4 +1,16 @@
|
|||
"""Provide common test tools for Z-Wave JS."""
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
from typing import Any
|
||||
|
||||
from zwave_js_server.model.node.data_model import NodeDataType
|
||||
|
||||
from homeassistant.components.zwave_js.helpers import (
|
||||
ZwaveValueMatcher,
|
||||
value_matches_matcher,
|
||||
)
|
||||
|
||||
AIR_TEMPERATURE_SENSOR = "sensor.multisensor_6_air_temperature"
|
||||
BATTERY_SENSOR = "sensor.multisensor_6_battery_level"
|
||||
TAMPER_SENSOR = "binary_sensor.multisensor_6_tampering_product_cover_removed"
|
||||
|
@ -37,3 +49,16 @@ HUMIDIFIER_ADC_T3000_ENTITY = "humidifier.adc_t3000_humidifier"
|
|||
DEHUMIDIFIER_ADC_T3000_ENTITY = "humidifier.adc_t3000_dehumidifier"
|
||||
|
||||
PROPERTY_ULTRAVIOLET = "Ultraviolet"
|
||||
|
||||
|
||||
def replace_value_of_zwave_value(
|
||||
node_data: NodeDataType, matchers: list[ZwaveValueMatcher], new_value: Any
|
||||
) -> NodeDataType:
|
||||
"""Replace the value of a zwave value that matches the input matchers."""
|
||||
new_node_data = deepcopy(node_data)
|
||||
for value_data in new_node_data["values"]:
|
||||
for matcher in matchers:
|
||||
if value_matches_matcher(matcher, value_data):
|
||||
value_data["value"] = new_value
|
||||
|
||||
return new_node_data
|
||||
|
|
15
tests/components/zwave_js/test_addon.py
Normal file
15
tests/components/zwave_js/test_addon.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
"""Tests for Z-Wave JS addon module."""
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.zwave_js.addon import AddonError, get_addon_manager
|
||||
|
||||
|
||||
async def test_not_installed_raises_exception(hass, addon_not_installed):
|
||||
"""Test addon not installed raises exception."""
|
||||
addon_manager = get_addon_manager(hass)
|
||||
|
||||
with pytest.raises(AddonError):
|
||||
await addon_manager.async_configure_addon("/test", "123", "456", "789", "012")
|
||||
|
||||
with pytest.raises(AddonError):
|
||||
await addon_manager.async_update_addon()
|
|
@ -3,7 +3,7 @@ from zwave_js_server.event import Event
|
|||
from zwave_js_server.model.node import Node
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
|
||||
from homeassistant.const import ATTR_DEVICE_CLASS, STATE_OFF, STATE_ON
|
||||
from homeassistant.const import ATTR_DEVICE_CLASS, STATE_OFF, STATE_ON, STATE_UNKNOWN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity import EntityCategory
|
||||
|
@ -69,6 +69,29 @@ async def test_enabled_legacy_sensor(hass, ecolink_door_sensor, integration):
|
|||
state = hass.states.get(ENABLED_LEGACY_BINARY_SENSOR)
|
||||
assert state.state == STATE_ON
|
||||
|
||||
# Test state updates from value updated event
|
||||
event = Event(
|
||||
type="value updated",
|
||||
data={
|
||||
"source": "node",
|
||||
"event": "value updated",
|
||||
"nodeId": 53,
|
||||
"args": {
|
||||
"commandClassName": "Binary Sensor",
|
||||
"commandClass": 48,
|
||||
"endpoint": 0,
|
||||
"property": "Any",
|
||||
"newValue": None,
|
||||
"prevValue": True,
|
||||
"propertyName": "Any",
|
||||
},
|
||||
},
|
||||
)
|
||||
node.receive_event(event)
|
||||
|
||||
state = hass.states.get(ENABLED_LEGACY_BINARY_SENSOR)
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
|
||||
async def test_disabled_legacy_sensor(hass, multisensor_6, integration):
|
||||
"""Test disabled legacy boolean binary sensor."""
|
||||
|
@ -198,3 +221,26 @@ async def test_property_sensor_door_status(hass, lock_august_pro, integration):
|
|||
state = hass.states.get(PROPERTY_DOOR_STATUS_BINARY_SENSOR)
|
||||
assert state
|
||||
assert state.state == STATE_OFF
|
||||
|
||||
# door state unknown
|
||||
event = Event(
|
||||
type="value updated",
|
||||
data={
|
||||
"source": "node",
|
||||
"event": "value updated",
|
||||
"nodeId": 6,
|
||||
"args": {
|
||||
"commandClassName": "Door Lock",
|
||||
"commandClass": 98,
|
||||
"endpoint": 0,
|
||||
"property": "doorStatus",
|
||||
"newValue": None,
|
||||
"prevValue": "open",
|
||||
"propertyName": "doorStatus",
|
||||
},
|
||||
},
|
||||
)
|
||||
node.receive_event(event)
|
||||
state = hass.states.get(PROPERTY_DOOR_STATUS_BINARY_SENSOR)
|
||||
assert state
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
"""Test the Z-Wave JS climate platform."""
|
||||
import pytest
|
||||
from zwave_js_server.const import CommandClass
|
||||
from zwave_js_server.const.command_class.thermostat import (
|
||||
THERMOSTAT_OPERATING_STATE_PROPERTY,
|
||||
)
|
||||
from zwave_js_server.event import Event
|
||||
from zwave_js_server.model.node import Node
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_CURRENT_HUMIDITY,
|
||||
|
@ -25,6 +30,7 @@ from homeassistant.components.climate import (
|
|||
HVACMode,
|
||||
)
|
||||
from homeassistant.components.zwave_js.climate import ATTR_FAN_STATE
|
||||
from homeassistant.components.zwave_js.helpers import ZwaveValueMatcher
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_SUPPORTED_FEATURES,
|
||||
|
@ -37,6 +43,7 @@ from .common import (
|
|||
CLIMATE_FLOOR_THERMOSTAT_ENTITY,
|
||||
CLIMATE_MAIN_HEAT_ACTIONNER,
|
||||
CLIMATE_RADIO_THERMOSTAT_ENTITY,
|
||||
replace_value_of_zwave_value,
|
||||
)
|
||||
|
||||
|
||||
|
@ -632,3 +639,25 @@ async def test_temp_unit_fix(
|
|||
state = hass.states.get("climate.z_wave_thermostat")
|
||||
assert state
|
||||
assert state.attributes["current_temperature"] == 21.1
|
||||
|
||||
|
||||
async def test_thermostat_unknown_values(
|
||||
hass, client, climate_radio_thermostat_ct100_plus_state, integration
|
||||
):
|
||||
"""Test a thermostat v2 with unknown values."""
|
||||
node_state = replace_value_of_zwave_value(
|
||||
climate_radio_thermostat_ct100_plus_state,
|
||||
[
|
||||
ZwaveValueMatcher(
|
||||
THERMOSTAT_OPERATING_STATE_PROPERTY,
|
||||
command_class=CommandClass.THERMOSTAT_OPERATING_STATE,
|
||||
)
|
||||
],
|
||||
None,
|
||||
)
|
||||
node = Node(client, node_state)
|
||||
client.driver.controller.emit("node added", {"node": node})
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(CLIMATE_RADIO_THERMOSTAT_ENTITY)
|
||||
|
||||
assert ATTR_HVAC_ACTION not in state.attributes
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue