mirror of
https://github.com/home-assistant/core.git
synced 2026-10-07 06:50:41 -04:00
Clean up Z-Wave JS conditions (#183782)
This commit is contained in:
@@ -2,7 +2,6 @@
|
||||
|
||||
import abc
|
||||
from collections.abc import Callable, Iterable
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING, Any, Unpack, override
|
||||
|
||||
import probatio
|
||||
@@ -12,7 +11,6 @@ from zwave_js_server.model.node import Node as ZwaveNode
|
||||
from homeassistant.const import ATTR_DEVICE_ID, CONF_OPTIONS
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.automation import move_top_level_schema_fields_to_options
|
||||
from homeassistant.helpers.condition import (
|
||||
ATTR_BEHAVIOR,
|
||||
BEHAVIOR_ALL,
|
||||
@@ -88,47 +86,25 @@ def _condition_schema(
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class _ResolvedNodes:
|
||||
"""Z-Wave nodes resolved from the targeted devices."""
|
||||
|
||||
nodes: set[ZwaveNode] = field(default_factory=set)
|
||||
unresolved: int = 0
|
||||
|
||||
|
||||
@callback
|
||||
def _async_resolve_nodes(
|
||||
hass: HomeAssistant, device_ids: Iterable[str]
|
||||
) -> _ResolvedNodes:
|
||||
"""Resolve targeted device IDs to Z-Wave nodes."""
|
||||
resolved = _ResolvedNodes()
|
||||
) -> set[ZwaveNode]:
|
||||
"""Resolve targeted device IDs to Z-Wave nodes, skipping any that don't resolve."""
|
||||
nodes: set[ZwaveNode] = set()
|
||||
for device_id in set(device_ids):
|
||||
try:
|
||||
node = async_get_node_from_device_id(hass, device_id)
|
||||
nodes.add(async_get_node_from_device_id(hass, device_id))
|
||||
except ValueError:
|
||||
resolved.unresolved += 1
|
||||
else:
|
||||
resolved.nodes.add(node)
|
||||
return resolved
|
||||
continue
|
||||
return nodes
|
||||
|
||||
|
||||
class _ZwaveNodeCondition(Condition):
|
||||
"""Base for conditions evaluated per Z-Wave node."""
|
||||
|
||||
options_schema_dict: dict[probatio.Marker, Any]
|
||||
_schema: probatio.Schema
|
||||
|
||||
@classmethod
|
||||
@override
|
||||
async def async_validate_complete_config(
|
||||
cls, hass: HomeAssistant, complete_config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate complete config."""
|
||||
complete_config = move_top_level_schema_fields_to_options(
|
||||
complete_config, cls.options_schema_dict
|
||||
)
|
||||
return await super().async_validate_complete_config(hass, complete_config)
|
||||
|
||||
@classmethod
|
||||
@override
|
||||
async def async_validate_config(
|
||||
@@ -140,10 +116,10 @@ class _ZwaveNodeCondition(Condition):
|
||||
if async_bypass_dynamic_config_validation(hass, {ATTR_DEVICE_ID: device_ids}):
|
||||
return config
|
||||
|
||||
resolved = _async_resolve_nodes(hass, device_ids)
|
||||
if not resolved.nodes:
|
||||
nodes = _async_resolve_nodes(hass, device_ids)
|
||||
if not nodes:
|
||||
raise probatio.Invalid("No nodes found for the given devices")
|
||||
cls._validate_nodes(resolved.nodes, config[CONF_OPTIONS])
|
||||
cls._validate_nodes(nodes, config[CONF_OPTIONS])
|
||||
return config
|
||||
|
||||
@classmethod
|
||||
@@ -164,20 +140,16 @@ class _ZwaveNodeCondition(Condition):
|
||||
@override
|
||||
def _async_check(self, **kwargs: Unpack[ConditionCheckParams]) -> bool:
|
||||
"""Test the condition against all targeted nodes."""
|
||||
resolved = _async_resolve_nodes(self._hass, self._options[ATTR_DEVICE_ID])
|
||||
if not resolved.nodes:
|
||||
return False
|
||||
behavior_all = self._options[ATTR_BEHAVIOR] == BEHAVIOR_ALL
|
||||
if behavior_all and resolved.unresolved:
|
||||
return False
|
||||
combine: Callable[[Iterable[object]], bool] = all if behavior_all else any
|
||||
return combine(self._node_matches(node) for node in resolved.nodes)
|
||||
nodes = _async_resolve_nodes(self._hass, self._options[ATTR_DEVICE_ID])
|
||||
combine: Callable[[Iterable[object]], bool] = (
|
||||
all if self._options[ATTR_BEHAVIOR] == BEHAVIOR_ALL else any
|
||||
)
|
||||
return combine(self._node_matches(node) for node in nodes)
|
||||
|
||||
|
||||
class NodeStatusCondition(_ZwaveNodeCondition):
|
||||
"""Test the status of Z-Wave nodes."""
|
||||
|
||||
options_schema_dict = _NODE_STATUS_OPTIONS_SCHEMA_DICT
|
||||
_schema = _condition_schema(_NODE_STATUS_OPTIONS_SCHEMA_DICT)
|
||||
|
||||
@override
|
||||
@@ -224,7 +196,6 @@ class _ZwaveValueCondition(_ZwaveNodeCondition):
|
||||
class ValueCondition(_ZwaveValueCondition):
|
||||
"""Test a Z-Wave value."""
|
||||
|
||||
options_schema_dict = _VALUE_OPTIONS_SCHEMA_DICT
|
||||
_schema = _condition_schema(_VALUE_OPTIONS_SCHEMA_DICT)
|
||||
|
||||
@classmethod
|
||||
@@ -247,7 +218,6 @@ class ValueCondition(_ZwaveValueCondition):
|
||||
class ConfigParameterCondition(_ZwaveValueCondition):
|
||||
"""Test a Z-Wave configuration parameter."""
|
||||
|
||||
options_schema_dict = _CONFIG_PARAMETER_OPTIONS_SCHEMA_DICT
|
||||
_schema = _condition_schema(_CONFIG_PARAMETER_OPTIONS_SCHEMA_DICT)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -423,42 +423,20 @@ async def test_config_parameter_with_bitmask(
|
||||
assert checker.async_check() is True
|
||||
|
||||
|
||||
async def test_top_level_fields_moved_to_options(
|
||||
@pytest.mark.parametrize(
|
||||
("behavior", "expected"),
|
||||
[("any", False), ("all", True)],
|
||||
)
|
||||
async def test_check_when_nodes_disappear(
|
||||
hass: HomeAssistant,
|
||||
client: MagicMock,
|
||||
lock_schlage_be469: Node,
|
||||
integration: MockConfigEntry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
behavior: str,
|
||||
expected: bool,
|
||||
) -> None:
|
||||
"""Test top level option fields are moved into the options block."""
|
||||
device_id = _device_id(device_registry, client, lock_schlage_be469, integration)
|
||||
validated = await condition.async_validate_condition_config(
|
||||
hass,
|
||||
cv.CONDITION_SCHEMA(
|
||||
{
|
||||
"condition": f"{DOMAIN}.node_status",
|
||||
"device_id": device_id,
|
||||
"status": "alive",
|
||||
"behavior": "all",
|
||||
}
|
||||
),
|
||||
)
|
||||
assert validated["options"] == {
|
||||
"device_id": [device_id],
|
||||
"behavior": "all",
|
||||
"status": "alive",
|
||||
}
|
||||
assert "status" not in validated
|
||||
|
||||
|
||||
async def test_check_false_when_nodes_disappear(
|
||||
hass: HomeAssistant,
|
||||
client: MagicMock,
|
||||
lock_schlage_be469: Node,
|
||||
integration: MockConfigEntry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
) -> None:
|
||||
"""Test the condition is False once the devices no longer resolve to nodes."""
|
||||
"""Test an empty node set follows any/all semantics like entity conditions."""
|
||||
checker = await _checker(
|
||||
hass,
|
||||
{
|
||||
@@ -467,19 +445,17 @@ async def test_check_false_when_nodes_disappear(
|
||||
"device_id": _device_id(
|
||||
device_registry, client, lock_schlage_be469, integration
|
||||
),
|
||||
"behavior": behavior,
|
||||
"status": "alive",
|
||||
},
|
||||
},
|
||||
)
|
||||
assert checker.async_check() is True
|
||||
await hass.config_entries.async_unload(integration.entry_id)
|
||||
assert checker.async_check() is False
|
||||
assert checker.async_check() is expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("behavior", "expected"),
|
||||
[("any", True), ("all", False)],
|
||||
)
|
||||
@pytest.mark.parametrize("behavior", ["any", "all"])
|
||||
async def test_partially_unresolved_target(
|
||||
hass: HomeAssistant,
|
||||
client: MagicMock,
|
||||
@@ -488,9 +464,8 @@ async def test_partially_unresolved_target(
|
||||
integration: MockConfigEntry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
behavior: str,
|
||||
expected: bool,
|
||||
) -> None:
|
||||
"""Test a targeted Z-Wave node that cannot be resolved fails an all behavior."""
|
||||
"""Test a targeted Z-Wave node that cannot be resolved is ignored."""
|
||||
device_ids = [
|
||||
_device_id(device_registry, client, lock_schlage_be469, integration),
|
||||
_device_id(device_registry, client, multisensor_6, integration),
|
||||
@@ -507,7 +482,7 @@ async def test_partially_unresolved_target(
|
||||
},
|
||||
},
|
||||
)
|
||||
assert checker.async_check() is expected
|
||||
assert checker.async_check() is True
|
||||
|
||||
|
||||
async def test_config_parameter_missing_on_node(
|
||||
@@ -539,7 +514,7 @@ async def test_condition_description_fields_match_schema(
|
||||
hass: HomeAssistant, condition_type: str
|
||||
) -> None:
|
||||
"""Test the described fields and required flags match the options schema."""
|
||||
schema = CONDITIONS[condition_type].options_schema_dict
|
||||
schema = next(iter(CONDITIONS[condition_type]._schema.schema.values()))
|
||||
descriptions = await condition.async_get_all_descriptions(hass)
|
||||
description = descriptions[f"{DOMAIN}.{condition_type}"]
|
||||
# Nodes are targeted with a device selector field, not a target selector
|
||||
@@ -587,14 +562,16 @@ async def test_node_status_selector_translations(hass: HomeAssistant) -> None:
|
||||
} == {"alive", "asleep", "awake", "dead"}
|
||||
|
||||
|
||||
async def test_non_zwave_device_is_unresolved(
|
||||
@pytest.mark.parametrize("behavior", ["any", "all"])
|
||||
async def test_non_zwave_device_is_ignored(
|
||||
hass: HomeAssistant,
|
||||
client: MagicMock,
|
||||
lock_schlage_be469: Node,
|
||||
integration: MockConfigEntry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
behavior: str,
|
||||
) -> None:
|
||||
"""Test a device from another integration counts as an unresolved node."""
|
||||
"""Test a device from another integration is ignored when checking."""
|
||||
other_entry = MockConfigEntry(domain="other")
|
||||
other_entry.add_to_hass(hass)
|
||||
other_device = device_registry.async_get_or_create(
|
||||
@@ -604,32 +581,18 @@ async def test_non_zwave_device_is_unresolved(
|
||||
_device_id(device_registry, client, lock_schlage_be469, integration),
|
||||
other_device.id,
|
||||
]
|
||||
assert (
|
||||
await _checker(
|
||||
hass,
|
||||
{
|
||||
"condition": f"{DOMAIN}.node_status",
|
||||
"options": {
|
||||
"device_id": device_ids,
|
||||
"behavior": "any",
|
||||
"status": "alive",
|
||||
},
|
||||
checker = await _checker(
|
||||
hass,
|
||||
{
|
||||
"condition": f"{DOMAIN}.node_status",
|
||||
"options": {
|
||||
"device_id": device_ids,
|
||||
"behavior": behavior,
|
||||
"status": "alive",
|
||||
},
|
||||
)
|
||||
).async_check() is True
|
||||
assert (
|
||||
await _checker(
|
||||
hass,
|
||||
{
|
||||
"condition": f"{DOMAIN}.node_status",
|
||||
"options": {
|
||||
"device_id": device_ids,
|
||||
"behavior": "all",
|
||||
"status": "alive",
|
||||
},
|
||||
},
|
||||
)
|
||||
).async_check() is False
|
||||
},
|
||||
)
|
||||
assert checker.async_check() is True
|
||||
|
||||
|
||||
async def test_value_empty_property_key(
|
||||
|
||||
Reference in New Issue
Block a user