Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Helpers for device automations."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from collections.abc import Awaitable, Callable, Coroutine, Iterable, Mapping
7 from dataclasses import dataclass
8 from enum import Enum
9 from functools import wraps
10 import logging
11 from types import ModuleType
12 from typing import TYPE_CHECKING, Any, Literal, overload
13 
14 import voluptuous as vol
15 import voluptuous_serialize
16 
17 from homeassistant.components import websocket_api
18 from homeassistant.components.websocket_api import ActiveConnection
19 from homeassistant.const import (
20  ATTR_ENTITY_ID,
21  CONF_DEVICE_ID,
22  CONF_DOMAIN,
23  CONF_ENTITY_ID,
24  CONF_PLATFORM,
25 )
26 from homeassistant.core import HomeAssistant, callback
27 from homeassistant.helpers import (
28  config_validation as cv,
29  device_registry as dr,
30  entity_registry as er,
31 )
32 from homeassistant.helpers.typing import ConfigType, VolSchemaType
33 from homeassistant.loader import IntegrationNotFound
34 from homeassistant.requirements import (
35  RequirementsNotFound,
36  async_get_integration_with_requirements,
37 )
38 
39 from .const import ( # noqa: F401
40  CONF_IS_OFF,
41  CONF_IS_ON,
42  CONF_TURNED_OFF,
43  CONF_TURNED_ON,
44 )
45 from .exceptions import DeviceNotFound, EntityNotFound, InvalidDeviceAutomationConfig
46 
47 if TYPE_CHECKING:
48  from .action import DeviceAutomationActionProtocol
49  from .condition import DeviceAutomationConditionProtocol
50  from .trigger import DeviceAutomationTriggerProtocol
51 
52  type DeviceAutomationPlatformType = (
53  ModuleType
54  | DeviceAutomationTriggerProtocol
55  | DeviceAutomationConditionProtocol
56  | DeviceAutomationActionProtocol
57  )
58 
59 
60 DOMAIN = "device_automation"
61 
62 CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
63 
64 DEVICE_TRIGGER_BASE_SCHEMA: vol.Schema = cv.TRIGGER_BASE_SCHEMA.extend(
65  {
66  vol.Required(CONF_PLATFORM): "device",
67  vol.Required(CONF_DOMAIN): str,
68  vol.Required(CONF_DEVICE_ID): str,
69  vol.Remove("metadata"): dict,
70  }
71 )
72 
73 
74 @dataclass
76  """Details for device automation."""
77 
78  section: str
79  get_automations_func: str
80  get_capabilities_func: str
81 
82 
84  """Device automation type."""
85 
87  "device_trigger",
88  "async_get_triggers",
89  "async_get_trigger_capabilities",
90  )
92  "device_condition",
93  "async_get_conditions",
94  "async_get_condition_capabilities",
95  )
97  "device_action",
98  "async_get_actions",
99  "async_get_action_capabilities",
100  )
101 
102 
103 # TYPES is deprecated as of Home Assistant 2022.2, use DeviceAutomationType instead
104 TYPES = {
105  "trigger": DeviceAutomationType.TRIGGER.value,
106  "condition": DeviceAutomationType.CONDITION.value,
107  "action": DeviceAutomationType.ACTION.value,
108 }
109 
110 
111 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
112  """Set up device automation."""
113  websocket_api.async_register_command(hass, websocket_device_automation_list_actions)
114  websocket_api.async_register_command(
115  hass, websocket_device_automation_list_conditions
116  )
117  websocket_api.async_register_command(
118  hass, websocket_device_automation_list_triggers
119  )
120  websocket_api.async_register_command(
121  hass, websocket_device_automation_get_action_capabilities
122  )
123  websocket_api.async_register_command(
124  hass, websocket_device_automation_get_condition_capabilities
125  )
126  websocket_api.async_register_command(
127  hass, websocket_device_automation_get_trigger_capabilities
128  )
129  return True
130 
131 
132 @overload
134  hass: HomeAssistant,
135  domain: str,
136  automation_type: Literal[DeviceAutomationType.TRIGGER],
137 ) -> DeviceAutomationTriggerProtocol: ...
138 
139 
140 @overload
142  hass: HomeAssistant,
143  domain: str,
144  automation_type: Literal[DeviceAutomationType.CONDITION],
145 ) -> DeviceAutomationConditionProtocol: ...
146 
147 
148 @overload
150  hass: HomeAssistant,
151  domain: str,
152  automation_type: Literal[DeviceAutomationType.ACTION],
153 ) -> DeviceAutomationActionProtocol: ...
154 
155 
156 @overload
158  hass: HomeAssistant, domain: str, automation_type: DeviceAutomationType
159 ) -> DeviceAutomationPlatformType: ...
160 
161 
163  hass: HomeAssistant, domain: str, automation_type: DeviceAutomationType
164 ) -> DeviceAutomationPlatformType:
165  """Load device automation platform for integration.
166 
167  Throws InvalidDeviceAutomationConfig if the integration is not found or does not support device automation.
168  """
169  platform_name = automation_type.value.section
170  try:
171  integration = await async_get_integration_with_requirements(hass, domain)
172  platform = await integration.async_get_platform(platform_name)
173  except IntegrationNotFound as err:
175  f"Integration '{domain}' not found"
176  ) from err
177  except RequirementsNotFound as err:
179  f"Integration '{domain}' could not be loaded"
180  ) from err
181  except ImportError as err:
183  f"Integration '{domain}' does not support device automation "
184  f"{automation_type.name.lower()}s"
185  ) from err
186 
187  return platform
188 
189 
190 @callback
192  hass: HomeAssistant, automation: dict[str, Any]
193 ) -> None:
194  """Set device automation metadata based on entity registry entry data."""
195  if "metadata" not in automation:
196  automation["metadata"] = {}
197  if ATTR_ENTITY_ID not in automation or "secondary" in automation["metadata"]:
198  return
199 
200  entity_registry = er.async_get(hass)
201  # Guard against the entry being removed before this is called
202  if not (entry := entity_registry.async_get(automation[ATTR_ENTITY_ID])):
203  return
204 
205  automation["metadata"]["secondary"] = bool(entry.entity_category or entry.hidden_by)
206 
207 
209  hass: HomeAssistant,
210  domain: str,
211  automation_type: DeviceAutomationType,
212  device_ids: Iterable[str],
213  return_exceptions: bool,
214 ) -> list[list[dict[str, Any]] | Exception]:
215  """List device automations."""
216  try:
217  platform = await async_get_device_automation_platform(
218  hass, domain, automation_type
219  )
220  except InvalidDeviceAutomationConfig:
221  return []
222 
223  function_name = automation_type.value.get_automations_func
224 
225  return await asyncio.gather( # type: ignore[no-any-return]
226  *(
227  getattr(platform, function_name)(hass, device_id)
228  for device_id in device_ids
229  ),
230  return_exceptions=return_exceptions,
231  )
232 
233 
235  hass: HomeAssistant,
236  automation_type: DeviceAutomationType,
237  device_ids: Iterable[str] | None = None,
238 ) -> Mapping[str, list[dict[str, Any]]]:
239  """List device automations."""
240  device_registry = dr.async_get(hass)
241  entity_registry = er.async_get(hass)
242  domain_devices: dict[str, set[str]] = {}
243  device_entities_domains: dict[str, set[str]] = {}
244  match_device_ids = set(device_ids or device_registry.devices)
245  combined_results: dict[str, list[dict[str, Any]]] = {}
246 
247  for device_id in match_device_ids:
248  for entry in entity_registry.entities.get_entries_for_device_id(device_id):
249  device_entities_domains.setdefault(device_id, set()).add(entry.domain)
250 
251  for device_id in match_device_ids:
252  combined_results[device_id] = []
253  if (device := device_registry.async_get(device_id)) is None:
254  raise DeviceNotFound
255  for entry_id in device.config_entries:
256  if config_entry := hass.config_entries.async_get_entry(entry_id):
257  domain_devices.setdefault(config_entry.domain, set()).add(device_id)
258  for domain in device_entities_domains.get(device_id, []):
259  domain_devices.setdefault(domain, set()).add(device_id)
260 
261  # If specific device ids were requested, we allow
262  # InvalidDeviceAutomationConfig to be thrown, otherwise we skip
263  # devices that do not have valid triggers
264  return_exceptions = not bool(device_ids)
265 
266  for domain_results in await asyncio.gather(
267  *(
269  hass, domain, automation_type, domain_device_ids, return_exceptions
270  )
271  for domain, domain_device_ids in domain_devices.items()
272  )
273  ):
274  for device_results in domain_results:
275  if device_results is None or isinstance(
276  device_results, InvalidDeviceAutomationConfig
277  ):
278  continue
279  if isinstance(device_results, Exception):
280  logging.getLogger(__name__).error(
281  "Unexpected error fetching device %ss",
282  automation_type.name.lower(),
283  exc_info=device_results,
284  )
285  continue
286  for automation in device_results:
288  combined_results[automation["device_id"]].append(automation)
289 
290  return combined_results
291 
292 
294  hass: HomeAssistant,
295  automation_type: DeviceAutomationType,
296  automation: Mapping[str, Any],
297 ) -> dict[str, Any]:
298  """List device automations."""
299  try:
300  platform = await async_get_device_automation_platform(
301  hass, automation[CONF_DOMAIN], automation_type
302  )
303  except InvalidDeviceAutomationConfig:
304  return {}
305 
306  function_name = automation_type.value.get_capabilities_func
307 
308  if not hasattr(platform, function_name):
309  # The device automation has no capabilities
310  return {}
311 
312  try:
313  capabilities = await getattr(platform, function_name)(hass, automation)
314  except (EntityNotFound, InvalidDeviceAutomationConfig):
315  return {}
316 
317  capabilities = capabilities.copy()
318 
319  if (extra_fields := capabilities.get("extra_fields")) is None:
320  capabilities["extra_fields"] = []
321  else:
322  capabilities["extra_fields"] = voluptuous_serialize.convert(
323  extra_fields, custom_serializer=cv.custom_serializer
324  )
325 
326  return capabilities # type: ignore[no-any-return]
327 
328 
329 @callback
331  hass: HomeAssistant, entity_registry_id: str
332 ) -> er.RegistryEntry:
333  """Get an entity registry entry from entry ID or raise."""
334  entity_registry = er.async_get(hass)
335  entry = entity_registry.async_get(entity_registry_id)
336  if entry is None:
337  raise EntityNotFound
338  return entry
339 
340 
341 @callback
343  hass: HomeAssistant, config: ConfigType, schema: VolSchemaType
344 ) -> ConfigType:
345  """Validate schema and resolve entity registry entry id to entity_id."""
346  config = schema(config)
347 
348  registry = er.async_get(hass)
349  if CONF_ENTITY_ID in config:
350  config[CONF_ENTITY_ID] = er.async_resolve_entity_id(
351  registry, config[CONF_ENTITY_ID]
352  )
353 
354  return config
355 
356 
358  func: Callable[[HomeAssistant, ActiveConnection, dict[str, Any]], Awaitable[None]],
359 ) -> Callable[
360  [HomeAssistant, ActiveConnection, dict[str, Any]], Coroutine[Any, Any, None]
361 ]:
362  """Handle device automation errors."""
363 
364  @wraps(func)
365  async def with_error_handling(
366  hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
367  ) -> None:
368  try:
369  await func(hass, connection, msg)
370  except DeviceNotFound:
371  connection.send_error(
372  msg["id"], websocket_api.ERR_NOT_FOUND, "Device not found"
373  )
374 
375  return with_error_handling
376 
377 
378 @websocket_api.websocket_command( { vol.Required("type"): "device_automation/action/list",
379  vol.Required("device_id"): str,
380  }
381 )
382 @websocket_api.async_response
383 @handle_device_errors
385  hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
386 ) -> None:
387  """Handle request for device actions."""
388  device_id = msg["device_id"]
389  actions = (
391  hass, DeviceAutomationType.ACTION, [device_id]
392  )
393  ).get(device_id)
394  connection.send_result(msg["id"], actions)
395 
396 
397 @websocket_api.websocket_command( { vol.Required("type"): "device_automation/condition/list",
398  vol.Required("device_id"): str,
399  }
400 )
401 @websocket_api.async_response
402 @handle_device_errors
404  hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
405 ) -> None:
406  """Handle request for device conditions."""
407  device_id = msg["device_id"]
408  conditions = (
410  hass, DeviceAutomationType.CONDITION, [device_id]
411  )
412  ).get(device_id)
413  connection.send_result(msg["id"], conditions)
414 
415 
416 @websocket_api.websocket_command( { vol.Required("type"): "device_automation/trigger/list",
417  vol.Required("device_id"): str,
418  }
419 )
420 @websocket_api.async_response
421 @handle_device_errors
423  hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
424 ) -> None:
425  """Handle request for device triggers."""
426  device_id = msg["device_id"]
427  triggers = (
429  hass, DeviceAutomationType.TRIGGER, [device_id]
430  )
431  ).get(device_id)
432  connection.send_result(msg["id"], triggers)
433 
434 
435 @websocket_api.websocket_command( { vol.Required("type"): "device_automation/action/capabilities",
436  vol.Required("action"): dict,
437  }
438 )
439 @websocket_api.async_response
440 @handle_device_errors
442  hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
443 ) -> None:
444  """Handle request for device action capabilities."""
445  action = msg["action"]
446  capabilities = await _async_get_device_automation_capabilities(
447  hass, DeviceAutomationType.ACTION, action
448  )
449  connection.send_result(msg["id"], capabilities)
450 
451 
452 @websocket_api.websocket_command( { vol.Required("type"): "device_automation/condition/capabilities",
453  vol.Required("condition"): cv.DEVICE_CONDITION_BASE_SCHEMA.extend(
454  {}, extra=vol.ALLOW_EXTRA
455  ),
456  }
457 )
458 @websocket_api.async_response
459 @handle_device_errors
461  hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
462 ) -> None:
463  """Handle request for device condition capabilities."""
464  condition = msg["condition"]
465  capabilities = await _async_get_device_automation_capabilities(
466  hass, DeviceAutomationType.CONDITION, condition
467  )
468  connection.send_result(msg["id"], capabilities)
469 
470 
471 @websocket_api.websocket_command( { vol.Required("type"): "device_automation/trigger/capabilities",
472  # The frontend responds with `trigger` as key, while the
473  # `DEVICE_TRIGGER_BASE_SCHEMA` expects `platform1` as key.
474  vol.Required("trigger"): vol.All(
475  cv._trigger_pre_validator, # noqa: SLF001
476  DEVICE_TRIGGER_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA),
477  ),
478  }
479 )
480 @websocket_api.async_response
481 @handle_device_errors
483  hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
484 ) -> None:
485  """Handle request for device trigger capabilities."""
486  trigger = msg["trigger"]
487  capabilities = await _async_get_device_automation_capabilities(
488  hass, DeviceAutomationType.TRIGGER, trigger
489  )
490  connection.send_result(msg["id"], capabilities)
491 
bool add(self, _T matcher)
Definition: match.py:185
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
None websocket_device_automation_list_triggers(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg)
Definition: __init__.py:430
list[list[dict[str, Any]]|Exception] _async_get_device_automations_from_domain(HomeAssistant hass, str domain, DeviceAutomationType automation_type, Iterable[str] device_ids, bool return_exceptions)
Definition: __init__.py:214
er.RegistryEntry async_get_entity_registry_entry_or_raise(HomeAssistant hass, str entity_registry_id)
Definition: __init__.py:332
ConfigType async_validate_entity_schema(HomeAssistant hass, ConfigType config, VolSchemaType schema)
Definition: __init__.py:344
dict[str, Any] _async_get_device_automation_capabilities(HomeAssistant hass, DeviceAutomationType automation_type, Mapping[str, Any] automation)
Definition: __init__.py:297
None websocket_device_automation_list_actions(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg)
Definition: __init__.py:388
None websocket_device_automation_get_trigger_capabilities(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg)
Definition: __init__.py:496
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:111
None websocket_device_automation_get_condition_capabilities(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg)
Definition: __init__.py:472
None _async_set_entity_device_automation_metadata(HomeAssistant hass, dict[str, Any] automation)
Definition: __init__.py:193
Mapping[str, list[dict[str, Any]]] async_get_device_automations(HomeAssistant hass, DeviceAutomationType automation_type, Iterable[str]|None device_ids=None)
Definition: __init__.py:238
DeviceAutomationTriggerProtocol async_get_device_automation_platform(HomeAssistant hass, str domain, Literal[DeviceAutomationType.TRIGGER] automation_type)
Definition: __init__.py:137
None websocket_device_automation_get_action_capabilities(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg)
Definition: __init__.py:451
Callable[[HomeAssistant, ActiveConnection, dict[str, Any]], Coroutine[Any, Any, None]] handle_device_errors(Callable[[HomeAssistant, ActiveConnection, dict[str, Any]], Awaitable[None]] func)
Definition: __init__.py:361
None websocket_device_automation_list_conditions(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg)
Definition: __init__.py:409
Integration async_get_integration_with_requirements(HomeAssistant hass, str domain)
Definition: requirements.py:46