From b4559a172c73143e3aca0847ec29491d25937d61 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 1 Feb 2021 23:47:58 +0100 Subject: [PATCH] Add value notification events to zwave_js integration (#45814) --- homeassistant/components/zwave_js/__init__.py | 52 ++++++- homeassistant/components/zwave_js/const.py | 15 ++ homeassistant/components/zwave_js/entity.py | 16 ++- tests/components/zwave_js/test_events.py | 130 ++++++++++++++++++ 4 files changed, 199 insertions(+), 14 deletions(-) create mode 100644 tests/components/zwave_js/test_events.py diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index c995749f924..2b4b33e9b88 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -1,11 +1,11 @@ """The Z-Wave JS integration.""" import asyncio import logging -from typing import Tuple from async_timeout import timeout 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 ValueNotification from homeassistant.components.hassio.handler import HassioAPIError 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 .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, DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN, EVENT_DEVICE_ADDED_TO_REGISTRY, PLATFORMS, + ZWAVE_JS_EVENT, ) from .discovery import async_discover_values +from .entity import get_device_id LOGGER = logging.getLogger(__name__) CONNECT_TIMEOUT = 10 @@ -37,12 +51,6 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: 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 def register_node_in_dev_reg( hass: HomeAssistant, @@ -106,6 +114,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async_dispatcher_send( 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 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 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: """Handle HA shutdown.""" await client.disconnect() diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index 526a8429bd4..163f4fff9ac 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -17,3 +17,18 @@ DATA_CLIENT = "client" DATA_UNSUBSCRIBE = "unsubs" 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" diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index 84870ba75f4..9626ae9a888 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -1,9 +1,10 @@ """Generic Z-Wave Entity Class.""" 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.model.node import Node as ZwaveNode from zwave_js_server.model.value import Value as ZwaveValue, get_value_id from homeassistant.config_entries import ConfigEntry @@ -19,6 +20,12 @@ LOGGER = logging.getLogger(__name__) 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): """Generic Entity Class for a Z-Wave Device.""" @@ -60,12 +67,7 @@ class ZWaveBaseEntity(Entity): """Return device information for the device registry.""" # device is precreated in main handler return { - "identifiers": { - ( - DOMAIN, - f"{self.client.driver.controller.home_id}-{self.info.node.node_id}", - ) - }, + "identifiers": {get_device_id(self.client, self.info.node)}, } @property diff --git a/tests/components/zwave_js/test_events.py b/tests/components/zwave_js/test_events.py new file mode 100644 index 00000000000..c4280ebb50d --- /dev/null +++ b/tests/components/zwave_js/test_events.py @@ -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"