From 1a99562e91a06a4523d9a4071db4d4d8d8aa7fdd Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 23 Feb 2021 18:58:04 -0500 Subject: [PATCH] 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 --- homeassistant/components/zwave_js/__init__.py | 4 +- homeassistant/components/zwave_js/const.py | 4 ++ homeassistant/components/zwave_js/entity.py | 38 ++++++++++ homeassistant/components/zwave_js/services.py | 31 +++++++- .../components/zwave_js/services.yaml | 16 +++++ tests/components/zwave_js/common.py | 3 + tests/components/zwave_js/test_climate.py | 8 ++- tests/components/zwave_js/test_services.py | 71 ++++++++++++++++++- 8 files changed, 168 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index a70716ad421..062b28cf6a9 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -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 diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index 1031a51719a..dba4e6d33a3 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -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" diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index 3141dd0caea..cb898e861e9 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -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: diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py index da60ddab666..c971891b35b 100644 --- a/homeassistant/components/zwave_js/services.py +++ b/homeassistant/components/zwave_js/services.py @@ -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], + ) diff --git a/homeassistant/components/zwave_js/services.yaml b/homeassistant/components/zwave_js/services.yaml index a5e9efd7216..8e6d907fc96 100644 --- a/homeassistant/components/zwave_js/services.yaml +++ b/homeassistant/components/zwave_js/services.yaml @@ -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: diff --git a/tests/components/zwave_js/common.py b/tests/components/zwave_js/common.py index 9c6adb100fa..ebba16136a0 100644 --- a/tests/components/zwave_js/common.py +++ b/tests/components/zwave_js/common.py @@ -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" diff --git a/tests/components/zwave_js/test_climate.py b/tests/components/zwave_js/test_climate.py index 17f4dd38144..1ccf6f82017 100644 --- a/tests/components/zwave_js/test_climate.py +++ b/tests/components/zwave_js/test_climate.py @@ -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( diff --git a/tests/components/zwave_js/test_services.py b/tests/components/zwave_js/test_services.py index b085d9e32fb..8e882b9547c 100644 --- a/tests/components/zwave_js/test_services.py +++ b/tests/components/zwave_js/test_services.py @@ -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, + )