Add zwave_js.refresh_value service (#46944)

* add poll_value service

* switch vol.All to vol.Schema

* more relevant log message

* switch service name to refresh_value, add parameter to refresh all watched values, fix tests

* rename parameter and create task for polling command so we don't wait for a response

* raise ValueError for unknown entity

* better error message

* fix test
pull/46979/head
Raman Gupta 2021-02-23 18:58:04 -05:00 committed by GitHub
parent 228096847b
commit 1a99562e91
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 168 additions and 7 deletions

View File

@ -15,7 +15,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_DOMAIN, CONF_URL, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry
from homeassistant.helpers import device_registry, entity_registry
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.dispatcher import async_dispatcher_send
@ -193,7 +193,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
DATA_UNSUBSCRIBE: unsubscribe_callbacks,
}
services = ZWaveServices(hass)
services = ZWaveServices(hass, entity_registry.async_get(hass))
services.async_register()
# Set up websocket API

View File

@ -40,4 +40,8 @@ ATTR_CONFIG_PARAMETER = "parameter"
ATTR_CONFIG_PARAMETER_BITMASK = "bitmask"
ATTR_CONFIG_VALUE = "value"
SERVICE_REFRESH_VALUE = "refresh_value"
ATTR_REFRESH_ALL_VALUES = "refresh_all_values"
ADDON_SLUG = "core_zwave_js"

View File

