Home Assistant Unofficial Reference 2024.12.1
device_trigger.py
Go to the documentation of this file.
1 """Provides device triggers for Z-Wave JS."""
2 
3 from __future__ import annotations
4 
5 from typing import Any
6 
7 import voluptuous as vol
8 from zwave_js_server.const import CommandClass
9 
11  DEVICE_TRIGGER_BASE_SCHEMA,
12  InvalidDeviceAutomationConfig,
13 )
15 from homeassistant.const import (
16  CONF_DEVICE_ID,
17  CONF_DOMAIN,
18  CONF_ENTITY_ID,
19  CONF_PLATFORM,
20  CONF_TYPE,
21 )
22 from homeassistant.core import CALLBACK_TYPE, HomeAssistant
23 from homeassistant.exceptions import HomeAssistantError
24 from homeassistant.helpers import (
25  config_validation as cv,
26  device_registry as dr,
27  entity_registry as er,
28 )
29 from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
30 from homeassistant.helpers.typing import ConfigType
31 
32 from . import trigger
33 from .config_validation import VALUE_SCHEMA
34 from .const import (
35  ATTR_COMMAND_CLASS,
36  ATTR_DATA_TYPE,
37  ATTR_ENDPOINT,
38  ATTR_EVENT,
39  ATTR_EVENT_LABEL,
40  ATTR_EVENT_TYPE,
41  ATTR_LABEL,
42  ATTR_PROPERTY,
43  ATTR_PROPERTY_KEY,
44  ATTR_TYPE,
45  ATTR_VALUE,
46  ATTR_VALUE_RAW,
47  DOMAIN,
48  ZWAVE_JS_NOTIFICATION_EVENT,
49  ZWAVE_JS_VALUE_NOTIFICATION_EVENT,
50 )
51 from .device_automation_helpers import (
52  CONF_SUBTYPE,
53  NODE_STATUSES,
54  async_bypass_dynamic_config_validation,
55  generate_config_parameter_subtype,
56 )
57 from .helpers import (
58  async_get_node_from_device_id,
59  async_get_node_status_sensor_entity_id,
60  check_type_schema_map,
61  copy_available_params,
62  get_value_state_schema,
63  get_zwave_value_from_config,
64  remove_keys_with_empty_values,
65 )
66 from .triggers.value_updated import (
67  ATTR_FROM,
68  ATTR_TO,
69  PLATFORM_TYPE as VALUE_UPDATED_PLATFORM_TYPE,
70 )
71 
72 # Trigger types
73 ENTRY_CONTROL_NOTIFICATION = "event.notification.entry_control"
74 NOTIFICATION_NOTIFICATION = "event.notification.notification"
75 BASIC_VALUE_NOTIFICATION = "event.value_notification.basic"
76 CENTRAL_SCENE_VALUE_NOTIFICATION = "event.value_notification.central_scene"
77 SCENE_ACTIVATION_VALUE_NOTIFICATION = "event.value_notification.scene_activation"
78 CONFIG_PARAMETER_VALUE_UPDATED = f"{VALUE_UPDATED_PLATFORM_TYPE}.config_parameter"
79 VALUE_VALUE_UPDATED = f"{VALUE_UPDATED_PLATFORM_TYPE}.value"
80 NODE_STATUS = "state.node_status"
81 
82 
83 NOTIFICATION_EVENT_CC_MAPPINGS = (
84  (ENTRY_CONTROL_NOTIFICATION, CommandClass.ENTRY_CONTROL),
85  (NOTIFICATION_NOTIFICATION, CommandClass.NOTIFICATION),
86 )
87 
88 # Event based trigger schemas
89 BASE_EVENT_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
90  {
91  vol.Required(ATTR_COMMAND_CLASS): vol.In([cc.value for cc in CommandClass]),
92  }
93 )
94 
95 NOTIFICATION_NOTIFICATION_SCHEMA = BASE_EVENT_SCHEMA.extend(
96  {
97  vol.Required(CONF_TYPE): NOTIFICATION_NOTIFICATION,
98  vol.Optional(f"{ATTR_TYPE}."): vol.Coerce(int),
99  vol.Optional(ATTR_LABEL): cv.string,
100  vol.Optional(ATTR_EVENT): vol.Coerce(int),
101  vol.Optional(ATTR_EVENT_LABEL): cv.string,
102  }
103 )
104 
105 ENTRY_CONTROL_NOTIFICATION_SCHEMA = BASE_EVENT_SCHEMA.extend(
106  {
107  vol.Required(CONF_TYPE): ENTRY_CONTROL_NOTIFICATION,
108  vol.Optional(ATTR_EVENT_TYPE): vol.Coerce(int),
109  vol.Optional(ATTR_DATA_TYPE): vol.Coerce(int),
110  }
111 )
112 
113 BASE_VALUE_NOTIFICATION_EVENT_SCHEMA = BASE_EVENT_SCHEMA.extend(
114  {
115  vol.Required(ATTR_PROPERTY): vol.Any(int, str),
116  vol.Optional(ATTR_PROPERTY_KEY): vol.Any(int, str),
117  vol.Required(ATTR_ENDPOINT): vol.Coerce(int),
118  vol.Optional(ATTR_VALUE): vol.Coerce(int),
119  vol.Required(CONF_SUBTYPE): cv.string,
120  }
121 )
122 
123 BASIC_VALUE_NOTIFICATION_SCHEMA = BASE_VALUE_NOTIFICATION_EVENT_SCHEMA.extend(
124  {
125  vol.Required(CONF_TYPE): BASIC_VALUE_NOTIFICATION,
126  }
127 )
128 
129 CENTRAL_SCENE_VALUE_NOTIFICATION_SCHEMA = BASE_VALUE_NOTIFICATION_EVENT_SCHEMA.extend(
130  {
131  vol.Required(CONF_TYPE): CENTRAL_SCENE_VALUE_NOTIFICATION,
132  }
133 )
134 
135 SCENE_ACTIVATION_VALUE_NOTIFICATION_SCHEMA = (
136  BASE_VALUE_NOTIFICATION_EVENT_SCHEMA.extend(
137  {
138  vol.Required(CONF_TYPE): SCENE_ACTIVATION_VALUE_NOTIFICATION,
139  }
140  )
141 )
142 
143 # State based trigger schemas
144 BASE_STATE_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
145  {
146  vol.Required(CONF_ENTITY_ID): cv.entity_id_or_uuid,
147  }
148 )
149 
150 NODE_STATUS_SCHEMA = BASE_STATE_SCHEMA.extend(
151  {
152  vol.Required(CONF_TYPE): NODE_STATUS,
153  vol.Optional(state.CONF_FROM): vol.In(NODE_STATUSES),
154  vol.Optional(state.CONF_TO): vol.In(NODE_STATUSES),
155  vol.Optional(state.CONF_FOR): cv.positive_time_period_dict,
156  }
157 )
158 
159 # zwave_js.value_updated based trigger schemas
160 BASE_VALUE_UPDATED_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
161  {
162  vol.Required(ATTR_COMMAND_CLASS): vol.In([cc.value for cc in CommandClass]),
163  vol.Required(ATTR_PROPERTY): vol.Any(int, str),
164  vol.Optional(ATTR_PROPERTY_KEY): vol.Any(None, vol.Coerce(int), str),
165  vol.Optional(ATTR_ENDPOINT, default=0): vol.Any(None, vol.Coerce(int)),
166  vol.Optional(ATTR_FROM): VALUE_SCHEMA,
167  vol.Optional(ATTR_TO): VALUE_SCHEMA,
168  }
169 )
170 
171 CONFIG_PARAMETER_VALUE_UPDATED_SCHEMA = BASE_VALUE_UPDATED_SCHEMA.extend(
172  {
173  vol.Required(CONF_TYPE): CONFIG_PARAMETER_VALUE_UPDATED,
174  vol.Required(CONF_SUBTYPE): cv.string,
175  }
176 )
177 
178 VALUE_VALUE_UPDATED_SCHEMA = BASE_VALUE_UPDATED_SCHEMA.extend(
179  {
180  vol.Required(CONF_TYPE): VALUE_VALUE_UPDATED,
181  }
182 )
183 
184 TYPE_SCHEMA_MAP = {
185  ENTRY_CONTROL_NOTIFICATION: ENTRY_CONTROL_NOTIFICATION_SCHEMA,
186  NOTIFICATION_NOTIFICATION: NOTIFICATION_NOTIFICATION_SCHEMA,
187  BASIC_VALUE_NOTIFICATION: BASIC_VALUE_NOTIFICATION_SCHEMA,
188  CENTRAL_SCENE_VALUE_NOTIFICATION: CENTRAL_SCENE_VALUE_NOTIFICATION_SCHEMA,
189  SCENE_ACTIVATION_VALUE_NOTIFICATION: SCENE_ACTIVATION_VALUE_NOTIFICATION_SCHEMA,
190  CONFIG_PARAMETER_VALUE_UPDATED: CONFIG_PARAMETER_VALUE_UPDATED_SCHEMA,
191  VALUE_VALUE_UPDATED: VALUE_VALUE_UPDATED_SCHEMA,
192  NODE_STATUS: NODE_STATUS_SCHEMA,
193 }
194 
195 
196 TRIGGER_TYPE_SCHEMA = vol.Schema(
197  {vol.Required(CONF_TYPE): vol.In(TYPE_SCHEMA_MAP)}, extra=vol.ALLOW_EXTRA
198 )
199 
200 TRIGGER_SCHEMA = vol.All(
201  remove_keys_with_empty_values,
202  TRIGGER_TYPE_SCHEMA,
203  check_type_schema_map(TYPE_SCHEMA_MAP),
204 )
205 
206 
208  hass: HomeAssistant, config: ConfigType
209 ) -> ConfigType:
210  """Validate config."""
211  config = TRIGGER_SCHEMA(config)
212 
213  # We return early if the config entry for this device is not ready because we can't
214  # validate the value without knowing the state of the device
215  try:
216  bypass_dynamic_config_validation = async_bypass_dynamic_config_validation(
217  hass, config[CONF_DEVICE_ID]
218  )
219  except ValueError as err:
221  f"Device {config[CONF_DEVICE_ID]} not found"
222  ) from err
223 
224  if bypass_dynamic_config_validation:
225  return config
226 
227  trigger_type = config[CONF_TYPE]
228  if get_trigger_platform_from_type(trigger_type) == VALUE_UPDATED_PLATFORM_TYPE:
229  try:
230  node = async_get_node_from_device_id(hass, config[CONF_DEVICE_ID])
231  get_zwave_value_from_config(node, config)
232  except vol.Invalid as err:
233  raise InvalidDeviceAutomationConfig(err.msg) from err
234 
235  return config
236 
237 
238 def get_trigger_platform_from_type(trigger_type: str) -> str:
239  """Get trigger platform from Z-Wave JS trigger type."""
240  trigger_split = trigger_type.split(".")
241  # Our convention for trigger types is to have the trigger type at the beginning
242  # delimited by a `.`. For zwave_js triggers, there is a `.` in the name
243  if (trigger_platform := trigger_split[0]) == DOMAIN:
244  return ".".join(trigger_split[:2])
245  return trigger_platform
246 
247 
249  hass: HomeAssistant, device_id: str
250 ) -> list[dict[str, Any]]:
251  """List device triggers for Z-Wave JS devices."""
252  triggers: list[dict] = []
253  base_trigger = {
254  CONF_PLATFORM: "device",
255  CONF_DEVICE_ID: device_id,
256  CONF_DOMAIN: DOMAIN,
257  }
258 
259  dev_reg = dr.async_get(hass)
260  node = async_get_node_from_device_id(hass, device_id, dev_reg)
261 
262  if node.client.driver and node.client.driver.controller.own_node == node:
263  return triggers
264 
265  # We can add a node status trigger if the node status sensor is enabled
266  ent_reg = er.async_get(hass)
268  hass, device_id, ent_reg, dev_reg
269  )
270  if (
271  entity_id
272  and (entity := ent_reg.async_get(entity_id)) is not None
273  and not entity.disabled
274  ):
275  triggers.append(
276  {**base_trigger, CONF_TYPE: NODE_STATUS, CONF_ENTITY_ID: entity.id}
277  )
278 
279  # Handle notification event triggers
280  triggers.extend(
281  [
282  {**base_trigger, CONF_TYPE: event_type, ATTR_COMMAND_CLASS: command_class}
283  for event_type, command_class in NOTIFICATION_EVENT_CC_MAPPINGS
284  if any(cc.id == command_class for cc in node.command_classes)
285  ]
286  )
287 
288  # Handle central scene value notification event triggers
289  triggers.extend(
290  [
291  {
292  **base_trigger,
293  CONF_TYPE: CENTRAL_SCENE_VALUE_NOTIFICATION,
294  ATTR_PROPERTY: value.property_,
295  ATTR_PROPERTY_KEY: value.property_key,
296  ATTR_ENDPOINT: value.endpoint,
297  ATTR_COMMAND_CLASS: CommandClass.CENTRAL_SCENE,
298  CONF_SUBTYPE: f"Endpoint {value.endpoint} Scene {value.property_key}",
299  }
300  for value in node.get_command_class_values(
301  CommandClass.CENTRAL_SCENE
302  ).values()
303  if value.property_ == "scene"
304  ]
305  )
306 
307  # Handle scene activation value notification event triggers
308  triggers.extend(
309  [
310  {
311  **base_trigger,
312  CONF_TYPE: SCENE_ACTIVATION_VALUE_NOTIFICATION,
313  ATTR_PROPERTY: value.property_,
314  ATTR_PROPERTY_KEY: value.property_key,
315  ATTR_ENDPOINT: value.endpoint,
316  ATTR_COMMAND_CLASS: CommandClass.SCENE_ACTIVATION,
317  CONF_SUBTYPE: f"Endpoint {value.endpoint}",
318  }
319  for value in node.get_command_class_values(
320  CommandClass.SCENE_ACTIVATION
321  ).values()
322  if value.property_ == "sceneId"
323  ]
324  )
325 
326  # Handle basic value notification event triggers
327  # Nodes will only send Basic CC value notifications if a compatibility flag is set
328  if node.device_config.compat.get("treatBasicSetAsEvent", False):
329  triggers.extend(
330  [
331  {
332  **base_trigger,
333  CONF_TYPE: BASIC_VALUE_NOTIFICATION,
334  ATTR_PROPERTY: value.property_,
335  ATTR_PROPERTY_KEY: value.property_key,
336  ATTR_ENDPOINT: value.endpoint,
337  ATTR_COMMAND_CLASS: CommandClass.BASIC,
338  CONF_SUBTYPE: f"Endpoint {value.endpoint}",
339  }
340  for value in node.get_command_class_values(CommandClass.BASIC).values()
341  if value.property_ == "event"
342  ]
343  )
344 
345  # Generic value update event trigger
346  triggers.append({**base_trigger, CONF_TYPE: VALUE_VALUE_UPDATED})
347 
348  # Config parameter value update event triggers
349  triggers.extend(
350  [
351  {
352  **base_trigger,
353  CONF_TYPE: CONFIG_PARAMETER_VALUE_UPDATED,
354  ATTR_PROPERTY: config_value.property_,
355  ATTR_PROPERTY_KEY: config_value.property_key,
356  ATTR_ENDPOINT: config_value.endpoint,
357  ATTR_COMMAND_CLASS: config_value.command_class,
358  CONF_SUBTYPE: generate_config_parameter_subtype(config_value),
359  }
360  for config_value in node.get_configuration_values().values()
361  ]
362  )
363 
364  return triggers
365 
366 
368  hass: HomeAssistant,
369  config: ConfigType,
370  action: TriggerActionType,
371  trigger_info: TriggerInfo,
372 ) -> CALLBACK_TYPE:
373  """Attach a trigger."""
374  trigger_type = config[CONF_TYPE]
375  trigger_platform = get_trigger_platform_from_type(trigger_type)
376 
377  # Take input data from automation trigger UI and add it to the trigger we are
378  # attaching to
379  if trigger_platform == "event":
380  event_data = {CONF_DEVICE_ID: config[CONF_DEVICE_ID]}
381  event_config = {
382  event.CONF_PLATFORM: "event",
383  event.CONF_EVENT_DATA: event_data,
384  }
385 
386  if ATTR_COMMAND_CLASS in config:
387  event_data[ATTR_COMMAND_CLASS] = config[ATTR_COMMAND_CLASS]
388 
389  if trigger_type == ENTRY_CONTROL_NOTIFICATION:
390  event_config[event.CONF_EVENT_TYPE] = ZWAVE_JS_NOTIFICATION_EVENT
391  copy_available_params(config, event_data, [ATTR_EVENT_TYPE, ATTR_DATA_TYPE])
392  elif trigger_type == NOTIFICATION_NOTIFICATION:
393  event_config[event.CONF_EVENT_TYPE] = ZWAVE_JS_NOTIFICATION_EVENT
395  config, event_data, [ATTR_LABEL, ATTR_EVENT_LABEL, ATTR_EVENT]
396  )
397  if (val := config.get(f"{ATTR_TYPE}.")) not in ("", None):
398  event_data[ATTR_TYPE] = val
399  elif trigger_type in (
400  BASIC_VALUE_NOTIFICATION,
401  CENTRAL_SCENE_VALUE_NOTIFICATION,
402  SCENE_ACTIVATION_VALUE_NOTIFICATION,
403  ):
404  event_config[event.CONF_EVENT_TYPE] = ZWAVE_JS_VALUE_NOTIFICATION_EVENT
406  config, event_data, [ATTR_PROPERTY, ATTR_PROPERTY_KEY, ATTR_ENDPOINT]
407  )
408  if ATTR_VALUE in config:
409  event_data[ATTR_VALUE_RAW] = config[ATTR_VALUE]
410  else:
411  raise HomeAssistantError(f"Unhandled trigger type {trigger_type}")
412 
413  event_config = event.TRIGGER_SCHEMA(event_config)
414  return await event.async_attach_trigger(
415  hass, event_config, action, trigger_info, platform_type="device"
416  )
417 
418  if trigger_platform == "state":
419  if trigger_type == NODE_STATUS:
420  state_config = {state.CONF_PLATFORM: "state"}
421 
422  state_config[state.CONF_ENTITY_ID] = config[CONF_ENTITY_ID]
424  config, state_config, [state.CONF_FOR, state.CONF_FROM, state.CONF_TO]
425  )
426  else:
427  raise HomeAssistantError(f"Unhandled trigger type {trigger_type}")
428 
429  state_config = await state.async_validate_trigger_config(hass, state_config)
430  return await state.async_attach_trigger(
431  hass, state_config, action, trigger_info, platform_type="device"
432  )
433 
434  if trigger_platform == VALUE_UPDATED_PLATFORM_TYPE:
435  zwave_js_config = {
436  state.CONF_PLATFORM: trigger_platform,
437  CONF_DEVICE_ID: config[CONF_DEVICE_ID],
438  }
440  config,
441  zwave_js_config,
442  [
443  ATTR_COMMAND_CLASS,
444  ATTR_PROPERTY,
445  ATTR_PROPERTY_KEY,
446  ATTR_ENDPOINT,
447  ATTR_FROM,
448  ATTR_TO,
449  ],
450  )
451  zwave_js_config = await trigger.async_validate_trigger_config(
452  hass, zwave_js_config
453  )
454  return await trigger.async_attach_trigger(
455  hass, zwave_js_config, action, trigger_info
456  )
457 
458  raise HomeAssistantError(f"Unhandled trigger type {trigger_type}")
459 
460 
462  hass: HomeAssistant, config: ConfigType
463 ) -> dict[str, vol.Schema]:
464  """List trigger capabilities."""
465  trigger_type = config[CONF_TYPE]
466 
467  node = async_get_node_from_device_id(hass, config[CONF_DEVICE_ID])
468 
469  # Add additional fields to the automation trigger UI
470  if trigger_type == NOTIFICATION_NOTIFICATION:
471  return {
472  "extra_fields": vol.Schema(
473  {
474  vol.Optional(f"{ATTR_TYPE}."): cv.string,
475  vol.Optional(ATTR_LABEL): cv.string,
476  vol.Optional(ATTR_EVENT): cv.string,
477  vol.Optional(ATTR_EVENT_LABEL): cv.string,
478  }
479  )
480  }
481 
482  if trigger_type == ENTRY_CONTROL_NOTIFICATION:
483  return {
484  "extra_fields": vol.Schema(
485  {
486  vol.Optional(ATTR_EVENT_TYPE): cv.string,
487  vol.Optional(ATTR_DATA_TYPE): cv.string,
488  }
489  )
490  }
491 
492  if trigger_type == NODE_STATUS:
493  return {
494  "extra_fields": vol.Schema(
495  {
496  vol.Optional(state.CONF_FROM): vol.In(NODE_STATUSES),
497  vol.Optional(state.CONF_TO): vol.In(NODE_STATUSES),
498  vol.Optional(state.CONF_FOR): cv.positive_time_period_dict,
499  }
500  )
501  }
502 
503  if trigger_type in (
504  BASIC_VALUE_NOTIFICATION,
505  CENTRAL_SCENE_VALUE_NOTIFICATION,
506  SCENE_ACTIVATION_VALUE_NOTIFICATION,
507  ):
508  value_schema = get_value_state_schema(get_zwave_value_from_config(node, config))
509 
510  # We should never get here, but just in case we should add a guard
511  if not value_schema:
512  return {}
513 
514  return {"extra_fields": vol.Schema({vol.Optional(ATTR_VALUE): value_schema})}
515 
516  if trigger_type == CONFIG_PARAMETER_VALUE_UPDATED:
517  value_schema = get_value_state_schema(get_zwave_value_from_config(node, config))
518  if not value_schema:
519  return {}
520  return {
521  "extra_fields": vol.Schema(
522  {
523  vol.Optional(ATTR_FROM): value_schema,
524  vol.Optional(ATTR_TO): value_schema,
525  }
526  )
527  }
528 
529  if trigger_type == VALUE_VALUE_UPDATED:
530  # Only show command classes on this node and exclude Configuration CC since it
531  # is already covered
532  return {
533  "extra_fields": vol.Schema(
534  {
535  vol.Required(ATTR_COMMAND_CLASS): vol.In(
536  {
537  CommandClass(cc.id).value: cc.name
538  for cc in sorted(
539  node.command_classes, key=lambda cc: cc.name
540  )
541  if cc.id != CommandClass.CONFIGURATION
542  }
543  ),
544  vol.Required(ATTR_PROPERTY): cv.string,
545  vol.Optional(ATTR_PROPERTY_KEY): cv.string,
546  vol.Optional(ATTR_ENDPOINT): cv.string,
547  vol.Optional(ATTR_FROM): cv.string,
548  vol.Optional(ATTR_TO): cv.string,
549  }
550  )
551  }
552 
553  return {}
bool async_bypass_dynamic_config_validation(HomeAssistant hass, str device_id)
CALLBACK_TYPE async_attach_trigger(HomeAssistant hass, ConfigType config, TriggerActionType action, TriggerInfo trigger_info)
ConfigType async_validate_trigger_config(HomeAssistant hass, ConfigType config)
dict[str, vol.Schema] async_get_trigger_capabilities(HomeAssistant hass, ConfigType config)
list[dict[str, Any]] async_get_triggers(HomeAssistant hass, str device_id)
str|None async_get_node_status_sensor_entity_id(HomeAssistant hass, str device_id, er.EntityRegistry|None ent_reg=None, dr.DeviceRegistry|None dev_reg=None)
Definition: helpers.py:431
None copy_available_params(dict[str, Any] input_dict, dict[str, Any] output_dict, list[str] params)
Definition: helpers.py:473
Callable[[ConfigType], ConfigType] check_type_schema_map(dict[str, vol.Schema] schema_map)
Definition: helpers.py:461
VolSchemaType|vol.Coerce|vol.In|None get_value_state_schema(ZwaveValue value)
Definition: helpers.py:482
ZwaveValue get_zwave_value_from_config(ZwaveNode node, ConfigType config)
Definition: helpers.py:396
ZwaveNode async_get_node_from_device_id(HomeAssistant hass, str device_id, dr.DeviceRegistry|None dev_reg=None)
Definition: helpers.py:253