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 testpull/46979/head
parent
228096847b
commit
1a99562e91
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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],
|
||||
)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue