1 """Helpers for device automations."""
3 from __future__
import annotations
6 from collections.abc
import Awaitable, Callable, Coroutine, Iterable, Mapping
7 from dataclasses
import dataclass
9 from functools
import wraps
11 from types
import ModuleType
12 from typing
import TYPE_CHECKING, Any, Literal, overload
14 import voluptuous
as vol
15 import voluptuous_serialize
28 config_validation
as cv,
29 device_registry
as dr,
30 entity_registry
as er,
36 async_get_integration_with_requirements,
45 from .exceptions
import DeviceNotFound, EntityNotFound, InvalidDeviceAutomationConfig
48 from .action
import DeviceAutomationActionProtocol
49 from .condition
import DeviceAutomationConditionProtocol
50 from .trigger
import DeviceAutomationTriggerProtocol
52 type DeviceAutomationPlatformType = (
54 | DeviceAutomationTriggerProtocol
55 | DeviceAutomationConditionProtocol
56 | DeviceAutomationActionProtocol
60 DOMAIN =
"device_automation"
62 CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
64 DEVICE_TRIGGER_BASE_SCHEMA: vol.Schema = cv.TRIGGER_BASE_SCHEMA.extend(
66 vol.Required(CONF_PLATFORM):
"device",
67 vol.Required(CONF_DOMAIN): str,
68 vol.Required(CONF_DEVICE_ID): str,
69 vol.Remove(
"metadata"): dict,
76 """Details for device automation."""
79 get_automations_func: str
80 get_capabilities_func: str
84 """Device automation type."""
89 "async_get_trigger_capabilities",
93 "async_get_conditions",
94 "async_get_condition_capabilities",
99 "async_get_action_capabilities",
105 "trigger": DeviceAutomationType.TRIGGER.value,
106 "condition": DeviceAutomationType.CONDITION.value,
107 "action": DeviceAutomationType.ACTION.value,
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
117 websocket_api.async_register_command(
118 hass, websocket_device_automation_list_triggers
120 websocket_api.async_register_command(
121 hass, websocket_device_automation_get_action_capabilities
123 websocket_api.async_register_command(
124 hass, websocket_device_automation_get_condition_capabilities
126 websocket_api.async_register_command(
127 hass, websocket_device_automation_get_trigger_capabilities
136 automation_type: Literal[DeviceAutomationType.TRIGGER],
137 ) -> DeviceAutomationTriggerProtocol: ...
144 automation_type: Literal[DeviceAutomationType.CONDITION],
145 ) -> DeviceAutomationConditionProtocol: ...
152 automation_type: Literal[DeviceAutomationType.ACTION],
153 ) -> DeviceAutomationActionProtocol: ...
158 hass: HomeAssistant, domain: str, automation_type: DeviceAutomationType
159 ) -> DeviceAutomationPlatformType: ...
163 hass: HomeAssistant, domain: str, automation_type: DeviceAutomationType
164 ) -> DeviceAutomationPlatformType:
165 """Load device automation platform for integration.
167 Throws InvalidDeviceAutomationConfig if the integration is not found or does not support device automation.
169 platform_name = automation_type.value.section
172 platform = await integration.async_get_platform(platform_name)
173 except IntegrationNotFound
as err:
175 f
"Integration '{domain}' not found"
177 except RequirementsNotFound
as err:
179 f
"Integration '{domain}' could not be loaded"
181 except ImportError
as err:
183 f
"Integration '{domain}' does not support device automation "
184 f
"{automation_type.name.lower()}s"
192 hass: HomeAssistant, automation: dict[str, Any]
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"]:
200 entity_registry = er.async_get(hass)
202 if not (entry := entity_registry.async_get(automation[ATTR_ENTITY_ID])):
205 automation[
"metadata"][
"secondary"] = bool(entry.entity_category
or entry.hidden_by)
211 automation_type: DeviceAutomationType,
212 device_ids: Iterable[str],
213 return_exceptions: bool,
214 ) -> list[list[dict[str, Any]] | Exception]:
215 """List device automations."""
218 hass, domain, automation_type
220 except InvalidDeviceAutomationConfig:
223 function_name = automation_type.value.get_automations_func
225 return await asyncio.gather(
227 getattr(platform, function_name)(hass, device_id)
228 for device_id
in device_ids
230 return_exceptions=return_exceptions,
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]]] = {}
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)
251 for device_id
in match_device_ids:
252 combined_results[device_id] = []
253 if (device := device_registry.async_get(device_id))
is None:
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)
264 return_exceptions =
not bool(device_ids)
266 for domain_results
in await asyncio.gather(
269 hass, domain, automation_type, domain_device_ids, return_exceptions
271 for domain, domain_device_ids
in domain_devices.items()
274 for device_results
in domain_results:
275 if device_results
is None or isinstance(
276 device_results, InvalidDeviceAutomationConfig
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,
286 for automation
in device_results:
288 combined_results[automation[
"device_id"]].append(automation)
290 return combined_results
295 automation_type: DeviceAutomationType,
296 automation: Mapping[str, Any],
298 """List device automations."""
301 hass, automation[CONF_DOMAIN], automation_type
303 except InvalidDeviceAutomationConfig:
306 function_name = automation_type.value.get_capabilities_func
308 if not hasattr(platform, function_name):
313 capabilities = await getattr(platform, function_name)(hass, automation)
314 except (EntityNotFound, InvalidDeviceAutomationConfig):
317 capabilities = capabilities.copy()
319 if (extra_fields := capabilities.get(
"extra_fields"))
is None:
320 capabilities[
"extra_fields"] = []
322 capabilities[
"extra_fields"] = voluptuous_serialize.convert(
323 extra_fields, custom_serializer=cv.custom_serializer
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)
343 hass: HomeAssistant, config: ConfigType, schema: VolSchemaType
345 """Validate schema and resolve entity registry entry id to entity_id."""
346 config = schema(config)
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]
358 func: Callable[[HomeAssistant, ActiveConnection, dict[str, Any]], Awaitable[
None]],
360 [HomeAssistant, ActiveConnection, dict[str, Any]], Coroutine[Any, Any,
None]
362 """Handle device automation errors."""
365 async
def with_error_handling(
366 hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
369 await func(hass, connection, msg)
370 except DeviceNotFound:
371 connection.send_error(
372 msg[
"id"], websocket_api.ERR_NOT_FOUND,
"Device not found"
375 return with_error_handling
378 @websocket_api.websocket_command(
{
vol.Required("type"):
"device_automation/action/list",
379 vol.Required(
"device_id"): str,
382 @websocket_api.async_response
383 @handle_device_errors
385 hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
387 """Handle request for device actions."""
388 device_id = msg[
"device_id"]
391 hass, DeviceAutomationType.ACTION, [device_id]
394 connection.send_result(msg[
"id"], actions)
397 @websocket_api.websocket_command(
{
vol.Required("type"):
"device_automation/condition/list",
398 vol.Required(
"device_id"): str,
401 @websocket_api.async_response
402 @handle_device_errors
404 hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
406 """Handle request for device conditions."""
407 device_id = msg[
"device_id"]
410 hass, DeviceAutomationType.CONDITION, [device_id]
413 connection.send_result(msg[
"id"], conditions)
416 @websocket_api.websocket_command(
{
vol.Required("type"):
"device_automation/trigger/list",
417 vol.Required(
"device_id"): str,
420 @websocket_api.async_response
421 @handle_device_errors
423 hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
425 """Handle request for device triggers."""
426 device_id = msg[
"device_id"]
429 hass, DeviceAutomationType.TRIGGER, [device_id]
432 connection.send_result(msg[
"id"], triggers)
435 @websocket_api.websocket_command(
{
vol.Required("type"):
"device_automation/action/capabilities",
436 vol.Required(
"action"): dict,
439 @websocket_api.async_response
440 @handle_device_errors
442 hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
444 """Handle request for device action capabilities."""
445 action = msg[
"action"]
447 hass, DeviceAutomationType.ACTION, action
449 connection.send_result(msg[
"id"], capabilities)
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
458 @websocket_api.async_response
459 @handle_device_errors
461 hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
463 """Handle request for device condition capabilities."""
464 condition = msg[
"condition"]
466 hass, DeviceAutomationType.CONDITION, condition
468 connection.send_result(msg[
"id"], capabilities)
471 @websocket_api.websocket_command(
{
vol.Required("type"):
"device_automation/trigger/capabilities",
474 vol.Required(
"trigger"): vol.All(
475 cv._trigger_pre_validator,
476 DEVICE_TRIGGER_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA),
480 @websocket_api.async_response
481 @handle_device_errors
483 hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
485 """Handle request for device trigger capabilities."""
486 trigger = msg[
"trigger"]
488 hass, DeviceAutomationType.TRIGGER, trigger
490 connection.send_result(msg[
"id"], capabilities)
491
bool add(self, _T matcher)
web.Response get(self, web.Request request, str config_key)
None websocket_device_automation_list_triggers(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg)
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)
er.RegistryEntry async_get_entity_registry_entry_or_raise(HomeAssistant hass, str entity_registry_id)
ConfigType async_validate_entity_schema(HomeAssistant hass, ConfigType config, VolSchemaType schema)
dict[str, Any] _async_get_device_automation_capabilities(HomeAssistant hass, DeviceAutomationType automation_type, Mapping[str, Any] automation)
None websocket_device_automation_list_actions(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg)
None websocket_device_automation_get_trigger_capabilities(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg)
bool async_setup(HomeAssistant hass, ConfigType config)
None websocket_device_automation_get_condition_capabilities(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg)
None _async_set_entity_device_automation_metadata(HomeAssistant hass, dict[str, Any] automation)
Mapping[str, list[dict[str, Any]]] async_get_device_automations(HomeAssistant hass, DeviceAutomationType automation_type, Iterable[str]|None device_ids=None)
DeviceAutomationTriggerProtocol async_get_device_automation_platform(HomeAssistant hass, str domain, Literal[DeviceAutomationType.TRIGGER] automation_type)
None websocket_device_automation_get_action_capabilities(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg)
Callable[[HomeAssistant, ActiveConnection, dict[str, Any]], Coroutine[Any, Any, None]] handle_device_errors(Callable[[HomeAssistant, ActiveConnection, dict[str, Any]], Awaitable[None]] func)
None websocket_device_automation_list_conditions(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg)
Integration async_get_integration_with_requirements(HomeAssistant hass, str domain)