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

View File

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

View File

@ -135,7 +135,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
self._setpoint_values[enum] = self.get_zwave_value( self._setpoint_values[enum] = self.get_zwave_value(
THERMOSTAT_SETPOINT_PROPERTY, THERMOSTAT_SETPOINT_PROPERTY,
command_class=CommandClass.THERMOSTAT_SETPOINT, command_class=CommandClass.THERMOSTAT_SETPOINT,
value_property_key=enum.value.key, value_property_key=enum.value,
add_to_watched_value_ids=True, add_to_watched_value_ids=True,
) )
# Use the first found non N/A setpoint value to always determine the # 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__) LOGGER = logging.getLogger(__package__)
# constants for events # 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_NODE_ID = "node_id"
ATTR_HOME_ID = "home_id" ATTR_HOME_ID = "home_id"
ATTR_ENDPOINT = "endpoint" ATTR_ENDPOINT = "endpoint"
@ -43,6 +44,11 @@ ATTR_PROPERTY_KEY_NAME = "property_key_name"
ATTR_PROPERTY = "property" ATTR_PROPERTY = "property"
ATTR_PROPERTY_KEY = "property_key" ATTR_PROPERTY_KEY = "property_key"
ATTR_PARAMETERS = "parameters" 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 constants
SERVICE_SET_CONFIG_PARAMETER = "set_config_parameter" 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: async def _async_set_color(self, color: ColorComponent, new_value: int) -> None:
"""Set defined color to given value.""" """Set defined color to given value."""
property_key = color.value
# actually set the new color value # actually set the new color value
target_zwave_value = self.get_zwave_value( target_zwave_value = self.get_zwave_value(
"targetColor", "targetColor",
CommandClass.SWITCH_COLOR, CommandClass.SWITCH_COLOR,
value_property_key=property_key.key, value_property_key=color.value,
) )
if target_zwave_value is None: if target_zwave_value is None:
# guard for unsupported color # guard for unsupported color
@ -315,27 +314,27 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
red_val = self.get_zwave_value( red_val = self.get_zwave_value(
"currentColor", "currentColor",
CommandClass.SWITCH_COLOR, CommandClass.SWITCH_COLOR,
value_property_key=ColorComponent.RED.value.key, value_property_key=ColorComponent.RED.value,
) )
green_val = self.get_zwave_value( green_val = self.get_zwave_value(
"currentColor", "currentColor",
CommandClass.SWITCH_COLOR, CommandClass.SWITCH_COLOR,
value_property_key=ColorComponent.GREEN.value.key, value_property_key=ColorComponent.GREEN.value,
) )
blue_val = self.get_zwave_value( blue_val = self.get_zwave_value(
"currentColor", "currentColor",
CommandClass.SWITCH_COLOR, CommandClass.SWITCH_COLOR,
value_property_key=ColorComponent.BLUE.value.key, value_property_key=ColorComponent.BLUE.value,
) )
ww_val = self.get_zwave_value( ww_val = self.get_zwave_value(
"currentColor", "currentColor",
CommandClass.SWITCH_COLOR, CommandClass.SWITCH_COLOR,
value_property_key=ColorComponent.WARM_WHITE.value.key, value_property_key=ColorComponent.WARM_WHITE.value,
) )
cw_val = self.get_zwave_value( cw_val = self.get_zwave_value(
"currentColor", "currentColor",
CommandClass.SWITCH_COLOR, CommandClass.SWITCH_COLOR,
value_property_key=ColorComponent.COLD_WHITE.value.key, value_property_key=ColorComponent.COLD_WHITE.value,
) )
# prefer the (new) combined color property # prefer the (new) combined color property
# https://github.com/zwave-js/node-zwave-js/pull/1782 # https://github.com/zwave-js/node-zwave-js/pull/1782

View File

@ -3,7 +3,7 @@
"name": "Z-Wave JS", "name": "Z-Wave JS",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/zwave_js", "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"], "codeowners": ["@home-assistant/z-wave"],
"dependencies": ["http", "websocket_api"] "dependencies": ["http", "websocket_api"]
} }

View File

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

View File

@ -2402,4 +2402,4 @@ zigpy==0.33.0
zm-py==0.5.2 zm-py==0.5.2
# homeassistant.components.zwave_js # 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 zigpy==0.33.0
# homeassistant.components.zwave_js # 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 assert len(client.async_send_command.call_args_list) == 1
args = client.async_send_command.call_args[0][0] args = client.async_send_command.call_args[0][0]
assert args["command"] == "update_log_config" assert args["command"] == "update_log_config"
assert args["config"] == {"level": 0} assert args["config"] == {"level": "error"}
client.async_send_command.reset_mock() 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] args = client.async_send_command.call_args[0][0]
assert args["command"] == "update_log_config" assert args["command"] == "update_log_config"
assert args["config"] == { assert args["config"] == {
"level": 0, "level": "error",
"logToFile": True, "logToFile": True,
"filename": "/test", "filename": "/test",
"forceConsole": True, "forceConsole": True,
@ -490,7 +490,7 @@ async def test_get_log_config(hass, client, integration, hass_ws_client):
"success": True, "success": True,
"config": { "config": {
"enabled": True, "enabled": True,
"level": 0, "level": "error",
"logToFile": False, "logToFile": False,
"filename": "/test.txt", "filename": "/test.txt",
"forceConsole": False, "forceConsole": False,

View File

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