diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index d7717922d10..d73f05c4c47 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -62,4 +62,6 @@ SERVICE_MULTICAST_SET_VALUE = "multicast_set_value" ATTR_BROADCAST = "broadcast" +SERVICE_PING = "ping" + ADDON_SLUG = "core_zwave_js" diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py index 65184abbd08..930f002cf1e 100644 --- a/homeassistant/components/zwave_js/services.py +++ b/homeassistant/components/zwave_js/services.py @@ -1,6 +1,7 @@ """Methods and classes related to executing Z-Wave commands and publishing these to hass.""" from __future__ import annotations +import asyncio import logging from typing import Any @@ -294,6 +295,24 @@ class ZWaveServices: ), ) + self._hass.services.async_register( + const.DOMAIN, + const.SERVICE_PING, + self.async_ping, + schema=vol.Schema( + vol.All( + { + vol.Optional(ATTR_DEVICE_ID): vol.All( + cv.ensure_list, [cv.string] + ), + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + }, + cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), + get_nodes_from_service_data, + ), + ), + ) + async def async_set_config_parameter(self, service: ServiceCall) -> None: """Set a config value on a node.""" nodes = service.data[const.ATTR_NODES] @@ -418,3 +437,8 @@ class ZWaveServices: if success is False: raise SetValueFailed("Unable to set value via multicast") + + async def async_ping(self, service: ServiceCall) -> None: + """Ping node(s).""" + nodes: set[ZwaveNode] = service.data[const.ATTR_NODES] + await asyncio.gather(*[node.async_ping() for node in nodes]) diff --git a/homeassistant/components/zwave_js/services.yaml b/homeassistant/components/zwave_js/services.yaml index 16be02b7f1b..c24fa4694cf 100644 --- a/homeassistant/components/zwave_js/services.yaml +++ b/homeassistant/components/zwave_js/services.yaml @@ -210,3 +210,10 @@ multicast_set_value: required: true selector: object: + +ping: + name: Ping a node + description: Forces Z-Wave JS to try to reach a node. This can be used to update the status of the node in Z-Wave JS when you think it doesn't accurately reflect reality, e.g. reviving a failed/dead node or marking the node as asleep. + target: + entity: + integration: zwave_js diff --git a/tests/components/zwave_js/test_services.py b/tests/components/zwave_js/test_services.py index 4f70543d3e2..a7aea70f6a7 100644 --- a/tests/components/zwave_js/test_services.py +++ b/tests/components/zwave_js/test_services.py @@ -18,6 +18,7 @@ from homeassistant.components.zwave_js.const import ( DOMAIN, SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS, SERVICE_MULTICAST_SET_VALUE, + SERVICE_PING, SERVICE_REFRESH_VALUE, SERVICE_SET_CONFIG_PARAMETER, SERVICE_SET_VALUE, @@ -790,3 +791,43 @@ async def test_multicast_set_value( }, blocking=True, ) + + +async def test_ping( + hass, + client, + climate_danfoss_lc_13, + climate_radio_thermostat_ct100_plus_different_endpoints, + integration, +): + """Test ping service.""" + client.async_send_command.return_value = {"responded": True} + + # Test successful ping call + await hass.services.async_call( + DOMAIN, + SERVICE_PING, + { + ATTR_ENTITY_ID: [ + CLIMATE_DANFOSS_LC13_ENTITY, + CLIMATE_RADIO_THERMOSTAT_ENTITY, + ], + }, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 2 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.ping" + assert args["nodeId"] == climate_danfoss_lc_13.node_id + + client.async_send_command.reset_mock() + + # Test no device or entity raises error + with pytest.raises(vol.Invalid): + await hass.services.async_call( + DOMAIN, + SERVICE_PING, + {}, + blocking=True, + )