From 1cfd292075fbb3a4cf413ed05c14d6821db35ff3 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 6 Dec 2022 18:41:09 +0100 Subject: [PATCH] Bypass zwave_js config validation if driver not ready (#83410) --- .../zwave_js/device_automation_helpers.py | 10 ++- .../zwave_js/triggers/trigger_helpers.py | 21 ++++-- .../zwave_js/test_device_trigger.py | 68 +++++++++++++++++++ tests/components/zwave_js/test_trigger.py | 59 ++++++++++++++++ 4 files changed, 149 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/zwave_js/device_automation_helpers.py b/homeassistant/components/zwave_js/device_automation_helpers.py index 25cce978df1..11c4fde3137 100644 --- a/homeassistant/components/zwave_js/device_automation_helpers.py +++ b/homeassistant/components/zwave_js/device_automation_helpers.py @@ -4,6 +4,7 @@ from __future__ import annotations from typing import 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.model.node import Node from zwave_js_server.model.value import ConfigurationValue @@ -12,7 +13,7 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr -from .const import DOMAIN +from .const import DATA_CLIENT, DOMAIN NODE_STATUSES = ["asleep", "awake", "dead", "alive"] @@ -66,4 +67,9 @@ def async_bypass_dynamic_config_validation(hass: HomeAssistant, device_id: str) ), None, ) - return not entry + if not entry: + return True + + # The driver may not be ready when the config entry is loaded. + client: ZwaveClient = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] + return client.driver is None diff --git a/homeassistant/components/zwave_js/triggers/trigger_helpers.py b/homeassistant/components/zwave_js/triggers/trigger_helpers.py index 706c4fc0aca..0fd9c3b4291 100644 --- a/homeassistant/components/zwave_js/triggers/trigger_helpers.py +++ b/homeassistant/components/zwave_js/triggers/trigger_helpers.py @@ -1,11 +1,13 @@ """Helpers for Z-Wave JS custom triggers.""" +from zwave_js_server.client import Client as ZwaveClient + from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.typing import ConfigType -from ..const import ATTR_CONFIG_ENTRY_ID, DOMAIN +from ..const import ATTR_CONFIG_ENTRY_ID, DATA_CLIENT, DOMAIN @callback @@ -19,9 +21,8 @@ def async_bypass_dynamic_config_validation( ent_reg = er.async_get(hass) trigger_devices = config.get(ATTR_DEVICE_ID, []) trigger_entities = config.get(ATTR_ENTITY_ID, []) - return any( - entry.state != ConfigEntryState.LOADED - and ( + for entry in hass.config_entries.async_entries(DOMAIN): + if entry.state != ConfigEntryState.LOADED and ( entry.entry_id == config.get(ATTR_CONFIG_ENTRY_ID) or any( device.id in trigger_devices @@ -31,6 +32,12 @@ def async_bypass_dynamic_config_validation( entity.entity_id in trigger_entities for entity in er.async_entries_for_config_entry(ent_reg, entry.entry_id) ) - ) - for entry in hass.config_entries.async_entries(DOMAIN) - ) + ): + return True + + # The driver may not be ready when the config entry is loaded. + client: ZwaveClient = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] + if client.driver is None: + return True + + return False diff --git a/tests/components/zwave_js/test_device_trigger.py b/tests/components/zwave_js/test_device_trigger.py index 859164aa4c3..84d87efed63 100644 --- a/tests/components/zwave_js/test_device_trigger.py +++ b/tests/components/zwave_js/test_device_trigger.py @@ -1080,6 +1080,74 @@ async def test_if_value_updated_value_fires( ) +async def test_value_updated_value_no_driver( + hass, client, lock_schlage_be469, integration, calls +): + """Test zwave_js.value_updated.value trigger with missing driver.""" + node: Node = lock_schlage_be469 + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + driver = client.driver + client.driver = None + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": device.id, + "type": "zwave_js.value_updated.value", + "command_class": CommandClass.DOOR_LOCK.value, + "property": "latchStatus", + "property_key": None, + "endpoint": None, + "from": "open", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "zwave_js.value_updated.value - " + "{{ trigger.platform}} - " + "{{ trigger.previous_value }}" + ) + }, + }, + }, + ] + }, + ) + await hass.async_block_till_done() + + client.driver = driver + + # No trigger as automation failed to setup. + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": node.node_id, + "args": { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "latchStatus", + "newValue": "closed", + "prevValue": "open", + "propertyName": "latchStatus", + }, + }, + ) + node.receive_event(event) + await hass.async_block_till_done() + assert len(calls) == 0 + + async def test_get_trigger_capabilities_value_updated_value( hass, client, lock_schlage_be469, integration ): diff --git a/tests/components/zwave_js/test_trigger.py b/tests/components/zwave_js/test_trigger.py index a5c226057d4..b3b6910e5f5 100644 --- a/tests/components/zwave_js/test_trigger.py +++ b/tests/components/zwave_js/test_trigger.py @@ -385,6 +385,65 @@ async def test_zwave_js_value_updated_bypass_dynamic_validation_no_nodes( assert len(no_value_filter) == 0 +async def test_zwave_js_value_updated_bypass_dynamic_validation_no_driver( + hass, client, lock_schlage_be469, integration +): + """Test zwave_js.value_updated trigger without driver.""" + trigger_type = f"{DOMAIN}.value_updated" + node: Node = lock_schlage_be469 + driver = client.driver + client.driver = None + + no_value_filter = async_capture_events(hass, "no_value_filter") + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + # no value filter + { + "trigger": { + "platform": trigger_type, + "entity_id": SCHLAGE_BE469_LOCK_ENTITY, + "command_class": CommandClass.DOOR_LOCK.value, + "property": "latchStatus", + }, + "action": { + "event": "no_value_filter", + }, + }, + ] + }, + ) + await hass.async_block_till_done() + + client.driver = driver + + # Test that no value filter is NOT triggered because automation failed setup + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": node.node_id, + "args": { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "latchStatus", + "newValue": "boo", + "prevValue": "hiss", + "propertyName": "latchStatus", + }, + }, + ) + node.receive_event(event) + await hass.async_block_till_done() + + assert len(no_value_filter) == 0 + + async def test_zwave_js_event(hass, client, lock_schlage_be469, integration): """Test for zwave_js.event automation trigger.""" trigger_type = f"{DOMAIN}.event"