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:
parent
cf6352e93c
commit
dda9f957b6
12 changed files with 175 additions and 61 deletions
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
@ -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]:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue