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
This commit is contained in:
parent
228096847b
commit
1a99562e91
8 changed files with 168 additions and 7 deletions
|
@ -15,7 +15,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import ATTR_DOMAIN, CONF_URL, EVENT_HOMEASSISTANT_STOP
|
from homeassistant.const import ATTR_DOMAIN, CONF_URL, EVENT_HOMEASSISTANT_STOP
|
||||||
from homeassistant.core import Event, HomeAssistant, callback
|
from homeassistant.core import Event, HomeAssistant, callback
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
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.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
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,
|
DATA_UNSUBSCRIBE: unsubscribe_callbacks,
|
||||||
}
|
}
|
||||||
|
|
||||||
services = ZWaveServices(hass)
|
services = ZWaveServices(hass, entity_registry.async_get(hass))
|
||||||
services.async_register()
|
services.async_register()
|
||||||
|
|
||||||
# Set up websocket API
|
# Set up websocket API
|
||||||
|
|
|
@ -40,4 +40,8 @@ ATTR_CONFIG_PARAMETER = "parameter"
|
||||||
ATTR_CONFIG_PARAMETER_BITMASK = "bitmask"
|
ATTR_CONFIG_PARAMETER_BITMASK = "bitmask"
|
||||||
ATTR_CONFIG_VALUE = "value"
|
ATTR_CONFIG_VALUE = "value"
|
||||||
|
|
||||||
|
SERVICE_REFRESH_VALUE = "refresh_value"
|
||||||
|
|
||||||
|
ATTR_REFRESH_ALL_VALUES = "refresh_all_values"
|
||||||
|
|
||||||
ADDON_SLUG = "core_zwave_js"
|
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.config_entries import ConfigEntry
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
from .discovery import ZwaveDiscoveryInfo
|
from .discovery import ZwaveDiscoveryInfo
|
||||||
from .helpers import get_device_id
|
from .helpers import get_device_id
|
||||||
|
|
||||||
|
@ -39,6 +41,35 @@ class ZWaveBaseEntity(Entity):
|
||||||
To be overridden by platforms needing this event.
|
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:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""Call when entity is added."""
|
"""Call when entity is added."""
|
||||||
assert self.hass # typing
|
assert self.hass # typing
|
||||||
|
@ -46,6 +77,13 @@ class ZWaveBaseEntity(Entity):
|
||||||
self.async_on_remove(
|
self.async_on_remove(
|
||||||
self.info.node.on(EVENT_VALUE_UPDATED, self._value_changed)
|
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
|
@property
|
||||||
def device_info(self) -> dict:
|
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.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID
|
||||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||||
import homeassistant.helpers.config_validation as cv
|
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 . import const
|
||||||
from .helpers import async_get_node_from_device_id, async_get_node_from_entity_id
|
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 ZWaveServices:
|
||||||
"""Class that holds our services (Zwave Commands) that should be published to hass."""
|
"""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."""
|
"""Initialize with hass object."""
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
|
self._ent_reg = ent_reg
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_register(self) -> None:
|
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:
|
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] = set()
|
nodes: Set[ZwaveNode] = set()
|
||||||
|
@ -108,3 +123,17 @@ class ZWaveServices:
|
||||||
f"Unable to set configuration parameter on Node {node} with "
|
f"Unable to set configuration parameter on Node {node} with "
|
||||||
f"value {new_value}"
|
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
|
advanced: true
|
||||||
selector:
|
selector:
|
||||||
object:
|
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 = (
|
PROPERTY_DOOR_STATUS_BINARY_SENSOR = (
|
||||||
"binary_sensor.august_smart_lock_pro_3rd_gen_the_current_status_of_the_door"
|
"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.components.zwave_js.climate import ATTR_FAN_STATE
|
||||||
from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE
|
from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE
|
||||||
|
|
||||||
CLIMATE_RADIO_THERMOSTAT_ENTITY = "climate.z_wave_thermostat"
|
from .common import (
|
||||||
CLIMATE_DANFOSS_LC13_ENTITY = "climate.living_connect_z_thermostat"
|
CLIMATE_DANFOSS_LC13_ENTITY,
|
||||||
CLIMATE_FLOOR_THERMOSTAT_ENTITY = "climate.floor_thermostat"
|
CLIMATE_FLOOR_THERMOSTAT_ENTITY,
|
||||||
|
CLIMATE_RADIO_THERMOSTAT_ENTITY,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_thermostat_v2(
|
async def test_thermostat_v2(
|
||||||
|
|
|
@ -6,14 +6,16 @@ from homeassistant.components.zwave_js.const import (
|
||||||
ATTR_CONFIG_PARAMETER,
|
ATTR_CONFIG_PARAMETER,
|
||||||
ATTR_CONFIG_PARAMETER_BITMASK,
|
ATTR_CONFIG_PARAMETER_BITMASK,
|
||||||
ATTR_CONFIG_VALUE,
|
ATTR_CONFIG_VALUE,
|
||||||
|
ATTR_REFRESH_ALL_VALUES,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
SERVICE_REFRESH_VALUE,
|
||||||
SERVICE_SET_CONFIG_PARAMETER,
|
SERVICE_SET_CONFIG_PARAMETER,
|
||||||
)
|
)
|
||||||
from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID
|
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.device_registry import async_get as async_get_dev_reg
|
||||||
from homeassistant.helpers.entity_registry import async_get as async_get_ent_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
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
@ -293,3 +295,70 @@ async def test_set_config_parameter(hass, client, multisensor_6, integration):
|
||||||
},
|
},
|
||||||
blocking=True,
|
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…
Add table
Add a link
Reference in a new issue