@ -8,8 +8,10 @@ from zwave_js_server.model.value import Value as ZwaveValue, get_value_id
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
from .const import DOMAIN
from .discovery import ZwaveDiscoveryInfo
from .helpers import get_device_id
@ -39,6 +41,35 @@ class ZWaveBaseEntity(Entity):
To be overridden by platforms needing this event.
"""
async def async_poll_value(self, refresh_all_values: bool) -> None:
"""Poll a value."""
assert self.hass
if not refresh_all_values:
self.hass.async_create_task(
self.info.node.async_poll_value(self.info.primary_value)
)
LOGGER.info(
(
"Refreshing primary value %s for %s, "
"state update may be delayed for devices on battery"
),
self.info.primary_value,
self.entity_id,
)
return
for value_id in self.watched_value_ids:
self.hass.async_create_task(self.info.node.async_poll_value(value_id))
LOGGER.info(
(
"Refreshing values %s for %s, state update may be delayed for "
"devices on battery"
),
", ".join(self.watched_value_ids),
self.entity_id,
)
async def async_added_to_hass(self) -> None:
"""Call when entity is added."""
assert self.hass # typing
@ -46,6 +77,13 @@ class ZWaveBaseEntity(Entity):
self.async_on_remove(
self.info.node.on(EVENT_VALUE_UPDATED, self._value_changed)
)
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{DOMAIN}_{self.unique_id}_poll_value",
self.async_poll_value,
)
)
@property
def device_info(self) -> dict:

View File

@ -10,6 +10,8 @@ from zwave_js_server.util.node import async_set_config_parameter
from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant, ServiceCall, callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.entity_registry import EntityRegistry
from . import const
from .helpers import async_get_node_from_device_id, async_get_node_from_entity_id
@ -41,9 +43,10 @@ BITMASK_SCHEMA = vol.All(
class ZWaveServices:
"""Class that holds our services (Zwave Commands) that should be published to hass."""
def __init__(self, hass: HomeAssistant):
def __init__(self, hass: HomeAssistant, ent_reg: EntityRegistry):
"""Initialize with hass object."""
self._hass = hass
self._ent_reg = ent_reg
@callback
def async_register(self) -> None:
@ -71,6 +74,18 @@ class ZWaveServices:
),
)
self._hass.services.async_register(
const.DOMAIN,
const.SERVICE_REFRESH_VALUE,
self.async_poll_value,
schema=vol.Schema(
{
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
vol.Optional(const.ATTR_REFRESH_ALL_VALUES, default=False): bool,
}
),
)
async def async_set_config_parameter(self, service: ServiceCall) -> None:
"""Set a config value on a node."""
nodes: Set[ZwaveNode] = set()
@ -108,3 +123,17 @@ class ZWaveServices:
f"Unable to set configuration parameter on Node {node} with "
f"value {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]:
entry = self._ent_reg.async_get(entity_id)
if entry is None or entry.platform != const.DOMAIN:
raise ValueError(
f"Entity {entity_id} is not a valid {const.DOMAIN} entity."
)
async_dispatcher_send(
self._hass,
f"{const.DOMAIN}_{entry.unique_id}_poll_value",
service.data[const.ATTR_REFRESH_ALL_VALUES],
)

View File

@ -66,3 +66,19 @@ set_config_parameter:
advanced: true
selector:
object:
refresh_value:
name: Refresh value(s) of a Z-Wave entity
description: Force update value(s) for a Z-Wave entity
target:
entity:
integration: zwave_js
fields:
refresh_all_values:
name: Refresh all values?
description: Whether to refresh all values (true) or just the primary value (false)
required: false
example: true
default: false
selector:
boolean:

View File

@ -13,3 +13,6 @@ NOTIFICATION_MOTION_SENSOR = "sensor.multisensor_6_home_security_motion_sensor_s
PROPERTY_DOOR_STATUS_BINARY_SENSOR = (
"binary_sensor.august_smart_lock_pro_3rd_gen_the_current_status_of_the_door"
)
CLIMATE_RADIO_THERMOSTAT_ENTITY = "climate.z_wave_thermostat"
CLIMATE_DANFOSS_LC13_ENTITY = "climate.living_connect_z_thermostat"
CLIMATE_FLOOR_THERMOSTAT_ENTITY = "climate.floor_thermostat"

View File

@ -28,9 +28,11 @@ from homeassistant.components.climate.const import (
from homeassistant.components.zwave_js.climate import ATTR_FAN_STATE
from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE
CLIMATE_RADIO_THERMOSTAT_ENTITY = "climate.z_wave_thermostat"
CLIMATE_DANFOSS_LC13_ENTITY = "climate.living_connect_z_thermostat"
CLIMATE_FLOOR_THERMOSTAT_ENTITY = "climate.floor_thermostat"
from .common import (
CLIMATE_DANFOSS_LC13_ENTITY,
CLIMATE_FLOOR_THERMOSTAT_ENTITY,
CLIMATE_RADIO_THERMOSTAT_ENTITY,
)
async def test_thermostat_v2(

View File

@ -6,14 +6,16 @@ from homeassistant.components.zwave_js.const import (
ATTR_CONFIG_PARAMETER,
ATTR_CONFIG_PARAMETER_BITMASK,
ATTR_CONFIG_VALUE,
ATTR_REFRESH_ALL_VALUES,
DOMAIN,
SERVICE_REFRESH_VALUE,
SERVICE_SET_CONFIG_PARAMETER,
)
from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID
from homeassistant.helpers.device_registry import async_get as async_get_dev_reg
from homeassistant.helpers.entity_registry import async_get as async_get_ent_reg
from .common import AIR_TEMPERATURE_SENSOR
from .common import AIR_TEMPERATURE_SENSOR, CLIMATE_RADIO_THERMOSTAT_ENTITY
from tests.common import MockConfigEntry
@ -293,3 +295,70 @@ async def test_set_config_parameter(hass, client, multisensor_6, integration):
},
blocking=True,
)
async def test_poll_value(
hass, client, climate_radio_thermostat_ct100_plus_different_endpoints, integration
):
"""Test the poll_value service."""
# Test polling the primary value
client.async_send_command.return_value = {"result": 2}
await hass.services.async_call(
DOMAIN,
SERVICE_REFRESH_VALUE,
{ATTR_ENTITY_ID: CLIMATE_RADIO_THERMOSTAT_ENTITY},
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.poll_value"
assert args["nodeId"] == 26
assert args["valueId"] == {
"commandClassName": "Thermostat Mode",
"commandClass": 64,
"endpoint": 0,
"property": "mode",
"propertyName": "mode",
"metadata": {
"type": "number",
"readable": True,
"writeable": True,
"min": 0,
"max": 31,
"label": "Thermostat mode",
"states": {
"0": "Off",
"1": "Heat",
"2": "Cool",
"3": "Auto",
"11": "Energy heat",
"12": "Energy cool",
},
},
"value": 1,
"ccVersion": 2,
}
client.async_send_command.reset_mock()
# Test polling all watched values
client.async_send_command.return_value = {"result": 2}
await hass.services.async_call(
DOMAIN,
SERVICE_REFRESH_VALUE,
{
ATTR_ENTITY_ID: CLIMATE_RADIO_THERMOSTAT_ENTITY,
ATTR_REFRESH_ALL_VALUES: True,
},
blocking=True,
)
assert len(client.async_send_command.call_args_list) == 8
# Test polling against an invalid entity raises ValueError
with pytest.raises(ValueError):
await hass.services.async_call(
DOMAIN,
SERVICE_REFRESH_VALUE,
{ATTR_ENTITY_ID: "sensor.fake_entity_id"},
blocking=True,
)