Add value notification events to zwave_js integration (#45814)
This commit is contained in:
parent
8222eb5e3e
commit
b4559a172c
4 changed files with 199 additions and 14 deletions
|
@ -1,11 +1,11 @@
|
||||||
"""The Z-Wave JS integration."""
|
"""The Z-Wave JS integration."""
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from typing import Tuple
|
|
||||||
|
|
||||||
from async_timeout import timeout
|
from async_timeout import timeout
|
||||||
from zwave_js_server.client import Client as ZwaveClient
|
from zwave_js_server.client import Client as ZwaveClient
|
||||||
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 ValueNotification
|
||||||
|
|
||||||
from homeassistant.components.hassio.handler import HassioAPIError
|
from homeassistant.components.hassio.handler import HassioAPIError
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
@ -18,14 +18,28 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||||
|
|
||||||
from .api import async_register_api
|
from .api import async_register_api
|
||||||
from .const import (
|
from .const import (
|
||||||
|
ATTR_COMMAND_CLASS,
|
||||||
|
ATTR_COMMAND_CLASS_NAME,
|
||||||
|
ATTR_DEVICE_ID,
|
||||||
|
ATTR_DOMAIN,
|
||||||
|
ATTR_ENDPOINT,
|
||||||
|
ATTR_HOME_ID,
|
||||||
|
ATTR_LABEL,
|
||||||
|
ATTR_NODE_ID,
|
||||||
|
ATTR_PROPERTY_KEY_NAME,
|
||||||
|
ATTR_PROPERTY_NAME,
|
||||||
|
ATTR_TYPE,
|
||||||
|
ATTR_VALUE,
|
||||||
CONF_INTEGRATION_CREATED_ADDON,
|
CONF_INTEGRATION_CREATED_ADDON,
|
||||||
DATA_CLIENT,
|
DATA_CLIENT,
|
||||||
DATA_UNSUBSCRIBE,
|
DATA_UNSUBSCRIBE,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
EVENT_DEVICE_ADDED_TO_REGISTRY,
|
EVENT_DEVICE_ADDED_TO_REGISTRY,
|
||||||
PLATFORMS,
|
PLATFORMS,
|
||||||
|
ZWAVE_JS_EVENT,
|
||||||
)
|
)
|
||||||
from .discovery import async_discover_values
|
from .discovery import async_discover_values
|
||||||
|
from .entity import get_device_id
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
CONNECT_TIMEOUT = 10
|
CONNECT_TIMEOUT = 10
|
||||||
|
@ -37,12 +51,6 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def get_device_id(client: ZwaveClient, node: ZwaveNode) -> Tuple[str, str]:
|
|
||||||
"""Get device registry identifier for Z-Wave node."""
|
|
||||||
return (DOMAIN, f"{client.driver.controller.home_id}-{node.node_id}")
|
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def register_node_in_dev_reg(
|
def register_node_in_dev_reg(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
@ -106,6 +114,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
async_dispatcher_send(
|
async_dispatcher_send(
|
||||||
hass, f"{DOMAIN}_{entry.entry_id}_add_{disc_info.platform}", disc_info
|
hass, f"{DOMAIN}_{entry.entry_id}_add_{disc_info.platform}", disc_info
|
||||||
)
|
)
|
||||||
|
# add listener for stateless node events (value notification)
|
||||||
|
node.on(
|
||||||
|
"value notification",
|
||||||
|
lambda event: async_on_value_notification(event["value_notification"]),
|
||||||
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_on_node_added(node: ZwaveNode) -> None:
|
def async_on_node_added(node: ZwaveNode) -> None:
|
||||||
|
@ -134,6 +147,31 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
# note: removal of entity registry is handled by core
|
# note: removal of entity registry is handled by core
|
||||||
dev_reg.async_remove_device(device.id)
|
dev_reg.async_remove_device(device.id)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_on_value_notification(notification: ValueNotification) -> None:
|
||||||
|
"""Relay stateless value notification events from Z-Wave nodes to hass."""
|
||||||
|
device = dev_reg.async_get_device({get_device_id(client, notification.node)})
|
||||||
|
value = notification.value
|
||||||
|
if notification.metadata.states:
|
||||||
|
value = notification.metadata.states.get(str(value), value)
|
||||||
|
hass.bus.async_fire(
|
||||||
|
ZWAVE_JS_EVENT,
|
||||||
|
{
|
||||||
|
ATTR_TYPE: "value_notification",
|
||||||
|
ATTR_DOMAIN: DOMAIN,
|
||||||
|
ATTR_NODE_ID: notification.node.node_id,
|
||||||
|
ATTR_HOME_ID: client.driver.controller.home_id,
|
||||||
|
ATTR_ENDPOINT: notification.endpoint,
|
||||||
|
ATTR_DEVICE_ID: device.id,
|
||||||
|
ATTR_COMMAND_CLASS: notification.command_class,
|
||||||
|
ATTR_COMMAND_CLASS_NAME: notification.command_class_name,
|
||||||
|
ATTR_LABEL: notification.metadata.label,
|
||||||
|
ATTR_PROPERTY_NAME: notification.property_name,
|
||||||
|
ATTR_PROPERTY_KEY_NAME: notification.property_key_name,
|
||||||
|
ATTR_VALUE: value,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
async def handle_ha_shutdown(event: Event) -> None:
|
async def handle_ha_shutdown(event: Event) -> None:
|
||||||
"""Handle HA shutdown."""
|
"""Handle HA shutdown."""
|
||||||
await client.disconnect()
|
await client.disconnect()
|
||||||
|
|
|
@ -17,3 +17,18 @@ DATA_CLIENT = "client"
|
||||||
DATA_UNSUBSCRIBE = "unsubs"
|
DATA_UNSUBSCRIBE = "unsubs"
|
||||||
|
|
||||||
EVENT_DEVICE_ADDED_TO_REGISTRY = f"{DOMAIN}_device_added_to_registry"
|
EVENT_DEVICE_ADDED_TO_REGISTRY = f"{DOMAIN}_device_added_to_registry"
|
||||||
|
|
||||||
|
# constants for events
|
||||||
|
ZWAVE_JS_EVENT = f"{DOMAIN}_event"
|
||||||
|
ATTR_NODE_ID = "node_id"
|
||||||
|
ATTR_HOME_ID = "home_id"
|
||||||
|
ATTR_ENDPOINT = "endpoint"
|
||||||
|
ATTR_LABEL = "label"
|
||||||
|
ATTR_VALUE = "value"
|
||||||
|
ATTR_COMMAND_CLASS = "command_class"
|
||||||
|
ATTR_COMMAND_CLASS_NAME = "command_class_name"
|
||||||
|
ATTR_TYPE = "type"
|
||||||
|
ATTR_DOMAIN = "domain"
|
||||||
|
ATTR_DEVICE_ID = "device_id"
|
||||||
|
ATTR_PROPERTY_NAME = "property_name"
|
||||||
|
ATTR_PROPERTY_KEY_NAME = "property_key_name"
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
"""Generic Z-Wave Entity Class."""
|
"""Generic Z-Wave Entity Class."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Optional, Union
|
from typing import Optional, Tuple, Union
|
||||||
|
|
||||||
from zwave_js_server.client import Client as ZwaveClient
|
from zwave_js_server.client import Client as ZwaveClient
|
||||||
|
from zwave_js_server.model.node import Node as ZwaveNode
|
||||||
from zwave_js_server.model.value import Value as ZwaveValue, get_value_id
|
from zwave_js_server.model.value import Value as ZwaveValue, get_value_id
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
@ -19,6 +20,12 @@ LOGGER = logging.getLogger(__name__)
|
||||||
EVENT_VALUE_UPDATED = "value updated"
|
EVENT_VALUE_UPDATED = "value updated"
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def get_device_id(client: ZwaveClient, node: ZwaveNode) -> Tuple[str, str]:
|
||||||
|
"""Get device registry identifier for Z-Wave node."""
|
||||||
|
return (DOMAIN, f"{client.driver.controller.home_id}-{node.node_id}")
|
||||||
|
|
||||||
|
|
||||||
class ZWaveBaseEntity(Entity):
|
class ZWaveBaseEntity(Entity):
|
||||||
"""Generic Entity Class for a Z-Wave Device."""
|
"""Generic Entity Class for a Z-Wave Device."""
|
||||||
|
|
||||||
|
@ -60,12 +67,7 @@ class ZWaveBaseEntity(Entity):
|
||||||
"""Return device information for the device registry."""
|
"""Return device information for the device registry."""
|
||||||
# device is precreated in main handler
|
# device is precreated in main handler
|
||||||
return {
|
return {
|
||||||
"identifiers": {
|
"identifiers": {get_device_id(self.client, self.info.node)},
|
||||||
(
|
|
||||||
DOMAIN,
|
|
||||||
f"{self.client.driver.controller.home_id}-{self.info.node.node_id}",
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
130
tests/components/zwave_js/test_events.py
Normal file
130
tests/components/zwave_js/test_events.py
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
"""Test Z-Wave JS (value notification) events."""
|
||||||
|
from zwave_js_server.event import Event
|
||||||
|
|
||||||
|
from tests.common import async_capture_events
|
||||||
|
|
||||||
|
|
||||||
|
async def test_scenes(hass, hank_binary_switch, integration, client):
|
||||||
|
"""Test scene events."""
|
||||||
|
# just pick a random node to fake the value notification events
|
||||||
|
node = hank_binary_switch
|
||||||
|
events = async_capture_events(hass, "zwave_js_event")
|
||||||
|
|
||||||
|
# Publish fake Basic Set value notification
|
||||||
|
event = Event(
|
||||||
|
type="value notification",
|
||||||
|
data={
|
||||||
|
"source": "node",
|
||||||
|
"event": "value notification",
|
||||||
|
"nodeId": 32,
|
||||||
|
"args": {
|
||||||
|
"commandClassName": "Basic",
|
||||||
|
"commandClass": 32,
|
||||||
|
"endpoint": 0,
|
||||||
|
"property": "event",
|
||||||
|
"propertyName": "event",
|
||||||
|
"value": 255,
|
||||||
|
"metadata": {
|
||||||
|
"type": "number",
|
||||||
|
"readable": True,
|
||||||
|
"writeable": False,
|
||||||
|
"min": 0,
|
||||||
|
"max": 255,
|
||||||
|
"label": "Event value",
|
||||||
|
},
|
||||||
|
"ccVersion": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
node.receive_event(event)
|
||||||
|
# wait for the event
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(events) == 1
|
||||||
|
assert events[0].data["home_id"] == client.driver.controller.home_id
|
||||||
|
assert events[0].data["node_id"] == 32
|
||||||
|
assert events[0].data["endpoint"] == 0
|
||||||
|
assert events[0].data["command_class"] == 32
|
||||||
|
assert events[0].data["command_class_name"] == "Basic"
|
||||||
|
assert events[0].data["label"] == "Event value"
|
||||||
|
assert events[0].data["value"] == 255
|
||||||
|
|
||||||
|
# Publish fake Scene Activation value notification
|
||||||
|
event = Event(
|
||||||
|
type="value notification",
|
||||||
|
data={
|
||||||
|
"source": "node",
|
||||||
|
"event": "value notification",
|
||||||
|
"nodeId": 32,
|
||||||
|
"args": {
|
||||||
|
"commandClassName": "Scene Activation",
|
||||||
|
"commandClass": 43,
|
||||||
|
"endpoint": 0,
|
||||||
|
"property": "SceneID",
|
||||||
|
"propertyName": "SceneID",
|
||||||
|
"value": 16,
|
||||||
|
"metadata": {
|
||||||
|
"type": "number",
|
||||||
|
"readable": True,
|
||||||
|
"writeable": False,
|
||||||
|
"min": 0,
|
||||||
|
"max": 255,
|
||||||
|
"label": "Scene ID",
|
||||||
|
},
|
||||||
|
"ccVersion": 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
node.receive_event(event)
|
||||||
|
# wait for the event
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(events) == 2
|
||||||
|
assert events[1].data["command_class"] == 43
|
||||||
|
assert events[1].data["command_class_name"] == "Scene Activation"
|
||||||
|
assert events[1].data["label"] == "Scene ID"
|
||||||
|
assert events[1].data["value"] == 16
|
||||||
|
|
||||||
|
# Publish fake Central Scene value notification
|
||||||
|
event = Event(
|
||||||
|
type="value notification",
|
||||||
|
data={
|
||||||
|
"source": "node",
|
||||||
|
"event": "value notification",
|
||||||
|
"nodeId": 32,
|
||||||
|
"args": {
|
||||||
|
"commandClassName": "Central Scene",
|
||||||
|
"commandClass": 91,
|
||||||
|
"endpoint": 0,
|
||||||
|
"property": "scene",
|
||||||
|
"propertyKey": "001",
|
||||||
|
"propertyName": "scene",
|
||||||
|
"propertyKeyName": "001",
|
||||||
|
"value": 4,
|
||||||
|
"metadata": {
|
||||||
|
"type": "number",
|
||||||
|
"readable": True,
|
||||||
|
"writeable": False,
|
||||||
|
"min": 0,
|
||||||
|
"max": 255,
|
||||||
|
"label": "Scene 001",
|
||||||
|
"states": {
|
||||||
|
"0": "KeyPressed",
|
||||||
|
"1": "KeyReleased",
|
||||||
|
"2": "KeyHeldDown",
|
||||||
|
"3": "KeyPressed2x",
|
||||||
|
"4": "KeyPressed3x",
|
||||||
|
"5": "KeyPressed4x",
|
||||||
|
"6": "KeyPressed5x",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ccVersion": 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
node.receive_event(event)
|
||||||
|
# wait for the event
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(events) == 3
|
||||||
|
assert events[2].data["command_class"] == 91
|
||||||
|
assert events[2].data["command_class_name"] == "Central Scene"
|
||||||
|
assert events[2].data["label"] == "Scene 001"
|
||||||
|
assert events[2].data["value"] == "KeyPressed3x"
|
Loading…
Add table
Add a link
Reference in a new issue