Add support for zwave_js event entities (#102285)

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Raman Gupta 2023-10-20 19:11:08 -04:00 committed by GitHub
parent a2c60d9015
commit 41b59b6990
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 733 additions and 1 deletions

View File

@ -162,6 +162,8 @@ class ZWaveValueDiscoverySchema(DataclassMustHaveAtLeastOne):
any_available_states: set[tuple[int, str]] | None = None
# [optional] the value's value must match this value
value: Any | None = None
# [optional] the value's metadata_stateful must match this value
stateful: bool | None = None
@dataclass
@ -1045,6 +1047,15 @@ DISCOVERY_SCHEMAS = [
any_available_states={(0, "idle")},
),
),
# event
# stateful = False
ZWaveDiscoverySchema(
platform=Platform.EVENT,
hint="stateless",
primary_value=ZWaveValueDiscoverySchema(
stateful=False,
),
),
]
@ -1294,6 +1305,9 @@ def check_value(value: ZwaveValue, schema: ZWaveValueDiscoverySchema) -> bool:
# check value
if schema.value is not None and value.value not in schema.value:
return False
# check metadata_stateful
if schema.stateful is not None and value.metadata.stateful != schema.stateful:
return False
return True

View File

@ -0,0 +1,98 @@
"""Support for Z-Wave controls using the event platform."""
from __future__ import annotations
from zwave_js_server.client import Client as ZwaveClient
from zwave_js_server.model.driver import Driver
from zwave_js_server.model.value import Value, ValueNotification
from homeassistant.components.event import DOMAIN as EVENT_DOMAIN, EventEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import ATTR_VALUE, DATA_CLIENT, DOMAIN
from .discovery import ZwaveDiscoveryInfo
from .entity import ZWaveBaseEntity
PARALLEL_UPDATES = 0
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Z-Wave Event entity from Config Entry."""
client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT]
@callback
def async_add_event(info: ZwaveDiscoveryInfo) -> None:
"""Add Z-Wave event entity."""
driver = client.driver
assert driver is not None # Driver is ready before platforms are loaded.
entities: list[ZWaveBaseEntity] = [ZwaveEventEntity(config_entry, driver, info)]
async_add_entities(entities)
config_entry.async_on_unload(
async_dispatcher_connect(
hass,
f"{DOMAIN}_{config_entry.entry_id}_add_{EVENT_DOMAIN}",
async_add_event,
)
)
def _cc_and_label(value: Value) -> str:
"""Return a string with the command class and label."""
label = value.metadata.label
if label:
label = label.lower()
return f"{value.command_class_name.capitalize()} {label}".strip()
class ZwaveEventEntity(ZWaveBaseEntity, EventEntity):
"""Representation of a Z-Wave event entity."""
def __init__(
self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo
) -> None:
"""Initialize a ZwaveEventEntity entity."""
super().__init__(config_entry, driver, info)
value = self.value = info.primary_value
self.states: dict[int, str] = {}
if states := value.metadata.states:
self._attr_event_types = sorted(states.values())
self.states = {int(k): v for k, v in states.items()}
else:
self._attr_event_types = [_cc_and_label(value)]
# Entity class attributes
self._attr_name = self.generate_name(include_value_name=True)
@callback
def _async_handle_event(self, value_notification: ValueNotification) -> None:
"""Handle a value notification event."""
# If the notification doesn't match the value we are tracking, we can return
value = self.value
if (
value_notification.command_class != value.command_class
or value_notification.endpoint != value.endpoint
or value_notification.property_ != value.property_
or value_notification.property_key != value.property_key
or (notification_value := value_notification.value) is None
):
return
event_name = self.states.get(notification_value, _cc_and_label(value))
self._trigger_event(event_name, {ATTR_VALUE: notification_value})
self.async_write_ha_state()
async def async_added_to_hass(self) -> None:
"""Call when entity is added."""
await super().async_added_to_hass()
self.async_on_remove(
self.info.node.on(
"value notification",
lambda event: self._async_handle_event(event["value_notification"]),
)
)

View File

@ -668,6 +668,12 @@ def climate_intermatic_pe653_state_fixture():
return json.loads(load_fixture("zwave_js/climate_intermatic_pe653_state.json"))
@pytest.fixture(name="central_scene_node_state", scope="session")
def central_scene_node_state_fixture():
"""Load node with Central Scene CC node state fixture data."""
return json.loads(load_fixture("zwave_js/central_scene_node_state.json"))
# model fixtures
@ -1304,3 +1310,11 @@ def climate_intermatic_pe653_fixture(client, climate_intermatic_pe653_state):
node = Node(client, copy.deepcopy(climate_intermatic_pe653_state))
client.driver.controller.nodes[node.node_id] = node
return node
@pytest.fixture(name="central_scene_node")
def central_scene_node_fixture(client, central_scene_node_state):
"""Mock a node with the Central Scene CC."""
node = Node(client, copy.deepcopy(central_scene_node_state))
client.driver.controller.nodes[node.node_id] = node
return node

View File

@ -0,0 +1,431 @@
{
"nodeId": 51,
"index": 0,
"status": 4,
"ready": true,
"isListening": true,
"isRouting": true,
"isSecure": true,
"firmwareVersion": "1.3.0",
"interviewAttempts": 1,
"endpoints": [
{
"nodeId": 51,
"index": 0,
"deviceClass": {
"basic": {
"key": 4,
"label": "Routing Slave"
},
"generic": {
"key": 24,
"label": "Wall Controller"
},
"specific": {
"key": 0,
"label": "Unused"
},
"mandatorySupportedCCs": [],
"mandatoryControlledCCs": []
},
"commandClasses": [
{
"id": 159,
"name": "Security 2",
"version": 1,
"isSecure": true
},
{
"id": 152,
"name": "Security",
"version": 1,
"isSecure": true
},
{
"id": 108,
"name": "Supervision",
"version": 1,
"isSecure": false
},
{
"id": 85,
"name": "Transport Service",
"version": 2,
"isSecure": false
},
{
"id": 134,
"name": "Version",
"version": 3,
"isSecure": true
},
{
"id": 133,
"name": "Association",
"version": 2,
"isSecure": true
},
{
"id": 89,
"name": "Association Group Information",
"version": 3,
"isSecure": true
},
{
"id": 91,
"name": "Central Scene",
"version": 3,
"isSecure": true
}
]
}
],
"values": [
{
"endpoint": 0,
"commandClass": 91,
"commandClassName": "Central Scene",
"property": "scene",
"propertyKey": "001",
"propertyName": "scene",
"propertyKeyName": "001",
"ccVersion": 3,
"metadata": {
"type": "number",
"readable": true,
"writeable": false,
"label": "Scene 001",
"min": 0,
"max": 255,
"states": {
"0": "KeyPressed"
},
"stateful": false,
"secret": false
}
},
{
"endpoint": 0,
"commandClass": 91,
"commandClassName": "Central Scene",
"property": "scene",
"propertyKey": "002",
"propertyName": "scene",
"propertyKeyName": "002",
"ccVersion": 3,
"metadata": {
"type": "number",
"readable": true,
"writeable": false,
"label": "Scene 002",
"min": 0,
"max": 255,
"states": {
"0": "KeyPressed",
"1": "KeyReleased",
"2": "KeyHeldDown",
"3": "KeyPressed2x",
"4": "KeyPressed3x",
"5": "KeyPressed4x",
"6": "KeyPressed5x"
},
"stateful": false,
"secret": false
}
},
{
"endpoint": 0,
"commandClass": 91,
"commandClassName": "Central Scene",
"property": "slowRefresh",
"propertyName": "slowRefresh",
"ccVersion": 3,
"metadata": {
"type": "boolean",
"readable": true,
"writeable": true,
"description": "When this is true, KeyHeldDown notifications are sent every 55s. When this is false, the notifications are sent every 200ms.",
"label": "Send held down notifications at a slow rate",
"stateful": true,
"secret": false
}
},
{
"endpoint": 0,
"commandClass": 134,
"commandClassName": "Version",
"property": "libraryType",
"propertyName": "libraryType",
"ccVersion": 3,
"metadata": {
"type": "number",
"readable": true,
"writeable": false,
"label": "Library type",
"states": {
"0": "Unknown",
"1": "Static Controller",
"2": "Controller",
"3": "Enhanced Slave",
"4": "Slave",
"5": "Installer",
"6": "Routing Slave",
"7": "Bridge Controller",
"8": "Device under Test",
"9": "N/A",
"10": "AV Remote",
"11": "AV Device"
},
"stateful": true,
"secret": false
},
"value": 3
},
{
"endpoint": 0,
"commandClass": 134,
"commandClassName": "Version",
"property": "protocolVersion",
"propertyName": "protocolVersion",
"ccVersion": 3,
"metadata": {
"type": "string",
"readable": true,
"writeable": false,
"label": "Z-Wave protocol version",
"stateful": true,
"secret": false
},
"value": "6.81"
},
{
"endpoint": 0,
"commandClass": 134,
"commandClassName": "Version",
"property": "firmwareVersions",
"propertyName": "firmwareVersions",
"ccVersion": 3,
"metadata": {
"type": "string[]",
"readable": true,
"writeable": false,
"label": "Z-Wave chip firmware versions",
"stateful": true,
"secret": false
},
"value": ["1.3"]
},
{
"endpoint": 0,
"commandClass": 134,
"commandClassName": "Version",
"property": "hardwareVersion",
"propertyName": "hardwareVersion",
"ccVersion": 3,
"metadata": {
"type": "number",
"readable": true,
"writeable": false,
"label": "Z-Wave chip hardware version",
"stateful": true,
"secret": false
},
"value": 0
},
{
"endpoint": 0,
"commandClass": 134,
"commandClassName": "Version",
"property": "sdkVersion",
"propertyName": "sdkVersion",
"ccVersion": 3,
"metadata": {
"type": "string",
"readable": true,
"writeable": false,
"label": "SDK version",
"stateful": true,
"secret": false
},
"value": "7.0.0"
},
{
"endpoint": 0,
"commandClass": 134,
"commandClassName": "Version",
"property": "applicationFrameworkAPIVersion",
"propertyName": "applicationFrameworkAPIVersion",
"ccVersion": 3,
"metadata": {
"type": "string",
"readable": true,
"writeable": false,
"label": "Z-Wave application framework API version",
"stateful": true,
"secret": false
},
"value": "unused"
},
{
"endpoint": 0,
"commandClass": 134,
"commandClassName": "Version",
"property": "applicationFrameworkBuildNumber",
"propertyName": "applicationFrameworkBuildNumber",
"ccVersion": 3,
"metadata": {
"type": "string",
"readable": true,
"writeable": false,
"label": "Z-Wave application framework API build number",
"stateful": true,
"secret": false
},
"value": 0
},
{
"endpoint": 0,
"commandClass": 134,
"commandClassName": "Version",
"property": "hostInterfaceVersion",
"propertyName": "hostInterfaceVersion",
"ccVersion": 3,
"metadata": {
"type": "string",
"readable": true,
"writeable": false,
"label": "Serial API version",
"stateful": true,
"secret": false
},
"value": "unused"
},
{
"endpoint": 0,
"commandClass": 134,
"commandClassName": "Version",
"property": "hostInterfaceBuildNumber",
"propertyName": "hostInterfaceBuildNumber",
"ccVersion": 3,
"metadata": {
"type": "string",
"readable": true,
"writeable": false,
"label": "Serial API build number",
"stateful": true,
"secret": false
},
"value": 0
},
{
"endpoint": 0,
"commandClass": 134,
"commandClassName": "Version",
"property": "zWaveProtocolVersion",
"propertyName": "zWaveProtocolVersion",
"ccVersion": 3,
"metadata": {
"type": "string",
"readable": true,
"writeable": false,
"label": "Z-Wave protocol version",
"stateful": true,
"secret": false
},
"value": "6.81.0"
},
{
"endpoint": 0,
"commandClass": 134,
"commandClassName": "Version",
"property": "zWaveProtocolBuildNumber",
"propertyName": "zWaveProtocolBuildNumber",
"ccVersion": 3,
"metadata": {
"type": "string",
"readable": true,
"writeable": false,
"label": "Z-Wave protocol build number",
"stateful": true,
"secret": false
},
"value": 255
},
{
"endpoint": 0,
"commandClass": 134,
"commandClassName": "Version",
"property": "applicationVersion",
"propertyName": "applicationVersion",
"ccVersion": 3,
"metadata": {
"type": "string",
"readable": true,
"writeable": false,
"label": "Application version",
"stateful": true,
"secret": false
},
"value": "1.3.0"
},
{
"endpoint": 0,
"commandClass": 134,
"commandClassName": "Version",
"property": "applicationBuildNumber",
"propertyName": "applicationBuildNumber",
"ccVersion": 3,
"metadata": {
"type": "string",
"readable": true,
"writeable": false,
"label": "Application build number",
"stateful": true,
"secret": false
},
"value": 255
}
],
"isFrequentListening": false,
"maxDataRate": 100000,
"supportedDataRates": [40000, 100000],
"protocolVersion": 3,
"supportsBeaming": true,
"supportsSecurity": false,
"nodeType": 1,
"deviceClass": {
"basic": {
"key": 4,
"label": "Routing Slave"
},
"generic": {
"key": 24,
"label": "Wall Controller"
},
"specific": {
"key": 0,
"label": "Unused"
},
"mandatorySupportedCCs": [],
"mandatoryControlledCCs": []
},
"interviewStage": "Complete",
"statistics": {
"commandsTX": 42,
"commandsRX": 46,
"commandsDroppedRX": 0,
"commandsDroppedTX": 0,
"timeoutResponse": 0,
"rtt": 55.4,
"rssi": -72,
"lwr": {
"protocolDataRate": 3,
"repeaters": [],
"rssi": -72,
"repeaterRSSI": []
}
},
"highestSecurityClass": 0,
"isControllerNode": false,
"keepAwake": false
}

View File

@ -0,0 +1,175 @@
"""Test the Z-Wave JS event platform."""
from datetime import timedelta
from freezegun import freeze_time
from zwave_js_server.event import Event
from homeassistant.components.event import ATTR_EVENT_TYPE
from homeassistant.components.zwave_js.const import ATTR_VALUE
from homeassistant.const import STATE_UNKNOWN
from homeassistant.core import HomeAssistant
from homeassistant.util import dt as dt_util
BASIC_EVENT_VALUE_ENTITY = "event.honeywell_in_wall_smart_fan_control_event_value"
CENTRAL_SCENE_ENTITY = "event.node_51_scene_002"
async def test_basic(
hass: HomeAssistant, client, fan_honeywell_39358, integration
) -> None:
"""Test the Basic CC event entity."""
dt_util.now()
fut = dt_util.now() + timedelta(minutes=1)
node = fan_honeywell_39358
state = hass.states.get(BASIC_EVENT_VALUE_ENTITY)
assert state
assert state.state == STATE_UNKNOWN
event = Event(
type="value notification",
data={
"source": "node",
"event": "value notification",
"nodeId": node.node_id,
"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,
},
},
)
with freeze_time(fut):
node.receive_event(event)
state = hass.states.get(BASIC_EVENT_VALUE_ENTITY)
assert state
assert state.state == dt_util.as_utc(fut).isoformat(timespec="milliseconds")
attributes = state.attributes
assert attributes[ATTR_EVENT_TYPE] == "Basic event value"
assert attributes[ATTR_VALUE] == 255
async def test_central_scene(
hass: HomeAssistant, client, central_scene_node, integration
) -> None:
"""Test the Central Scene CC event entity."""
dt_util.now()
fut = dt_util.now() + timedelta(minutes=1)
node = central_scene_node
state = hass.states.get(CENTRAL_SCENE_ENTITY)
assert state
assert state.state == STATE_UNKNOWN
event = Event(
type="value notification",
data={
"source": "node",
"event": "value notification",
"nodeId": node.node_id,
"args": {
"endpoint": 0,
"commandClass": 91,
"commandClassName": "Central Scene",
"property": "scene",
"propertyKey": "002",
"propertyName": "scene",
"propertyKeyName": "002",
"ccVersion": 3,
"metadata": {
"type": "number",
"readable": True,
"writeable": False,
"label": "Scene 002",
"min": 0,
"max": 255,
"states": {
"0": "KeyPressed",
"1": "KeyReleased",
"2": "KeyHeldDown",
"3": "KeyPressed2x",
"4": "KeyPressed3x",
"5": "KeyPressed4x",
"6": "KeyPressed5x",
},
"stateful": False,
"secret": False,
},
"value": 1,
},
},
)
with freeze_time(fut):
node.receive_event(event)
state = hass.states.get(CENTRAL_SCENE_ENTITY)
assert state
assert state.state == dt_util.as_utc(fut).isoformat(timespec="milliseconds")
attributes = state.attributes
assert attributes[ATTR_EVENT_TYPE] == "KeyReleased"
assert attributes[ATTR_VALUE] == 1
# Try invalid value
event = Event(
type="value notification",
data={
"source": "node",
"event": "value notification",
"nodeId": node.node_id,
"args": {
"endpoint": 0,
"commandClass": 91,
"commandClassName": "Central Scene",
"property": "scene",
"propertyKey": "002",
"propertyName": "scene",
"propertyKeyName": "002",
"ccVersion": 3,
"metadata": {
"type": "number",
"readable": True,
"writeable": False,
"label": "Scene 002",
"min": 0,
"max": 255,
"states": {
"0": "KeyPressed",
"1": "KeyReleased",
"2": "KeyHeldDown",
"3": "KeyPressed2x",
"4": "KeyPressed3x",
"5": "KeyPressed4x",
"6": "KeyPressed5x",
},
"stateful": False,
"secret": False,
},
},
},
)
with freeze_time(fut + timedelta(minutes=10)):
node.receive_event(event)
# Nothing should have changed even though the time has changed
state = hass.states.get(CENTRAL_SCENE_ENTITY)
assert state
assert state.state == dt_util.as_utc(fut).isoformat(timespec="milliseconds")
attributes = state.attributes
assert attributes[ATTR_EVENT_TYPE] == "KeyReleased"
assert attributes[ATTR_VALUE] == 1

View File

@ -1,4 +1,4 @@
"""Test Z-Wave JS (value notification) events."""
"""Test Z-Wave JS events."""
from unittest.mock import AsyncMock
import pytest