diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 3de4eac0461..10cc2543921 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -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 diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 087b94d0060..eed04a34c7d 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -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, diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index ccae63dd3ea..b814aef2a9d 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -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 diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index 49beb06283e..d9c3a3ebb9b 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -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" diff --git a/homeassistant/components/zwave_js/light.py b/homeassistant/components/zwave_js/light.py index 6b42c0ef08e..d809874c432 100644 --- a/homeassistant/components/zwave_js/light.py +++ b/homeassistant/components/zwave_js/light.py @@ -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 diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 7c97836c3b8..4d3f5c5f42d 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -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"] } diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py index 1944e4b3dd0..6b0d99f4920 100644 --- a/homeassistant/components/zwave_js/services.py +++ b/homeassistant/components/zwave_js/services.py @@ -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]: diff --git a/requirements_all.txt b/requirements_all.txt index c39a16c31d2..567a025a8cf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ff24952f6dd..20bc97aebe2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 75bde222111..304e941a32a 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -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, diff --git a/tests/components/zwave_js/test_events.py b/tests/components/zwave_js/test_events.py index e40782270a9..15de6aa8887 100644 --- a/tests/components/zwave_js/test_events.py +++ b/tests/components/zwave_js/test_events.py @@ -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" diff --git a/tests/components/zwave_js/test_services.py b/tests/components/zwave_js/test_services.py index 585e03402eb..a03d0b4544b 100644 --- a/tests/components/zwave_js/test_services.py +++ b/tests/components/zwave_js/test_services.py @@ -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