mirror of
https://github.com/home-assistant/core.git
synced 2025-07-17 02:07:09 +00:00
Add zwave_js.refresh_notifications
service (#101370)
This commit is contained in:
parent
21af563dfe
commit
0fcaa2c581
@ -72,6 +72,8 @@ ATTR_STATUS = "status"
|
|||||||
ATTR_ACKNOWLEDGED_FRAMES = "acknowledged_frames"
|
ATTR_ACKNOWLEDGED_FRAMES = "acknowledged_frames"
|
||||||
ATTR_EVENT_TYPE_LABEL = "event_type_label"
|
ATTR_EVENT_TYPE_LABEL = "event_type_label"
|
||||||
ATTR_DATA_TYPE_LABEL = "data_type_label"
|
ATTR_DATA_TYPE_LABEL = "data_type_label"
|
||||||
|
ATTR_NOTIFICATION_TYPE = "notification_type"
|
||||||
|
ATTR_NOTIFICATION_EVENT = "notification_event"
|
||||||
|
|
||||||
ATTR_NODE = "node"
|
ATTR_NODE = "node"
|
||||||
ATTR_ZWAVE_VALUE = "zwave_value"
|
ATTR_ZWAVE_VALUE = "zwave_value"
|
||||||
@ -92,6 +94,7 @@ SERVICE_CLEAR_LOCK_USERCODE = "clear_lock_usercode"
|
|||||||
SERVICE_INVOKE_CC_API = "invoke_cc_api"
|
SERVICE_INVOKE_CC_API = "invoke_cc_api"
|
||||||
SERVICE_MULTICAST_SET_VALUE = "multicast_set_value"
|
SERVICE_MULTICAST_SET_VALUE = "multicast_set_value"
|
||||||
SERVICE_PING = "ping"
|
SERVICE_PING = "ping"
|
||||||
|
SERVICE_REFRESH_NOTIFICATIONS = "refresh_notifications"
|
||||||
SERVICE_REFRESH_VALUE = "refresh_value"
|
SERVICE_REFRESH_VALUE = "refresh_value"
|
||||||
SERVICE_RESET_METER = "reset_meter"
|
SERVICE_RESET_METER = "reset_meter"
|
||||||
SERVICE_SET_CONFIG_PARAMETER = "set_config_parameter"
|
SERVICE_SET_CONFIG_PARAMETER = "set_config_parameter"
|
||||||
|
@ -4,11 +4,12 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Generator, Sequence
|
from collections.abc import Generator, Sequence
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any, TypeVar
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from zwave_js_server.client import Client as ZwaveClient
|
from zwave_js_server.client import Client as ZwaveClient
|
||||||
from zwave_js_server.const import SET_VALUE_SUCCESS, CommandClass, CommandStatus
|
from zwave_js_server.const import SET_VALUE_SUCCESS, CommandClass, CommandStatus
|
||||||
|
from zwave_js_server.const.command_class.notification import NotificationType
|
||||||
from zwave_js_server.exceptions import FailedZWaveCommand, SetValueFailed
|
from zwave_js_server.exceptions import FailedZWaveCommand, SetValueFailed
|
||||||
from zwave_js_server.model.endpoint import Endpoint
|
from zwave_js_server.model.endpoint import Endpoint
|
||||||
from zwave_js_server.model.node import Node as ZwaveNode
|
from zwave_js_server.model.node import Node as ZwaveNode
|
||||||
@ -39,6 +40,8 @@ from .helpers import (
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
T = TypeVar("T", ZwaveNode, Endpoint)
|
||||||
|
|
||||||
|
|
||||||
def parameter_name_does_not_need_bitmask(
|
def parameter_name_does_not_need_bitmask(
|
||||||
val: dict[str, int | str | list[str]]
|
val: dict[str, int | str | list[str]]
|
||||||
@ -66,8 +69,8 @@ def broadcast_command(val: dict[str, Any]) -> dict[str, Any]:
|
|||||||
|
|
||||||
|
|
||||||
def get_valid_responses_from_results(
|
def get_valid_responses_from_results(
|
||||||
zwave_objects: Sequence[ZwaveNode | Endpoint], results: Sequence[Any]
|
zwave_objects: Sequence[T], results: Sequence[Any]
|
||||||
) -> Generator[tuple[ZwaveNode | Endpoint, Any], None, None]:
|
) -> Generator[tuple[T, Any], None, None]:
|
||||||
"""Return valid responses from a list of results."""
|
"""Return valid responses from a list of results."""
|
||||||
for zwave_object, result in zip(zwave_objects, results):
|
for zwave_object, result in zip(zwave_objects, results):
|
||||||
if not isinstance(result, Exception):
|
if not isinstance(result, Exception):
|
||||||
@ -93,6 +96,49 @@ def raise_exceptions_from_results(
|
|||||||
raise HomeAssistantError("\n".join(lines))
|
raise HomeAssistantError("\n".join(lines))
|
||||||
|
|
||||||
|
|
||||||
|
async def _async_invoke_cc_api(
|
||||||
|
nodes_or_endpoints: set[T],
|
||||||
|
command_class: CommandClass,
|
||||||
|
method_name: str,
|
||||||
|
*args: Any,
|
||||||
|
) -> None:
|
||||||
|
"""Invoke the CC API on a node endpoint."""
|
||||||
|
nodes_or_endpoints_list = list(nodes_or_endpoints)
|
||||||
|
results = await asyncio.gather(
|
||||||
|
*(
|
||||||
|
node_or_endpoint.async_invoke_cc_api(command_class, method_name, *args)
|
||||||
|
for node_or_endpoint in nodes_or_endpoints_list
|
||||||
|
),
|
||||||
|
return_exceptions=True,
|
||||||
|
)
|
||||||
|
for node_or_endpoint, result in get_valid_responses_from_results(
|
||||||
|
nodes_or_endpoints_list, results
|
||||||
|
):
|
||||||
|
if isinstance(node_or_endpoint, ZwaveNode):
|
||||||
|
_LOGGER.info(
|
||||||
|
(
|
||||||
|
"Invoked %s CC API method %s on node %s with the following result: "
|
||||||
|
"%s"
|
||||||
|
),
|
||||||
|
command_class.name,
|
||||||
|
method_name,
|
||||||
|
node_or_endpoint,
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
_LOGGER.info(
|
||||||
|
(
|
||||||
|
"Invoked %s CC API method %s on endpoint %s with the following "
|
||||||
|
"result: %s"
|
||||||
|
),
|
||||||
|
command_class.name,
|
||||||
|
method_name,
|
||||||
|
node_or_endpoint,
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
raise_exceptions_from_results(nodes_or_endpoints_list, results)
|
||||||
|
|
||||||
|
|
||||||
class ZWaveServices:
|
class ZWaveServices:
|
||||||
"""Class that holds our services (Zwave Commands).
|
"""Class that holds our services (Zwave Commands).
|
||||||
|
|
||||||
@ -406,6 +452,34 @@ class ZWaveServices:
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self._hass.services.async_register(
|
||||||
|
const.DOMAIN,
|
||||||
|
const.SERVICE_REFRESH_NOTIFICATIONS,
|
||||||
|
self.async_refresh_notifications,
|
||||||
|
schema=vol.Schema(
|
||||||
|
vol.All(
|
||||||
|
{
|
||||||
|
vol.Optional(ATTR_AREA_ID): vol.All(
|
||||||
|
cv.ensure_list, [cv.string]
|
||||||
|
),
|
||||||
|
vol.Optional(ATTR_DEVICE_ID): vol.All(
|
||||||
|
cv.ensure_list, [cv.string]
|
||||||
|
),
|
||||||
|
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||||
|
vol.Required(const.ATTR_NOTIFICATION_TYPE): vol.All(
|
||||||
|
vol.Coerce(int), vol.Coerce(NotificationType)
|
||||||
|
),
|
||||||
|
vol.Optional(const.ATTR_NOTIFICATION_EVENT): vol.Coerce(int),
|
||||||
|
},
|
||||||
|
cv.has_at_least_one_key(
|
||||||
|
ATTR_DEVICE_ID, ATTR_ENTITY_ID, ATTR_AREA_ID
|
||||||
|
),
|
||||||
|
get_nodes_from_service_data,
|
||||||
|
has_at_least_one_node,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
async def async_set_config_parameter(self, service: ServiceCall) -> None:
|
async def async_set_config_parameter(self, service: ServiceCall) -> None:
|
||||||
"""Set a config value on a node."""
|
"""Set a config value on a node."""
|
||||||
nodes: set[ZwaveNode] = service.data[const.ATTR_NODES]
|
nodes: set[ZwaveNode] = service.data[const.ATTR_NODES]
|
||||||
@ -643,38 +717,14 @@ class ZWaveServices:
|
|||||||
method_name: str = service.data[const.ATTR_METHOD_NAME]
|
method_name: str = service.data[const.ATTR_METHOD_NAME]
|
||||||
parameters: list[Any] = service.data[const.ATTR_PARAMETERS]
|
parameters: list[Any] = service.data[const.ATTR_PARAMETERS]
|
||||||
|
|
||||||
async def _async_invoke_cc_api(endpoints: set[Endpoint]) -> None:
|
|
||||||
"""Invoke the CC API on a node endpoint."""
|
|
||||||
results = await asyncio.gather(
|
|
||||||
*(
|
|
||||||
endpoint.async_invoke_cc_api(
|
|
||||||
command_class, method_name, *parameters
|
|
||||||
)
|
|
||||||
for endpoint in endpoints
|
|
||||||
),
|
|
||||||
return_exceptions=True,
|
|
||||||
)
|
|
||||||
endpoints_list = list(endpoints)
|
|
||||||
for endpoint, result in get_valid_responses_from_results(
|
|
||||||
endpoints_list, results
|
|
||||||
):
|
|
||||||
_LOGGER.info(
|
|
||||||
(
|
|
||||||
"Invoked %s CC API method %s on endpoint %s with the following "
|
|
||||||
"result: %s"
|
|
||||||
),
|
|
||||||
command_class.name,
|
|
||||||
method_name,
|
|
||||||
endpoint,
|
|
||||||
result,
|
|
||||||
)
|
|
||||||
raise_exceptions_from_results(endpoints_list, results)
|
|
||||||
|
|
||||||
# If an endpoint is provided, we assume the user wants to call the CC API on
|
# If an endpoint is provided, we assume the user wants to call the CC API on
|
||||||
# that endpoint for all target nodes
|
# that endpoint for all target nodes
|
||||||
if (endpoint := service.data.get(const.ATTR_ENDPOINT)) is not None:
|
if (endpoint := service.data.get(const.ATTR_ENDPOINT)) is not None:
|
||||||
await _async_invoke_cc_api(
|
await _async_invoke_cc_api(
|
||||||
{node.endpoints[endpoint] for node in service.data[const.ATTR_NODES]}
|
{node.endpoints[endpoint] for node in service.data[const.ATTR_NODES]},
|
||||||
|
command_class,
|
||||||
|
method_name,
|
||||||
|
*parameters,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -723,4 +773,14 @@ class ZWaveServices:
|
|||||||
node.endpoints[endpoint_idx if endpoint_idx is not None else 0]
|
node.endpoints[endpoint_idx if endpoint_idx is not None else 0]
|
||||||
)
|
)
|
||||||
|
|
||||||
await _async_invoke_cc_api(endpoints)
|
await _async_invoke_cc_api(endpoints, command_class, method_name, *parameters)
|
||||||
|
|
||||||
|
async def async_refresh_notifications(self, service: ServiceCall) -> None:
|
||||||
|
"""Refresh notifications on a node."""
|
||||||
|
nodes: set[ZwaveNode] = service.data[const.ATTR_NODES]
|
||||||
|
notification_type: NotificationType = service.data[const.ATTR_NOTIFICATION_TYPE]
|
||||||
|
notification_event: int | None = service.data.get(const.ATTR_NOTIFICATION_EVENT)
|
||||||
|
param: dict[str, int] = {"notificationType": notification_type.value}
|
||||||
|
if notification_event is not None:
|
||||||
|
param["notificationEvent"] = notification_event
|
||||||
|
await _async_invoke_cc_api(nodes, CommandClass.NOTIFICATION, "get", param)
|
||||||
|
@ -223,3 +223,25 @@ invoke_cc_api:
|
|||||||
required: true
|
required: true
|
||||||
selector:
|
selector:
|
||||||
object:
|
object:
|
||||||
|
|
||||||
|
refresh_notifications:
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
integration: zwave_js
|
||||||
|
fields:
|
||||||
|
notification_type:
|
||||||
|
example: 1
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 1
|
||||||
|
max: 22
|
||||||
|
mode: box
|
||||||
|
notification_event:
|
||||||
|
example: 1
|
||||||
|
required: false
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 1
|
||||||
|
max: 255
|
||||||
|
mode: box
|
||||||
|
@ -363,6 +363,20 @@
|
|||||||
"description": "A list of parameters to pass to the API method. Refer to the Z-Wave JS Command Class API documentation (https://zwave-js.github.io/node-zwave-js/#/api/CCs/index) for parameters."
|
"description": "A list of parameters to pass to the API method. Refer to the Z-Wave JS Command Class API documentation (https://zwave-js.github.io/node-zwave-js/#/api/CCs/index) for parameters."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"refresh_notifications": {
|
||||||
|
"name": "Refresh notifications on a node (advanced)",
|
||||||
|
"description": "Refreshes notifications on a node based on notification type and optionally notification event.",
|
||||||
|
"fields": {
|
||||||
|
"notification_type": {
|
||||||
|
"name": "Notification Type",
|
||||||
|
"description": "The Notification Type number as defined in the Z-Wave specs."
|
||||||
|
},
|
||||||
|
"notification_event": {
|
||||||
|
"name": "Notification Event",
|
||||||
|
"description": "The Notification Event number as defined in the Z-Wave specs."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -62,7 +62,14 @@
|
|||||||
"index": 0,
|
"index": 0,
|
||||||
"installerIcon": 3079,
|
"installerIcon": 3079,
|
||||||
"userIcon": 3079,
|
"userIcon": 3079,
|
||||||
"commandClasses": []
|
"commandClasses": [
|
||||||
|
{
|
||||||
|
"id": 113,
|
||||||
|
"name": "Notification",
|
||||||
|
"version": 8,
|
||||||
|
"isSecure": false
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"values": [
|
"values": [
|
||||||
|
@ -14,6 +14,8 @@ from homeassistant.components.zwave_js.const import (
|
|||||||
ATTR_CONFIG_VALUE,
|
ATTR_CONFIG_VALUE,
|
||||||
ATTR_ENDPOINT,
|
ATTR_ENDPOINT,
|
||||||
ATTR_METHOD_NAME,
|
ATTR_METHOD_NAME,
|
||||||
|
ATTR_NOTIFICATION_EVENT,
|
||||||
|
ATTR_NOTIFICATION_TYPE,
|
||||||
ATTR_OPTIONS,
|
ATTR_OPTIONS,
|
||||||
ATTR_PARAMETERS,
|
ATTR_PARAMETERS,
|
||||||
ATTR_PROPERTY,
|
ATTR_PROPERTY,
|
||||||
@ -26,6 +28,7 @@ from homeassistant.components.zwave_js.const import (
|
|||||||
SERVICE_INVOKE_CC_API,
|
SERVICE_INVOKE_CC_API,
|
||||||
SERVICE_MULTICAST_SET_VALUE,
|
SERVICE_MULTICAST_SET_VALUE,
|
||||||
SERVICE_PING,
|
SERVICE_PING,
|
||||||
|
SERVICE_REFRESH_NOTIFICATIONS,
|
||||||
SERVICE_REFRESH_VALUE,
|
SERVICE_REFRESH_VALUE,
|
||||||
SERVICE_SET_CONFIG_PARAMETER,
|
SERVICE_SET_CONFIG_PARAMETER,
|
||||||
SERVICE_SET_VALUE,
|
SERVICE_SET_VALUE,
|
||||||
@ -1777,3 +1780,97 @@ async def test_invoke_cc_api(
|
|||||||
|
|
||||||
client.async_send_command.reset_mock()
|
client.async_send_command.reset_mock()
|
||||||
client.async_send_command_no_wait.reset_mock()
|
client.async_send_command_no_wait.reset_mock()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_refresh_notifications(
|
||||||
|
hass: HomeAssistant, client, zen_31, multisensor_6, integration
|
||||||
|
) -> None:
|
||||||
|
"""Test refresh_notifications service."""
|
||||||
|
dev_reg = async_get_dev_reg(hass)
|
||||||
|
zen_31_device = dev_reg.async_get_device(
|
||||||
|
identifiers={get_device_id(client.driver, zen_31)}
|
||||||
|
)
|
||||||
|
assert zen_31_device
|
||||||
|
multisensor_6_device = dev_reg.async_get_device(
|
||||||
|
identifiers={get_device_id(client.driver, multisensor_6)}
|
||||||
|
)
|
||||||
|
assert multisensor_6_device
|
||||||
|
|
||||||
|
area_reg = async_get_area_reg(hass)
|
||||||
|
area = area_reg.async_get_or_create("test")
|
||||||
|
dev_reg.async_update_device(zen_31_device.id, area_id=area.id)
|
||||||
|
|
||||||
|
# Test successful refresh_notifications call
|
||||||
|
client.async_send_command.return_value = {"response": True}
|
||||||
|
client.async_send_command_no_wait.return_value = {"response": True}
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_REFRESH_NOTIFICATIONS,
|
||||||
|
{
|
||||||
|
ATTR_AREA_ID: area.id,
|
||||||
|
ATTR_DEVICE_ID: [zen_31_device.id, multisensor_6_device.id],
|
||||||
|
ATTR_NOTIFICATION_TYPE: 1,
|
||||||
|
ATTR_NOTIFICATION_EVENT: 2,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(client.async_send_command.call_args_list) == 1
|
||||||
|
args = client.async_send_command.call_args[0][0]
|
||||||
|
assert args["command"] == "endpoint.invoke_cc_api"
|
||||||
|
assert args["commandClass"] == 113
|
||||||
|
assert args["endpoint"] == 0
|
||||||
|
assert args["methodName"] == "get"
|
||||||
|
assert args["args"] == [{"notificationType": 1, "notificationEvent": 2}]
|
||||||
|
assert args["nodeId"] == zen_31.node_id
|
||||||
|
|
||||||
|
assert len(client.async_send_command_no_wait.call_args_list) == 1
|
||||||
|
args = client.async_send_command_no_wait.call_args[0][0]
|
||||||
|
assert args["command"] == "endpoint.invoke_cc_api"
|
||||||
|
assert args["commandClass"] == 113
|
||||||
|
assert args["endpoint"] == 0
|
||||||
|
assert args["methodName"] == "get"
|
||||||
|
assert args["args"] == [{"notificationType": 1, "notificationEvent": 2}]
|
||||||
|
assert args["nodeId"] == multisensor_6.node_id
|
||||||
|
|
||||||
|
client.async_send_command.reset_mock()
|
||||||
|
client.async_send_command_no_wait.reset_mock()
|
||||||
|
|
||||||
|
# Test failed refresh_notifications call on one node. We return the error on
|
||||||
|
# the first node in the call to make sure that gather works as expected
|
||||||
|
client.async_send_command.return_value = {"response": True}
|
||||||
|
client.async_send_command_no_wait.side_effect = FailedZWaveCommand(
|
||||||
|
"test", 12, "test"
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(HomeAssistantError):
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_REFRESH_NOTIFICATIONS,
|
||||||
|
{
|
||||||
|
ATTR_DEVICE_ID: [multisensor_6_device.id, zen_31_device.id],
|
||||||
|
ATTR_NOTIFICATION_TYPE: 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"] == "endpoint.invoke_cc_api"
|
||||||
|
assert args["commandClass"] == 113
|
||||||
|
assert args["endpoint"] == 0
|
||||||
|
assert args["methodName"] == "get"
|
||||||
|
assert args["args"] == [{"notificationType": 1}]
|
||||||
|
assert args["nodeId"] == zen_31.node_id
|
||||||
|
|
||||||
|
assert len(client.async_send_command_no_wait.call_args_list) == 1
|
||||||
|
args = client.async_send_command_no_wait.call_args[0][0]
|
||||||
|
assert args["command"] == "endpoint.invoke_cc_api"
|
||||||
|
assert args["commandClass"] == 113
|
||||||
|
assert args["endpoint"] == 0
|
||||||
|
assert args["methodName"] == "get"
|
||||||
|
assert args["args"] == [{"notificationType": 1}]
|
||||||
|
assert args["nodeId"] == multisensor_6.node_id
|
||||||
|
|
||||||
|
client.async_send_command.reset_mock()
|
||||||
|
client.async_send_command_no_wait.reset_mock()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user