mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 20:57:21 +00:00
Reduce coverage gaps for zwave_js (#79520)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
f3007b22c4
commit
9b7eb6b5a1
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
"""
|
"""
|
||||||
|
@ -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
|
||||||
|
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 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
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user