Skip to content

Commit

Permalink
Add zwave_js.refresh_notifications service (#101370)
Browse files Browse the repository at this point in the history
  • Loading branch information
raman325 committed Nov 7, 2023
1 parent 21af563 commit 0fcaa2c
Show file tree
Hide file tree
Showing 6 changed files with 236 additions and 33 deletions.
3 changes: 3 additions & 0 deletions homeassistant/components/zwave_js/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@
ATTR_ACKNOWLEDGED_FRAMES = "acknowledged_frames"
ATTR_EVENT_TYPE_LABEL = "event_type_label"
ATTR_DATA_TYPE_LABEL = "data_type_label"
ATTR_NOTIFICATION_TYPE = "notification_type"
ATTR_NOTIFICATION_EVENT = "notification_event"

ATTR_NODE = "node"
ATTR_ZWAVE_VALUE = "zwave_value"
Expand All @@ -92,6 +94,7 @@
SERVICE_INVOKE_CC_API = "invoke_cc_api"
SERVICE_MULTICAST_SET_VALUE = "multicast_set_value"
SERVICE_PING = "ping"
SERVICE_REFRESH_NOTIFICATIONS = "refresh_notifications"
SERVICE_REFRESH_VALUE = "refresh_value"
SERVICE_RESET_METER = "reset_meter"
SERVICE_SET_CONFIG_PARAMETER = "set_config_parameter"
Expand Down
124 changes: 92 additions & 32 deletions homeassistant/components/zwave_js/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
import asyncio
from collections.abc import Generator, Sequence
import logging
from typing import Any
from typing import Any, TypeVar

import voluptuous as vol
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.command_class.notification import NotificationType
from zwave_js_server.exceptions import FailedZWaveCommand, SetValueFailed
from zwave_js_server.model.endpoint import Endpoint
from zwave_js_server.model.node import Node as ZwaveNode
Expand Down Expand Up @@ -39,6 +40,8 @@

_LOGGER = logging.getLogger(__name__)

T = TypeVar("T", ZwaveNode, Endpoint)


def parameter_name_does_not_need_bitmask(
val: dict[str, int | str | list[str]]
Expand Down Expand Up @@ -66,8 +69,8 @@ def broadcast_command(val: dict[str, Any]) -> dict[str, Any]:


def get_valid_responses_from_results(
zwave_objects: Sequence[ZwaveNode | Endpoint], results: Sequence[Any]
) -> Generator[tuple[ZwaveNode | Endpoint, Any], None, None]:
zwave_objects: Sequence[T], results: Sequence[Any]
) -> Generator[tuple[T, Any], None, None]:
"""Return valid responses from a list of results."""
for zwave_object, result in zip(zwave_objects, results):
if not isinstance(result, Exception):
Expand All @@ -93,6 +96,49 @@ def raise_exceptions_from_results(
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 that holds our services (Zwave Commands).
Expand Down Expand Up @@ -406,6 +452,34 @@ def validate_entities(val: dict[str, Any]) -> dict[str, Any]:
),
)

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:
"""Set a config value on a node."""
nodes: set[ZwaveNode] = service.data[const.ATTR_NODES]
Expand Down Expand Up @@ -643,38 +717,14 @@ async def async_invoke_cc_api(self, service: ServiceCall) -> None:
method_name: str = service.data[const.ATTR_METHOD_NAME]
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
# that endpoint for all target nodes
if (endpoint := service.data.get(const.ATTR_ENDPOINT)) is not None:
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

Expand Down Expand Up @@ -723,4 +773,14 @@ async def _async_invoke_cc_api(endpoints: set[Endpoint]) -> None:
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)
22 changes: 22 additions & 0 deletions homeassistant/components/zwave_js/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -223,3 +223,25 @@ invoke_cc_api:
required: true
selector:
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
14 changes: 14 additions & 0 deletions homeassistant/components/zwave_js/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
}
},
"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."
}
}
}
}
}
9 changes: 8 additions & 1 deletion tests/components/zwave_js/fixtures/multisensor_6_state.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,14 @@
"index": 0,
"installerIcon": 3079,
"userIcon": 3079,
"commandClasses": []
"commandClasses": [
{
"id": 113,
"name": "Notification",
"version": 8,
"isSecure": false
}
]
}
],
"values": [
Expand Down
97 changes: 97 additions & 0 deletions tests/components/zwave_js/test_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
ATTR_CONFIG_VALUE,
ATTR_ENDPOINT,
ATTR_METHOD_NAME,
ATTR_NOTIFICATION_EVENT,
ATTR_NOTIFICATION_TYPE,
ATTR_OPTIONS,
ATTR_PARAMETERS,
ATTR_PROPERTY,
Expand All @@ -26,6 +28,7 @@
SERVICE_INVOKE_CC_API,
SERVICE_MULTICAST_SET_VALUE,
SERVICE_PING,
SERVICE_REFRESH_NOTIFICATIONS,
SERVICE_REFRESH_VALUE,
SERVICE_SET_CONFIG_PARAMETER,
SERVICE_SET_VALUE,
Expand Down Expand Up @@ -1777,3 +1780,97 @@ async def test_invoke_cc_api(

client.async_send_command.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()

0 comments on commit 0fcaa2c

Please sign in to comment.