Bump zwave-js-server-python to 0.23.0 to support zwave-js 7 (#48094)

* Bump zwave-js-server-python to 0.23.0 and update integration to support schema changes

* refactor notification evenets a bit

* fix tests and bug fixes

* additional changes due to new PR

* add command class and command name

* use new event names so we can retain event property names

* handle command status being returned from async_set_config_parameter

* bump dependency version

* adjust log message to be consistent

* disable pylint warning

* Update homeassistant/components/zwave_js/services.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* add test for awake node

* switch async_get_registry to async_get

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Raman Gupta 2021-03-29 16:28:55 -04:00 committed by GitHub
parent cf6352e93c
commit dda9f957b6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 175 additions and 61 deletions

View file

@ -8,7 +8,10 @@ from async_timeout import timeout
from zwave_js_server.client import Client as ZwaveClient
from zwave_js_server.exceptions import BaseZwaveJSServerError, InvalidServerVersion
from zwave_js_server.model.node import Node as ZwaveNode
from zwave_js_server.model.notification import Notification
from zwave_js_server.model.notification import (
EntryControlNotification,
NotificationNotification,
)
from zwave_js_server.model.value import ValueNotification
from homeassistant.config_entries import ConfigEntry
@ -29,7 +32,12 @@ from .api import async_register_api
from .const import (
ATTR_COMMAND_CLASS,
ATTR_COMMAND_CLASS_NAME,
ATTR_DATA_TYPE,
ATTR_ENDPOINT,
ATTR_EVENT,
ATTR_EVENT_DATA,
ATTR_EVENT_LABEL,
ATTR_EVENT_TYPE,
ATTR_HOME_ID,
ATTR_LABEL,
ATTR_NODE_ID,
@ -51,7 +59,8 @@ from .const import (
EVENT_DEVICE_ADDED_TO_REGISTRY,
LOGGER,
PLATFORMS,
ZWAVE_JS_EVENT,
ZWAVE_JS_NOTIFICATION_EVENT,
ZWAVE_JS_VALUE_NOTIFICATION_EVENT,
)
from .discovery import async_discover_values
from .helpers import get_device_id
@ -102,7 +111,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await async_ensure_addon_running(hass, entry)
client = ZwaveClient(entry.data[CONF_URL], async_get_clientsession(hass))
dev_reg = await device_registry.async_get_registry(hass)
dev_reg = device_registry.async_get(hass)
ent_reg = entity_registry.async_get(hass)
@callback
@ -169,9 +178,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if notification.metadata.states:
value = notification.metadata.states.get(str(value), value)
hass.bus.async_fire(
ZWAVE_JS_EVENT,
ZWAVE_JS_VALUE_NOTIFICATION_EVENT,
{
ATTR_TYPE: "value_notification",
ATTR_DOMAIN: DOMAIN,
ATTR_NODE_ID: notification.node.node_id,
ATTR_HOME_ID: client.driver.controller.home_id,
@ -190,21 +198,41 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
@callback
def async_on_notification(notification: Notification) -> None:
def async_on_notification(
notification: EntryControlNotification | NotificationNotification,
) -> None:
"""Relay stateless notification events from Z-Wave nodes to hass."""
device = dev_reg.async_get_device({get_device_id(client, notification.node)})
hass.bus.async_fire(
ZWAVE_JS_EVENT,
{
ATTR_TYPE: "notification",
ATTR_DOMAIN: DOMAIN,
ATTR_NODE_ID: notification.node.node_id,
ATTR_HOME_ID: client.driver.controller.home_id,
ATTR_DEVICE_ID: device.id, # type: ignore
ATTR_LABEL: notification.notification_label,
ATTR_PARAMETERS: notification.parameters,
},
)
event_data = {
ATTR_DOMAIN: DOMAIN,
ATTR_NODE_ID: notification.node.node_id,
ATTR_HOME_ID: client.driver.controller.home_id,
ATTR_DEVICE_ID: device.id, # type: ignore
ATTR_COMMAND_CLASS: notification.command_class,
}
if isinstance(notification, EntryControlNotification):
event_data.update(
{
ATTR_COMMAND_CLASS_NAME: "Entry Control",
ATTR_EVENT_TYPE: notification.event_type,
ATTR_DATA_TYPE: notification.data_type,
ATTR_EVENT_DATA: notification.event_data,
}
)
else:
event_data.update(
{
ATTR_COMMAND_CLASS_NAME: "Notification",
ATTR_LABEL: notification.label,
ATTR_TYPE: notification.type_,
ATTR_EVENT: notification.event,
ATTR_EVENT_LABEL: notification.event_label,
ATTR_PARAMETERS: notification.parameters,
}
)
hass.bus.async_fire(ZWAVE_JS_NOTIFICATION_EVENT, event_data)
entry_hass_data: dict = hass.data[DOMAIN].setdefault(entry.entry_id, {})
# connect and throw error if connection failed

View file

@ -46,6 +46,10 @@ FILENAME = "filename"
ENABLED = "enabled"
FORCE_CONSOLE = "force_console"
# constants for setting config parameters
VALUE_ID = "value_id"
STATUS = "status"
@callback
def async_register_api(hass: HomeAssistant) -> None:
@ -321,7 +325,7 @@ async def websocket_set_config_parameter(
client = hass.data[DOMAIN][entry_id][DATA_CLIENT]
node = client.driver.controller.nodes[node_id]
try:
result = await async_set_config_parameter(
zwave_value, cmd_status = await async_set_config_parameter(
node, value, property_, property_key=property_key
)
except (InvalidNewValue, NotFoundError, NotImplementedError, SetValueFailed) as err:
@ -340,7 +344,10 @@ async def websocket_set_config_parameter(
connection.send_result(
msg[ID],
str(result),
{
VALUE_ID: zwave_value.value_id,
STATUS: cmd_status,
},
)
@ -395,11 +402,6 @@ def websocket_get_config_parameters(
)
def convert_log_level_to_enum(value: str) -> LogLevel:
"""Convert log level string to LogLevel enum."""
return LogLevel[value.upper()]
def filename_is_present_if_logging_to_file(obj: dict) -> dict:
"""Validate that filename is provided if log_to_file is True."""
if obj.get(LOG_TO_FILE, False) and FILENAME not in obj:
@ -420,8 +422,8 @@ def filename_is_present_if_logging_to_file(obj: dict) -> dict:
vol.Optional(LEVEL): vol.All(
cv.string,
vol.Lower,
vol.In([log_level.name.lower() for log_level in LogLevel]),
lambda val: LogLevel[val.upper()],
vol.In([log_level.value for log_level in LogLevel]),
lambda val: LogLevel(val), # pylint: disable=unnecessary-lambda
),
vol.Optional(LOG_TO_FILE): cv.boolean,
vol.Optional(FILENAME): cv.string,

View file

@ -135,7 +135,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
self._setpoint_values[enum] = self.get_zwave_value(
THERMOSTAT_SETPOINT_PROPERTY,
command_class=CommandClass.THERMOSTAT_SETPOINT,
value_property_key=enum.value.key,
value_property_key=enum.value,
add_to_watched_value_ids=True,
)
# Use the first found non N/A setpoint value to always determine the

View file

@ -28,7 +28,8 @@ EVENT_DEVICE_ADDED_TO_REGISTRY = f"{DOMAIN}_device_added_to_registry"
LOGGER = logging.getLogger(__package__)
# constants for events
ZWAVE_JS_EVENT = f"{DOMAIN}_event"
ZWAVE_JS_VALUE_NOTIFICATION_EVENT = f"{DOMAIN}_value_notification"
ZWAVE_JS_NOTIFICATION_EVENT = f"{DOMAIN}_notification"
ATTR_NODE_ID = "node_id"
ATTR_HOME_ID = "home_id"
ATTR_ENDPOINT = "endpoint"
@ -43,6 +44,11 @@ ATTR_PROPERTY_KEY_NAME = "property_key_name"
ATTR_PROPERTY = "property"
ATTR_PROPERTY_KEY = "property_key"
ATTR_PARAMETERS = "parameters"
ATTR_EVENT = "event"
ATTR_EVENT_LABEL = "event_label"
ATTR_EVENT_TYPE = "event_type"
ATTR_EVENT_DATA = "event_data"
ATTR_DATA_TYPE = "data_type"
# service constants
SERVICE_SET_CONFIG_PARAMETER = "set_config_parameter"

View file

@ -247,12 +247,11 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
async def _async_set_color(self, color: ColorComponent, new_value: int) -> None:
"""Set defined color to given value."""
property_key = color.value
# actually set the new color value
target_zwave_value = self.get_zwave_value(
"targetColor",
CommandClass.SWITCH_COLOR,
value_property_key=property_key.key,
value_property_key=color.value,
)
if target_zwave_value is None:
# guard for unsupported color
@ -315,27 +314,27 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
red_val = self.get_zwave_value(
"currentColor",
CommandClass.SWITCH_COLOR,
value_property_key=ColorComponent.RED.value.key,
value_property_key=ColorComponent.RED.value,
)
green_val = self.get_zwave_value(
"currentColor",
CommandClass.SWITCH_COLOR,
value_property_key=ColorComponent.GREEN.value.key,
value_property_key=ColorComponent.GREEN.value,
)
blue_val = self.get_zwave_value(
"currentColor",
CommandClass.SWITCH_COLOR,
value_property_key=ColorComponent.BLUE.value.key,
value_property_key=ColorComponent.BLUE.value,
)
ww_val = self.get_zwave_value(
"currentColor",
CommandClass.SWITCH_COLOR,
value_property_key=ColorComponent.WARM_WHITE.value.key,
value_property_key=ColorComponent.WARM_WHITE.value,
)
cw_val = self.get_zwave_value(
"currentColor",
CommandClass.SWITCH_COLOR,
value_property_key=ColorComponent.COLD_WHITE.value.key,
value_property_key=ColorComponent.COLD_WHITE.value,
)
# prefer the (new) combined color property
# https://github.com/zwave-js/node-zwave-js/pull/1782

View file

@ -3,7 +3,7 @@
"name": "Z-Wave JS",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/zwave_js",
"requirements": ["zwave-js-server-python==0.22.0"],
"requirements": ["zwave-js-server-python==0.23.0"],
"codeowners": ["@home-assistant/z-wave"],
"dependencies": ["http", "websocket_api"]
}

View file

@ -4,6 +4,7 @@ from __future__ import annotations
import logging
import voluptuous as vol
from zwave_js_server.const import CommandStatus
from zwave_js_server.model.node import Node as ZwaveNode
from zwave_js_server.util.node import async_set_config_parameter
@ -104,26 +105,23 @@ class ZWaveServices:
new_value = service.data[const.ATTR_CONFIG_VALUE]
for node in nodes:
zwave_value = await async_set_config_parameter(
zwave_value, cmd_status = await async_set_config_parameter(
node,
new_value,
property_or_property_name,
property_key=property_key,
)
if zwave_value:
_LOGGER.info(
"Set configuration parameter %s on Node %s with value %s",
zwave_value,
node,
new_value,
)
if cmd_status == CommandStatus.ACCEPTED:
msg = "Set configuration parameter %s on Node %s with value %s"
else:
raise ValueError(
f"Unable to set configuration parameter on Node {node} with "
f"value {new_value}"
msg = (
"Added command to queue to set configuration parameter %s on Node "
"%s with value %s. Parameter will be set when the device wakes up"
)
_LOGGER.info(msg, zwave_value, node, new_value)
async def async_poll_value(self, service: ServiceCall) -> None:
"""Poll value on a node."""
for entity_id in service.data[ATTR_ENTITY_ID]:

View file

@ -2402,4 +2402,4 @@ zigpy==0.33.0
zm-py==0.5.2
# homeassistant.components.zwave_js
zwave-js-server-python==0.22.0
zwave-js-server-python==0.23.0

View file

@ -1251,4 +1251,4 @@ zigpy-znp==0.4.0
zigpy==0.33.0
# homeassistant.components.zwave_js
zwave-js-server-python==0.22.0
zwave-js-server-python==0.23.0

View file

@ -381,7 +381,7 @@ async def test_update_log_config(hass, client, integration, hass_ws_client):
assert len(client.async_send_command.call_args_list) == 1
args = client.async_send_command.call_args[0][0]
assert args["command"] == "update_log_config"
assert args["config"] == {"level": 0}
assert args["config"] == {"level": "error"}
client.async_send_command.reset_mock()
@ -428,7 +428,7 @@ async def test_update_log_config(hass, client, integration, hass_ws_client):
args = client.async_send_command.call_args[0][0]
assert args["command"] == "update_log_config"
assert args["config"] == {
"level": 0,
"level": "error",
"logToFile": True,
"filename": "/test",
"forceConsole": True,
@ -490,7 +490,7 @@ async def test_get_log_config(hass, client, integration, hass_ws_client):
"success": True,
"config": {
"enabled": True,
"level": 0,
"level": "error",
"logToFile": False,
"filename": "/test.txt",
"forceConsole": False,

View file

@ -1,4 +1,5 @@
"""Test Z-Wave JS (value notification) events."""
from zwave_js_server.const import CommandClass
from zwave_js_server.event import Event
from tests.common import async_capture_events
@ -8,7 +9,7 @@ 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")
events = async_capture_events(hass, "zwave_js_value_notification")
# Publish fake Basic Set value notification
event = Event(
@ -137,25 +138,59 @@ async def test_notifications(hass, hank_binary_switch, integration, client):
"""Test notification events."""
# just pick a random node to fake the value notification events
node = hank_binary_switch
events = async_capture_events(hass, "zwave_js_event")
events = async_capture_events(hass, "zwave_js_notification")
# Publish fake Basic Set value notification
# Publish fake Notification CC notification
event = Event(
type="notification",
data={
"source": "node",
"event": "notification",
"nodeId": 23,
"notificationLabel": "Keypad lock operation",
"parameters": {"userId": 1},
"nodeId": 32,
"ccId": 113,
"args": {
"type": 6,
"event": 5,
"label": "Access Control",
"eventLabel": "Keypad lock operation",
"parameters": {"userId": 1},
},
},
)
node.receive_event(event)
# wait for the event
await hass.async_block_till_done()
assert len(events) == 1
assert events[0].data["type"] == "notification"
assert events[0].data["home_id"] == client.driver.controller.home_id
assert events[0].data["node_id"] == 32
assert events[0].data["label"] == "Keypad lock operation"
assert events[0].data["type"] == 6
assert events[0].data["event"] == 5
assert events[0].data["label"] == "Access Control"
assert events[0].data["event_label"] == "Keypad lock operation"
assert events[0].data["parameters"]["userId"] == 1
assert events[0].data["command_class"] == CommandClass.NOTIFICATION
assert events[0].data["command_class_name"] == "Notification"
# Publish fake Entry Control CC notification
event = Event(
type="notification",
data={
"source": "node",
"event": "notification",
"nodeId": 32,
"ccId": 111,
"args": {"eventType": 5, "dataType": 2, "eventData": "555"},
},
)
node.receive_event(event)
# wait for the event
await hass.async_block_till_done()
assert len(events) == 2
assert events[1].data["home_id"] == client.driver.controller.home_id
assert events[1].data["node_id"] == 32
assert events[1].data["event_type"] == 5
assert events[1].data["data_type"] == 2
assert events[1].data["event_data"] == "555"
assert events[1].data["command_class"] == CommandClass.ENTRY_CONTROL
assert events[1].data["command_class_name"] == "Entry Control"

View file

@ -296,6 +296,52 @@ async def test_set_config_parameter(hass, client, multisensor_6, integration):
blocking=True,
)
# Test that when a device is awake, we call async_send_command instead of
# async_send_command_no_wait
multisensor_6.handle_wake_up(None)
await hass.services.async_call(
DOMAIN,
SERVICE_SET_CONFIG_PARAMETER,
{
ATTR_ENTITY_ID: AIR_TEMPERATURE_SENSOR,
ATTR_CONFIG_PARAMETER: 102,
ATTR_CONFIG_PARAMETER_BITMASK: 1,
ATTR_CONFIG_VALUE: 1,
},
blocking=True,
)
assert len(client.async_send_command.call_args_list) == 1
args = client.async_send_command.call_args[0][0]
assert args["command"] == "node.set_value"
assert args["nodeId"] == 52
assert args["valueId"] == {
"commandClassName": "Configuration",
"commandClass": 112,
"endpoint": 0,
"property": 102,
"propertyName": "Group 2: Send battery reports",
"propertyKey": 1,
"metadata": {
"type": "number",
"readable": True,
"writeable": True,
"valueSize": 4,
"min": 0,
"max": 1,
"default": 1,
"format": 0,
"allowManualEntry": True,
"label": "Group 2: Send battery reports",
"description": "Include battery information in periodic reports to Group 2",
"isFromConfig": True,
},
"value": 0,
}
assert args["value"] == 1
client.async_send_command.reset_mock()
async def test_poll_value(
hass, client, climate_radio_thermostat_ct100_plus_different_endpoints, integration