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() await client.connect()
except InvalidServerVersion as err: except InvalidServerVersion as err:
if use_addon: if use_addon:
async_ensure_addon_updated(hass) addon_manager = _get_addon_manager(hass)
addon_manager.async_schedule_update_addon(catch_error=True)
else: else:
async_create_issue( async_create_issue(
hass, hass,
@ -205,8 +206,7 @@ async def start_client(
LOGGER.info("Connection to Zwave JS Server initialized") LOGGER.info("Connection to Zwave JS Server initialized")
if client.driver is None: assert client.driver
raise RuntimeError("Driver not ready.")
await driver_events.setup(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] info = hass.data[DOMAIN][entry.entry_id]
driver_events: DriverEvents = info[DATA_DRIVER_EVENTS] driver_events: DriverEvents = info[DATA_DRIVER_EVENTS]
tasks: list[asyncio.Task | Coroutine] = [] tasks: list[Coroutine] = [
for platform, task in driver_events.platform_setup_tasks.items(): hass.config_entries.async_forward_entry_unload(entry, platform)
if task.done(): for platform, task in driver_events.platform_setup_tasks.items()
tasks.append( if not task.cancel()
hass.config_entries.async_forward_entry_unload(entry, platform) ]
)
else:
task.cancel()
tasks.append(task)
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: if DATA_CLIENT_LISTEN_TASK in info:
await disconnect_client(hass, entry) 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: async def async_ensure_addon_running(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Ensure that Z-Wave JS add-on is installed and running.""" """Ensure that Z-Wave JS add-on is installed and running."""
addon_manager: AddonManager = get_addon_manager(hass) addon_manager = _get_addon_manager(hass)
if addon_manager.task_in_progress():
raise ConfigEntryNotReady
try: try:
addon_info = await addon_manager.async_get_addon_info() addon_info = await addon_manager.async_get_addon_info()
except AddonError as err: except AddonError as err:
@ -911,9 +905,9 @@ async def async_ensure_addon_running(hass: HomeAssistant, entry: ConfigEntry) ->
@callback @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.""" """Ensure that Z-Wave JS add-on is updated and running."""
addon_manager: AddonManager = get_addon_manager(hass) addon_manager: AddonManager = get_addon_manager(hass)
if addon_manager.task_in_progress(): if addon_manager.task_in_progress():
raise ConfigEntryNotReady 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 __future__ import annotations
from copy import deepcopy from copy import deepcopy
from dataclasses import astuple, dataclass
from typing import Any from typing import Any
from zwave_js_server.client import Client 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 .const import DATA_CLIENT, DOMAIN, USER_AGENT
from .helpers import ( from .helpers import (
ZwaveValueMatcher,
get_home_and_node_id_from_device_entry, get_home_and_node_id_from_device_entry,
get_state_key_from_unique_id, get_state_key_from_unique_id,
get_value_id_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"} KEYS_TO_REDACT = {"homeId", "location"}
VALUES_TO_REDACT = ( VALUES_TO_REDACT = (
@ -55,21 +40,7 @@ def redact_value_of_zwave_value(zwave_value: ValueDataType) -> ValueDataType:
if zwave_value.get("value") in (None, ""): if zwave_value.get("value") in (None, ""):
return zwave_value return zwave_value
for value_to_redact in VALUES_TO_REDACT: for value_to_redact in VALUES_TO_REDACT:
command_class = None if value_matches_matcher(value_to_redact, zwave_value):
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)
)
):
redacted_value: ValueDataType = deepcopy(zwave_value) redacted_value: ValueDataType = deepcopy(zwave_value)
redacted_value["value"] = REDACTED redacted_value["value"] = REDACTED
return redacted_value return redacted_value

View File

@ -2,18 +2,19 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass from dataclasses import astuple, dataclass
import logging import logging
from typing import Any, cast from typing import Any, cast
import voluptuous as vol import voluptuous as vol
from zwave_js_server.client import Client as ZwaveClient 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.driver import Driver
from zwave_js_server.model.node import Node as ZwaveNode from zwave_js_server.model.node import Node as ZwaveNode
from zwave_js_server.model.value import ( from zwave_js_server.model.value import (
ConfigurationValue, ConfigurationValue,
Value as ZwaveValue, Value as ZwaveValue,
ValueDataType,
get_value_id_str, get_value_id_str,
) )
@ -55,6 +56,42 @@ class ZwaveValueID:
property_key: str | int | None = None 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 @callback
def get_value_id_from_unique_id(unique_id: str) -> str | None: 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.""" """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" AIR_TEMPERATURE_SENSOR = "sensor.multisensor_6_air_temperature"
BATTERY_SENSOR = "sensor.multisensor_6_battery_level" BATTERY_SENSOR = "sensor.multisensor_6_battery_level"
TAMPER_SENSOR = "binary_sensor.multisensor_6_tampering_product_cover_removed" 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" DEHUMIDIFIER_ADC_T3000_ENTITY = "humidifier.adc_t3000_dehumidifier"
PROPERTY_ULTRAVIOLET = "Ultraviolet" 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 zwave_js_server.model.node import Node
from homeassistant.components.binary_sensor import BinarySensorDeviceClass 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.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity import EntityCategory 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) state = hass.states.get(ENABLED_LEGACY_BINARY_SENSOR)
assert state.state == STATE_ON 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): async def test_disabled_legacy_sensor(hass, multisensor_6, integration):
"""Test disabled legacy boolean binary sensor.""" """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) state = hass.states.get(PROPERTY_DOOR_STATUS_BINARY_SENSOR)
assert state assert state
assert state.state == STATE_OFF 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.""" """Test the Z-Wave JS climate platform."""
import pytest 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.event import Event
from zwave_js_server.model.node import Node
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ATTR_CURRENT_HUMIDITY, ATTR_CURRENT_HUMIDITY,
@ -25,6 +30,7 @@ from homeassistant.components.climate import (
HVACMode, HVACMode,
) )
from homeassistant.components.zwave_js.climate import ATTR_FAN_STATE from homeassistant.components.zwave_js.climate import ATTR_FAN_STATE
from homeassistant.components.zwave_js.helpers import ZwaveValueMatcher
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_ENTITY_ID,
ATTR_SUPPORTED_FEATURES, ATTR_SUPPORTED_FEATURES,
@ -37,6 +43,7 @@ from .common import (
CLIMATE_FLOOR_THERMOSTAT_ENTITY, CLIMATE_FLOOR_THERMOSTAT_ENTITY,
CLIMATE_MAIN_HEAT_ACTIONNER, CLIMATE_MAIN_HEAT_ACTIONNER,
CLIMATE_RADIO_THERMOSTAT_ENTITY, 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") state = hass.states.get("climate.z_wave_thermostat")
assert state assert state
assert state.attributes["current_temperature"] == 21.1 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