Home Assistant Unofficial Reference 2024.12.1
device_action.py
Go to the documentation of this file.
1 """Provides device actions for Z-Wave JS."""
2 
3 from __future__ import annotations
4 
5 from collections import defaultdict
6 import re
7 from typing import Any
8 
9 import voluptuous as vol
10 from zwave_js_server.const import CommandClass
11 from zwave_js_server.const.command_class.lock import ATTR_CODE_SLOT, ATTR_USERCODE
12 from zwave_js_server.const.command_class.meter import CC_SPECIFIC_METER_TYPE
13 from zwave_js_server.model.value import get_value_id_str
14 from zwave_js_server.util.command_class.meter import get_meter_type
15 
16 from homeassistant.components.device_automation import async_validate_entity_schema
17 from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN
18 from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
19 from homeassistant.const import (
20  ATTR_DEVICE_ID,
21  ATTR_DOMAIN,
22  CONF_DEVICE_ID,
23  CONF_DOMAIN,
24  CONF_ENTITY_ID,
25  CONF_TYPE,
26  STATE_UNAVAILABLE,
27 )
28 from homeassistant.core import Context, HomeAssistant
29 from homeassistant.exceptions import HomeAssistantError
30 from homeassistant.helpers import config_validation as cv, entity_registry as er
31 from homeassistant.helpers.typing import ConfigType, TemplateVarsType
32 
33 from .config_validation import VALUE_SCHEMA
34 from .const import (
35  ATTR_COMMAND_CLASS,
36  ATTR_CONFIG_PARAMETER,
37  ATTR_CONFIG_PARAMETER_BITMASK,
38  ATTR_ENDPOINT,
39  ATTR_METER_TYPE,
40  ATTR_PROPERTY,
41  ATTR_PROPERTY_KEY,
42  ATTR_REFRESH_ALL_VALUES,
43  ATTR_VALUE,
44  ATTR_WAIT_FOR_RESULT,
45  DOMAIN,
46  SERVICE_CLEAR_LOCK_USERCODE,
47  SERVICE_PING,
48  SERVICE_REFRESH_VALUE,
49  SERVICE_RESET_METER,
50  SERVICE_SET_CONFIG_PARAMETER,
51  SERVICE_SET_LOCK_USERCODE,
52  SERVICE_SET_VALUE,
53 )
54 from .device_automation_helpers import (
55  CONF_SUBTYPE,
56  VALUE_ID_REGEX,
57  generate_config_parameter_subtype,
58 )
59 from .helpers import async_get_node_from_device_id, get_value_state_schema
60 
61 ACTION_TYPES = {
62  SERVICE_CLEAR_LOCK_USERCODE,
63  SERVICE_PING,
64  SERVICE_REFRESH_VALUE,
65  SERVICE_RESET_METER,
66  SERVICE_SET_CONFIG_PARAMETER,
67  SERVICE_SET_LOCK_USERCODE,
68  SERVICE_SET_VALUE,
69 }
70 
71 CLEAR_LOCK_USERCODE_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend(
72  {
73  vol.Required(CONF_TYPE): SERVICE_CLEAR_LOCK_USERCODE,
74  vol.Required(CONF_ENTITY_ID): cv.entity_id_or_uuid,
75  vol.Required(ATTR_CODE_SLOT): vol.Coerce(int),
76  }
77 )
78 
79 PING_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend(
80  {
81  vol.Required(CONF_TYPE): SERVICE_PING,
82  }
83 )
84 
85 REFRESH_VALUE_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend(
86  {
87  vol.Required(CONF_TYPE): SERVICE_REFRESH_VALUE,
88  vol.Required(CONF_ENTITY_ID): cv.entity_id_or_uuid,
89  vol.Optional(ATTR_REFRESH_ALL_VALUES, default=False): cv.boolean,
90  }
91 )
92 
93 RESET_METER_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend(
94  {
95  vol.Required(CONF_TYPE): SERVICE_RESET_METER,
96  vol.Required(CONF_ENTITY_ID): cv.entity_id_or_uuid,
97  vol.Optional(ATTR_METER_TYPE): vol.Coerce(int),
98  vol.Optional(ATTR_VALUE): vol.Coerce(int),
99  }
100 )
101 
102 SET_CONFIG_PARAMETER_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend(
103  {
104  vol.Required(CONF_TYPE): SERVICE_SET_CONFIG_PARAMETER,
105  vol.Required(ATTR_ENDPOINT, default=0): vol.Coerce(int),
106  vol.Required(ATTR_CONFIG_PARAMETER): vol.Any(int, str),
107  vol.Required(ATTR_CONFIG_PARAMETER_BITMASK): vol.Any(None, int, str),
108  vol.Required(ATTR_VALUE): vol.Coerce(int),
109  vol.Required(CONF_SUBTYPE): cv.string,
110  }
111 )
112 
113 SET_LOCK_USERCODE_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend(
114  {
115  vol.Required(CONF_TYPE): SERVICE_SET_LOCK_USERCODE,
116  vol.Required(CONF_ENTITY_ID): cv.entity_id_or_uuid,
117  vol.Required(ATTR_CODE_SLOT): vol.Coerce(int),
118  vol.Required(ATTR_USERCODE): cv.string,
119  }
120 )
121 
122 SET_VALUE_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend(
123  {
124  vol.Required(CONF_TYPE): SERVICE_SET_VALUE,
125  vol.Required(ATTR_COMMAND_CLASS): vol.In([cc.value for cc in CommandClass]),
126  vol.Required(ATTR_PROPERTY): vol.Any(int, str),
127  vol.Optional(ATTR_PROPERTY_KEY): vol.Any(vol.Coerce(int), cv.string),
128  vol.Optional(ATTR_ENDPOINT): vol.Coerce(int),
129  vol.Required(ATTR_VALUE): VALUE_SCHEMA,
130  vol.Optional(ATTR_WAIT_FOR_RESULT, default=False): cv.boolean,
131  }
132 )
133 
134 _ACTION_SCHEMA = vol.Any(
135  CLEAR_LOCK_USERCODE_SCHEMA,
136  PING_SCHEMA,
137  REFRESH_VALUE_SCHEMA,
138  RESET_METER_SCHEMA,
139  SET_CONFIG_PARAMETER_SCHEMA,
140  SET_LOCK_USERCODE_SCHEMA,
141  SET_VALUE_SCHEMA,
142 )
143 
144 
146  hass: HomeAssistant, config: ConfigType
147 ) -> ConfigType:
148  """Validate config."""
149  return async_validate_entity_schema(hass, config, _ACTION_SCHEMA)
150 
151 
153  hass: HomeAssistant, device_id: str
154 ) -> list[dict[str, Any]]:
155  """List device actions for Z-Wave JS devices."""
156  registry = er.async_get(hass)
157  actions: list[dict] = []
158 
159  node = async_get_node_from_device_id(hass, device_id)
160 
161  if node.client.driver and node.client.driver.controller.own_node == node:
162  return actions
163 
164  base_action = {
165  CONF_DEVICE_ID: device_id,
166  CONF_DOMAIN: DOMAIN,
167  }
168 
169  actions.extend(
170  [
171  {**base_action, CONF_TYPE: SERVICE_SET_VALUE},
172  {**base_action, CONF_TYPE: SERVICE_PING},
173  ]
174  )
175  actions.extend(
176  [
177  {
178  **base_action,
179  CONF_TYPE: SERVICE_SET_CONFIG_PARAMETER,
180  ATTR_ENDPOINT: config_value.endpoint,
181  ATTR_CONFIG_PARAMETER: config_value.property_,
182  ATTR_CONFIG_PARAMETER_BITMASK: config_value.property_key,
183  CONF_SUBTYPE: generate_config_parameter_subtype(config_value),
184  }
185  for config_value in node.get_configuration_values().values()
186  ]
187  )
188 
189  meter_endpoints: dict[int, dict[str, Any]] = defaultdict(dict)
190 
191  for entry in er.async_entries_for_device(
192  registry, device_id, include_disabled_entities=False
193  ):
194  # If an entry is unavailable, it is possible that the underlying value
195  # is no longer valid. Additionally, if an entry is disabled, its
196  # underlying value is not being monitored by HA so we shouldn't allow
197  # actions against it.
198  if (
199  not (state := hass.states.get(entry.entity_id))
200  or state.state == STATE_UNAVAILABLE
201  ):
202  continue
203  entity_action = {**base_action, CONF_ENTITY_ID: entry.id}
204  actions.append({**entity_action, CONF_TYPE: SERVICE_REFRESH_VALUE})
205  if entry.domain == LOCK_DOMAIN:
206  actions.extend(
207  [
208  {**entity_action, CONF_TYPE: SERVICE_SET_LOCK_USERCODE},
209  {**entity_action, CONF_TYPE: SERVICE_CLEAR_LOCK_USERCODE},
210  ]
211  )
212 
213  if entry.domain == SENSOR_DOMAIN:
214  value_id = entry.unique_id.split(".")[1]
215  # If this unique ID doesn't have a value ID, we know it is the node status
216  # sensor which doesn't have any relevant actions
217  if not re.match(VALUE_ID_REGEX, value_id):
218  continue
219  value = node.values[value_id]
220  # If the value has the meterType CC specific value, we can add a reset_meter
221  # action for it
222  if CC_SPECIFIC_METER_TYPE in value.metadata.cc_specific:
223  endpoint_idx = value.endpoint or 0
224  meter_endpoints[endpoint_idx].setdefault(CONF_ENTITY_ID, entry.id)
225  meter_endpoints[endpoint_idx].setdefault(ATTR_METER_TYPE, set()).add(
226  get_meter_type(value)
227  )
228 
229  if not meter_endpoints:
230  return actions
231 
232  for endpoint, endpoint_data in meter_endpoints.items():
233  base_action[CONF_ENTITY_ID] = endpoint_data[CONF_ENTITY_ID]
234  actions.append(
235  {
236  **base_action,
237  CONF_TYPE: SERVICE_RESET_METER,
238  CONF_SUBTYPE: f"Endpoint {endpoint} (All)",
239  }
240  )
241  actions.extend(
242  {
243  **base_action,
244  CONF_TYPE: SERVICE_RESET_METER,
245  ATTR_METER_TYPE: meter_type,
246  CONF_SUBTYPE: f"Endpoint {endpoint} ({meter_type.name})",
247  }
248  for meter_type in endpoint_data[ATTR_METER_TYPE]
249  )
250 
251  return actions
252 
253 
255  hass: HomeAssistant,
256  config: ConfigType,
257  variables: TemplateVarsType,
258  context: Context | None,
259 ) -> None:
260  """Execute a device action."""
261  action_type = service = config[CONF_TYPE]
262  if action_type not in ACTION_TYPES:
263  raise HomeAssistantError(f"Unhandled action type {action_type}")
264 
265  # Don't include domain, subtype or any null/empty values in the service call
266  service_data = {
267  k: v
268  for k, v in config.items()
269  if k not in (ATTR_DOMAIN, CONF_TYPE, CONF_SUBTYPE) and v not in (None, "")
270  }
271 
272  # Entity services (including refresh value which is a fake entity service) expect
273  # just an entity ID
274  if action_type in (
275  SERVICE_REFRESH_VALUE,
276  SERVICE_SET_LOCK_USERCODE,
277  SERVICE_CLEAR_LOCK_USERCODE,
278  SERVICE_RESET_METER,
279  ):
280  service_data.pop(ATTR_DEVICE_ID)
281  await hass.services.async_call(
282  DOMAIN, service, service_data, blocking=True, context=context
283  )
284 
285 
287  hass: HomeAssistant, config: ConfigType
288 ) -> dict[str, vol.Schema]:
289  """List action capabilities."""
290  action_type = config[CONF_TYPE]
291  node = async_get_node_from_device_id(hass, config[CONF_DEVICE_ID])
292 
293  # Add additional fields to the automation action UI
294  if action_type == SERVICE_CLEAR_LOCK_USERCODE:
295  return {
296  "extra_fields": vol.Schema(
297  {
298  vol.Required(ATTR_CODE_SLOT): cv.string,
299  }
300  )
301  }
302 
303  if action_type == SERVICE_SET_LOCK_USERCODE:
304  return {
305  "extra_fields": vol.Schema(
306  {
307  vol.Required(ATTR_CODE_SLOT): cv.string,
308  vol.Required(ATTR_USERCODE): cv.string,
309  }
310  )
311  }
312 
313  if action_type == SERVICE_RESET_METER:
314  return {
315  "extra_fields": vol.Schema(
316  {
317  vol.Optional(ATTR_VALUE): cv.string,
318  }
319  )
320  }
321 
322  if action_type == SERVICE_REFRESH_VALUE:
323  return {
324  "extra_fields": vol.Schema(
325  {
326  vol.Optional(ATTR_REFRESH_ALL_VALUES): cv.boolean,
327  }
328  )
329  }
330 
331  if action_type == SERVICE_SET_VALUE:
332  return {
333  "extra_fields": vol.Schema(
334  {
335  vol.Required(ATTR_COMMAND_CLASS): vol.In(
336  {
337  CommandClass(cc.id).value: cc.name
338  for cc in sorted(
339  node.command_classes, key=lambda cc: cc.name
340  )
341  }
342  ),
343  vol.Required(ATTR_PROPERTY): cv.string,
344  vol.Optional(ATTR_PROPERTY_KEY): cv.string,
345  vol.Optional(ATTR_ENDPOINT): cv.string,
346  vol.Required(ATTR_VALUE): cv.string,
347  vol.Optional(ATTR_WAIT_FOR_RESULT): cv.boolean,
348  }
349  )
350  }
351 
352  if action_type == SERVICE_SET_CONFIG_PARAMETER:
353  value_id = get_value_id_str(
354  node,
355  CommandClass.CONFIGURATION,
356  config[ATTR_CONFIG_PARAMETER],
357  property_key=config[ATTR_CONFIG_PARAMETER_BITMASK],
358  endpoint=config[ATTR_ENDPOINT],
359  )
360  value_schema = get_value_state_schema(node.values[value_id])
361  if value_schema is None:
362  return {}
363  return {"extra_fields": vol.Schema({vol.Required(ATTR_VALUE): value_schema})}
364 
365  return {}
bool add(self, _T matcher)
Definition: match.py:185
ConfigType async_validate_entity_schema(HomeAssistant hass, ConfigType config, VolSchemaType schema)
Definition: __init__.py:344
ConfigType async_validate_action_config(HomeAssistant hass, ConfigType config)
dict[str, vol.Schema] async_get_action_capabilities(HomeAssistant hass, ConfigType config)
list[dict[str, Any]] async_get_actions(HomeAssistant hass, str device_id)
None async_call_action_from_config(HomeAssistant hass, ConfigType config, TemplateVarsType variables, Context|None context)
VolSchemaType|vol.Coerce|vol.In|None get_value_state_schema(ZwaveValue value)
Definition: helpers.py:482
ZwaveNode async_get_node_from_device_id(HomeAssistant hass, str device_id, dr.DeviceRegistry|None dev_reg=None)
Definition: helpers.py:253