Reduce coverage gaps for zwave_js (#79520)

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Raman Gupta 2022-10-03 14:24:11 -04:00 committed by GitHub
parent f3007b22c4
commit 9b7eb6b5a1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 170 additions and 53 deletions

View file

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

View file

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

View file

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

View file

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

View 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()

View file

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

View file

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