forked from home-assistant/core
Compare commits
7 Commits
master
...
refactor_z
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1f42b6eaa | ||
|
|
a61e0577b1 | ||
|
|
17b3d16a26 | ||
|
|
c2a011b16f | ||
|
|
56506f259a | ||
|
|
a441e2ddca | ||
|
|
ed43b3a4ec |
@@ -37,7 +37,7 @@ class DeviceAutomationTriggerProtocol(TriggerProtocol, Protocol):
|
||||
) -> dict[str, vol.Schema]:
|
||||
"""List trigger capabilities."""
|
||||
|
||||
async def async_get_triggers(
|
||||
async def async_get_triggers( # type: ignore[override]
|
||||
self, hass: HomeAssistant, device_id: str
|
||||
) -> list[dict[str, Any]]:
|
||||
"""List triggers."""
|
||||
|
||||
@@ -29,7 +29,6 @@ from homeassistant.helpers import (
|
||||
from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from . import trigger
|
||||
from .config_validation import VALUE_SCHEMA
|
||||
from .const import (
|
||||
ATTR_COMMAND_CLASS,
|
||||
@@ -67,6 +66,8 @@ from .triggers.value_updated import (
|
||||
ATTR_FROM,
|
||||
ATTR_TO,
|
||||
PLATFORM_TYPE as VALUE_UPDATED_PLATFORM_TYPE,
|
||||
async_attach_trigger as attach_value_updated_trigger,
|
||||
async_validate_trigger_config as validate_value_updated_trigger_config,
|
||||
)
|
||||
|
||||
# Trigger types
|
||||
@@ -448,10 +449,10 @@ async def async_attach_trigger(
|
||||
ATTR_TO,
|
||||
],
|
||||
)
|
||||
zwave_js_config = await trigger.async_validate_trigger_config(
|
||||
zwave_js_config = await validate_value_updated_trigger_config(
|
||||
hass, zwave_js_config
|
||||
)
|
||||
return await trigger.async_attach_trigger(
|
||||
return await attach_value_updated_trigger(
|
||||
hass, zwave_js_config, action, trigger_info
|
||||
)
|
||||
|
||||
|
||||
@@ -2,45 +2,17 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.const import CONF_PLATFORM
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
|
||||
from homeassistant.helpers.trigger import (
|
||||
TriggerActionType,
|
||||
TriggerInfo,
|
||||
TriggerProtocol,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.trigger import Trigger
|
||||
|
||||
from .triggers import event, value_updated
|
||||
|
||||
TRIGGERS = {
|
||||
"value_updated": value_updated,
|
||||
"event": event,
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
event.PLATFORM_TYPE: event.EventTrigger,
|
||||
value_updated.PLATFORM_TYPE: value_updated.ValueUpdatedTrigger,
|
||||
}
|
||||
|
||||
|
||||
def _get_trigger_platform(config: ConfigType) -> TriggerProtocol:
|
||||
"""Return trigger platform."""
|
||||
platform_split = config[CONF_PLATFORM].split(".", maxsplit=1)
|
||||
if len(platform_split) < 2 or platform_split[1] not in TRIGGERS:
|
||||
raise ValueError(f"Unknown Z-Wave JS trigger platform {config[CONF_PLATFORM]}")
|
||||
return TRIGGERS[platform_split[1]]
|
||||
|
||||
|
||||
async def async_validate_trigger_config(
|
||||
hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
platform = _get_trigger_platform(config)
|
||||
return await platform.async_validate_trigger_config(hass, config)
|
||||
|
||||
|
||||
async def async_attach_trigger(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
action: TriggerActionType,
|
||||
trigger_info: TriggerInfo,
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach trigger of specified platform."""
|
||||
platform = _get_trigger_platform(config)
|
||||
return await platform.async_attach_trigger(hass, config, action, trigger_info)
|
||||
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
|
||||
"""Return the triggers for Z-Wave JS."""
|
||||
return TRIGGERS
|
||||
|
||||
@@ -16,7 +16,7 @@ from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID, CONF_PLATFORM
|
||||
from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
|
||||
from homeassistant.helpers.trigger import Trigger, TriggerActionType, TriggerInfo
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from ..const import (
|
||||
@@ -131,44 +131,72 @@ async def async_validate_trigger_config(
|
||||
return config
|
||||
|
||||
|
||||
async def async_attach_trigger(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
action: TriggerActionType,
|
||||
trigger_info: TriggerInfo,
|
||||
*,
|
||||
platform_type: str = PLATFORM_TYPE,
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Listen for state changes based on configuration."""
|
||||
dev_reg = dr.async_get(hass)
|
||||
if config[ATTR_EVENT_SOURCE] == "node" and not async_get_nodes_from_targets(
|
||||
hass, config, dev_reg=dev_reg
|
||||
):
|
||||
raise ValueError(
|
||||
f"No nodes found for given {ATTR_DEVICE_ID}s or {ATTR_ENTITY_ID}s."
|
||||
)
|
||||
class EventTrigger(Trigger):
|
||||
"""Z-Wave JS event trigger."""
|
||||
|
||||
event_source = config[ATTR_EVENT_SOURCE]
|
||||
event_name = config[ATTR_EVENT]
|
||||
event_data_filter = config.get(ATTR_EVENT_DATA, {})
|
||||
_platform_type = PLATFORM_TYPE
|
||||
|
||||
unsubs: list[Callable] = []
|
||||
job = HassJob(action)
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
action: TriggerActionType,
|
||||
trigger_info: TriggerInfo,
|
||||
) -> None:
|
||||
"""Initialize trigger."""
|
||||
self._config = config
|
||||
self._hass = hass
|
||||
self._event_source = config[ATTR_EVENT_SOURCE]
|
||||
self._event_name = config[ATTR_EVENT]
|
||||
self._event_data_filter = config.get(ATTR_EVENT_DATA, {})
|
||||
|
||||
trigger_data = trigger_info["trigger_data"]
|
||||
self._unsubs: list[Callable] = []
|
||||
self._job = HassJob(action)
|
||||
|
||||
self._trigger_data = trigger_info["trigger_data"]
|
||||
|
||||
@classmethod
|
||||
async def async_validate_trigger_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
return await async_validate_trigger_config(hass, config)
|
||||
|
||||
@classmethod
|
||||
async def async_attach_trigger(
|
||||
cls,
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
action: TriggerActionType,
|
||||
trigger_info: TriggerInfo,
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach a trigger."""
|
||||
dev_reg = dr.async_get(hass)
|
||||
if config[ATTR_EVENT_SOURCE] == "node" and not async_get_nodes_from_targets(
|
||||
hass, config, dev_reg=dev_reg
|
||||
):
|
||||
raise ValueError(
|
||||
f"No nodes found for given {ATTR_DEVICE_ID}s or {ATTR_ENTITY_ID}s."
|
||||
)
|
||||
|
||||
trigger = cls(hass, config, action, trigger_info)
|
||||
trigger._create_zwave_listeners()
|
||||
return trigger._async_remove
|
||||
|
||||
@callback
|
||||
def async_on_event(event_data: dict, device: dr.DeviceEntry | None = None) -> None:
|
||||
def _async_on_event(
|
||||
self, event_data: dict, device: dr.DeviceEntry | None = None
|
||||
) -> None:
|
||||
"""Handle event."""
|
||||
for key, val in event_data_filter.items():
|
||||
for key, val in self._event_data_filter.items():
|
||||
if key not in event_data:
|
||||
return
|
||||
if (
|
||||
config[ATTR_PARTIAL_DICT_MATCH]
|
||||
self._config[ATTR_PARTIAL_DICT_MATCH]
|
||||
and isinstance(event_data[key], dict)
|
||||
and isinstance(event_data_filter[key], dict)
|
||||
and isinstance(self._event_data_filter[key], dict)
|
||||
):
|
||||
for key2, val2 in event_data_filter[key].items():
|
||||
for key2, val2 in self._event_data_filter[key].items():
|
||||
if key2 not in event_data[key] or event_data[key][key2] != val2:
|
||||
return
|
||||
continue
|
||||
@@ -176,14 +204,16 @@ async def async_attach_trigger(
|
||||
return
|
||||
|
||||
payload = {
|
||||
**trigger_data,
|
||||
CONF_PLATFORM: platform_type,
|
||||
ATTR_EVENT_SOURCE: event_source,
|
||||
ATTR_EVENT: event_name,
|
||||
**self._trigger_data,
|
||||
CONF_PLATFORM: self._platform_type,
|
||||
ATTR_EVENT_SOURCE: self._event_source,
|
||||
ATTR_EVENT: self._event_name,
|
||||
ATTR_EVENT_DATA: event_data,
|
||||
}
|
||||
|
||||
primary_desc = f"Z-Wave JS '{event_source}' event '{event_name}' was emitted"
|
||||
primary_desc = (
|
||||
f"Z-Wave JS '{self._event_source}' event '{self._event_name}' was emitted"
|
||||
)
|
||||
|
||||
if device:
|
||||
device_name = device.name_by_user or device.name
|
||||
@@ -199,34 +229,41 @@ async def async_attach_trigger(
|
||||
f"{payload['description']} with event data: {event_data}"
|
||||
)
|
||||
|
||||
hass.async_run_hass_job(job, {"trigger": payload})
|
||||
self._hass.async_run_hass_job(self._job, {"trigger": payload})
|
||||
|
||||
@callback
|
||||
def async_remove() -> None:
|
||||
def _async_remove(self) -> None:
|
||||
"""Remove state listeners async."""
|
||||
for unsub in unsubs:
|
||||
for unsub in self._unsubs:
|
||||
unsub()
|
||||
unsubs.clear()
|
||||
self._unsubs.clear()
|
||||
|
||||
@callback
|
||||
def _create_zwave_listeners() -> None:
|
||||
def _create_zwave_listeners(self) -> None:
|
||||
"""Create Z-Wave JS listeners."""
|
||||
async_remove()
|
||||
self._async_remove()
|
||||
# Nodes list can come from different drivers and we will need to listen to
|
||||
# server connections for all of them.
|
||||
drivers: set[Driver] = set()
|
||||
if not (nodes := async_get_nodes_from_targets(hass, config, dev_reg=dev_reg)):
|
||||
entry_id = config[ATTR_CONFIG_ENTRY_ID]
|
||||
entry = hass.config_entries.async_get_entry(entry_id)
|
||||
dev_reg = dr.async_get(self._hass)
|
||||
if not (
|
||||
nodes := async_get_nodes_from_targets(
|
||||
self._hass, self._config, dev_reg=dev_reg
|
||||
)
|
||||
):
|
||||
entry_id = self._config[ATTR_CONFIG_ENTRY_ID]
|
||||
entry = self._hass.config_entries.async_get_entry(entry_id)
|
||||
assert entry
|
||||
client: Client = entry.runtime_data[DATA_CLIENT]
|
||||
driver = client.driver
|
||||
assert driver
|
||||
drivers.add(driver)
|
||||
if event_source == "controller":
|
||||
unsubs.append(driver.controller.on(event_name, async_on_event))
|
||||
if self._event_source == "controller":
|
||||
self._unsubs.append(
|
||||
driver.controller.on(self._event_name, self._async_on_event)
|
||||
)
|
||||
else:
|
||||
unsubs.append(driver.on(event_name, async_on_event))
|
||||
self._unsubs.append(driver.on(self._event_name, self._async_on_event))
|
||||
|
||||
for node in nodes:
|
||||
driver = node.client.driver
|
||||
@@ -236,18 +273,17 @@ async def async_attach_trigger(
|
||||
device = dev_reg.async_get_device(identifiers={device_identifier})
|
||||
assert device
|
||||
# We need to store the device for the callback
|
||||
unsubs.append(
|
||||
node.on(event_name, functools.partial(async_on_event, device=device))
|
||||
self._unsubs.append(
|
||||
node.on(
|
||||
self._event_name,
|
||||
functools.partial(self._async_on_event, device=device),
|
||||
)
|
||||
)
|
||||
unsubs.extend(
|
||||
self._unsubs.extend(
|
||||
async_dispatcher_connect(
|
||||
hass,
|
||||
self._hass,
|
||||
f"{DOMAIN}_{driver.controller.home_id}_connected_to_server",
|
||||
_create_zwave_listeners,
|
||||
self._create_zwave_listeners,
|
||||
)
|
||||
for driver in drivers
|
||||
)
|
||||
|
||||
_create_zwave_listeners()
|
||||
|
||||
return async_remove
|
||||
|
||||
@@ -14,7 +14,7 @@ from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID, CONF_PLATFORM, M
|
||||
from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
|
||||
from homeassistant.helpers.trigger import Trigger, TriggerActionType, TriggerInfo
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from ..config_validation import VALUE_SCHEMA
|
||||
@@ -202,3 +202,25 @@ async def async_attach_trigger(
|
||||
_create_zwave_listeners()
|
||||
|
||||
return async_remove
|
||||
|
||||
|
||||
class ValueUpdatedTrigger(Trigger):
|
||||
"""Z-Wave JS value updated trigger."""
|
||||
|
||||
@classmethod
|
||||
async def async_validate_trigger_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
return await async_validate_trigger_config(hass, config)
|
||||
|
||||
@classmethod
|
||||
async def async_attach_trigger(
|
||||
cls,
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
action: TriggerActionType,
|
||||
trigger_info: TriggerInfo,
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach a trigger."""
|
||||
return await async_attach_trigger(hass, config, action, trigger_info)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
import asyncio
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable, Coroutine
|
||||
@@ -49,12 +50,37 @@ DATA_PLUGGABLE_ACTIONS: HassKey[defaultdict[tuple, PluggableActionsEntry]] = Has
|
||||
)
|
||||
|
||||
|
||||
class Trigger(abc.ABC):
|
||||
"""Trigger class."""
|
||||
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
async def async_validate_trigger_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
async def async_attach_trigger(
|
||||
cls,
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
action: TriggerActionType,
|
||||
trigger_info: TriggerInfo,
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach a trigger."""
|
||||
|
||||
|
||||
class TriggerProtocol(Protocol):
|
||||
"""Define the format of trigger modules.
|
||||
|
||||
Each module must define either TRIGGER_SCHEMA or async_validate_trigger_config.
|
||||
New implementations should only implement async_get_triggers.
|
||||
"""
|
||||
|
||||
async def async_get_triggers(self, hass: HomeAssistant) -> dict[str, type[Trigger]]:
|
||||
"""Return the triggers provided by this integration."""
|
||||
|
||||
TRIGGER_SCHEMA: vol.Schema
|
||||
|
||||
async def async_validate_trigger_config(
|
||||
@@ -219,13 +245,14 @@ class PluggableAction:
|
||||
async def _async_get_trigger_platform(
|
||||
hass: HomeAssistant, config: ConfigType
|
||||
) -> TriggerProtocol:
|
||||
platform_and_sub_type = config[CONF_PLATFORM].split(".")
|
||||
trigger_key: str = config[CONF_PLATFORM]
|
||||
platform_and_sub_type = trigger_key.split(".")
|
||||
platform = platform_and_sub_type[0]
|
||||
platform = _PLATFORM_ALIASES.get(platform, platform)
|
||||
try:
|
||||
integration = await async_get_integration(hass, platform)
|
||||
except IntegrationNotFound:
|
||||
raise vol.Invalid(f"Invalid trigger '{platform}' specified") from None
|
||||
raise vol.Invalid(f"Invalid trigger '{trigger_key}' specified") from None
|
||||
try:
|
||||
return await integration.async_get_platform("trigger")
|
||||
except ImportError:
|
||||
@@ -241,7 +268,13 @@ async def async_validate_trigger_config(
|
||||
config = []
|
||||
for conf in trigger_config:
|
||||
platform = await _async_get_trigger_platform(hass, conf)
|
||||
if hasattr(platform, "async_validate_trigger_config"):
|
||||
if hasattr(platform, "async_get_triggers"):
|
||||
trigger_descriptors = await platform.async_get_triggers(hass)
|
||||
trigger_key: str = conf[CONF_PLATFORM]
|
||||
if not (trigger := trigger_descriptors[trigger_key]):
|
||||
raise vol.Invalid(f"Invalid trigger '{trigger_key}' specified")
|
||||
conf = await trigger.async_validate_trigger_config(hass, conf)
|
||||
elif hasattr(platform, "async_validate_trigger_config"):
|
||||
conf = await platform.async_validate_trigger_config(hass, conf)
|
||||
else:
|
||||
conf = platform.TRIGGER_SCHEMA(conf)
|
||||
@@ -337,11 +370,15 @@ async def async_initialize_triggers(
|
||||
trigger_data=trigger_data,
|
||||
)
|
||||
|
||||
if hasattr(platform, "async_get_triggers"):
|
||||
trigger_descriptors = await platform.async_get_triggers(hass)
|
||||
attach_fn = trigger_descriptors[conf[CONF_PLATFORM]].async_attach_trigger
|
||||
else:
|
||||
attach_fn = platform.async_attach_trigger
|
||||
|
||||
triggers.append(
|
||||
create_eager_task(
|
||||
platform.async_attach_trigger(
|
||||
hass, conf, _trigger_action_wrapper(hass, action, conf), info
|
||||
)
|
||||
attach_fn(hass, conf, _trigger_action_wrapper(hass, action, conf), info)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""The tests for Z-Wave JS automation triggers."""
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
import voluptuous as vol
|
||||
@@ -11,14 +11,11 @@ from zwave_js_server.model.node import Node
|
||||
from homeassistant.components import automation
|
||||
from homeassistant.components.zwave_js import DOMAIN
|
||||
from homeassistant.components.zwave_js.helpers import get_device_id
|
||||
from homeassistant.components.zwave_js.trigger import (
|
||||
_get_trigger_platform,
|
||||
async_validate_trigger_config,
|
||||
)
|
||||
from homeassistant.components.zwave_js.trigger import TRIGGERS
|
||||
from homeassistant.components.zwave_js.triggers.trigger_helpers import (
|
||||
async_bypass_dynamic_config_validation,
|
||||
)
|
||||
from homeassistant.const import CONF_PLATFORM, SERVICE_RELOAD
|
||||
from homeassistant.const import SERVICE_RELOAD
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.setup import async_setup_component
|
||||
@@ -977,22 +974,10 @@ async def test_zwave_js_event_invalid_config_entry_id(
|
||||
caplog.clear()
|
||||
|
||||
|
||||
async def test_async_validate_trigger_config(hass: HomeAssistant) -> None:
|
||||
"""Test async_validate_trigger_config."""
|
||||
mock_platform = AsyncMock()
|
||||
with patch(
|
||||
"homeassistant.components.zwave_js.trigger._get_trigger_platform",
|
||||
return_value=mock_platform,
|
||||
):
|
||||
mock_platform.async_validate_trigger_config.return_value = {}
|
||||
await async_validate_trigger_config(hass, {})
|
||||
mock_platform.async_validate_trigger_config.assert_awaited()
|
||||
|
||||
|
||||
async def test_invalid_trigger_configs(hass: HomeAssistant) -> None:
|
||||
"""Test invalid trigger configs."""
|
||||
with pytest.raises(vol.Invalid):
|
||||
await async_validate_trigger_config(
|
||||
await TRIGGERS[f"{DOMAIN}.event"].async_validate_trigger_config(
|
||||
hass,
|
||||
{
|
||||
"platform": f"{DOMAIN}.event",
|
||||
@@ -1003,7 +988,7 @@ async def test_invalid_trigger_configs(hass: HomeAssistant) -> None:
|
||||
)
|
||||
|
||||
with pytest.raises(vol.Invalid):
|
||||
await async_validate_trigger_config(
|
||||
await TRIGGERS[f"{DOMAIN}.value_updated"].async_validate_trigger_config(
|
||||
hass,
|
||||
{
|
||||
"platform": f"{DOMAIN}.value_updated",
|
||||
@@ -1041,7 +1026,7 @@ async def test_zwave_js_trigger_config_entry_unloaded(
|
||||
await hass.config_entries.async_unload(integration.entry_id)
|
||||
|
||||
# Test full validation for both events
|
||||
assert await async_validate_trigger_config(
|
||||
assert await TRIGGERS[f"{DOMAIN}.value_updated"].async_validate_trigger_config(
|
||||
hass,
|
||||
{
|
||||
"platform": f"{DOMAIN}.value_updated",
|
||||
@@ -1051,7 +1036,7 @@ async def test_zwave_js_trigger_config_entry_unloaded(
|
||||
},
|
||||
)
|
||||
|
||||
assert await async_validate_trigger_config(
|
||||
assert await TRIGGERS[f"{DOMAIN}.event"].async_validate_trigger_config(
|
||||
hass,
|
||||
{
|
||||
"platform": f"{DOMAIN}.event",
|
||||
@@ -1115,12 +1100,6 @@ async def test_zwave_js_trigger_config_entry_unloaded(
|
||||
)
|
||||
|
||||
|
||||
def test_get_trigger_platform_failure() -> None:
|
||||
"""Test _get_trigger_platform."""
|
||||
with pytest.raises(ValueError):
|
||||
_get_trigger_platform({CONF_PLATFORM: "zwave_js.invalid"})
|
||||
|
||||
|
||||
async def test_server_reconnect_event(
|
||||
hass: HomeAssistant,
|
||||
client,
|
||||
|
||||
Reference in New Issue
Block a user