2019-02-13 20:21:14 +00:00
|
|
|
"""Allow to set up simple automation rules via the config file."""
|
2021-03-17 22:34:25 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2015-01-16 07:32:27 +00:00
|
|
|
import logging
|
2021-03-17 22:34:25 +00:00
|
|
|
from typing import Any, Awaitable, Callable, Dict, cast
|
2015-01-16 07:32:27 +00:00
|
|
|
|
2016-04-04 19:18:58 +00:00
|
|
|
import voluptuous as vol
|
2020-11-02 14:00:13 +00:00
|
|
|
from voluptuous.humanize import humanize_error
|
2016-04-04 19:18:58 +00:00
|
|
|
|
2020-11-02 14:00:13 +00:00
|
|
|
from homeassistant.components import blueprint
|
2016-08-26 06:25:57 +00:00
|
|
|
from homeassistant.const import (
|
2019-07-31 19:25:30 +00:00
|
|
|
ATTR_ENTITY_ID,
|
2021-03-05 18:08:04 +00:00
|
|
|
ATTR_MODE,
|
2019-07-31 19:25:30 +00:00
|
|
|
ATTR_NAME,
|
2020-07-05 14:25:15 +00:00
|
|
|
CONF_ALIAS,
|
2021-02-05 02:41:43 +00:00
|
|
|
CONF_CONDITION,
|
2020-02-05 15:52:21 +00:00
|
|
|
CONF_DEVICE_ID,
|
|
|
|
CONF_ENTITY_ID,
|
2019-07-31 19:25:30 +00:00
|
|
|
CONF_ID,
|
2020-07-05 14:25:15 +00:00
|
|
|
CONF_MODE,
|
2019-07-31 19:25:30 +00:00
|
|
|
CONF_PLATFORM,
|
2020-09-10 18:41:42 +00:00
|
|
|
CONF_VARIABLES,
|
2020-02-05 15:52:21 +00:00
|
|
|
CONF_ZONE,
|
2020-04-24 21:13:39 +00:00
|
|
|
EVENT_HOMEASSISTANT_STARTED,
|
2019-07-31 19:25:30 +00:00
|
|
|
SERVICE_RELOAD,
|
|
|
|
SERVICE_TOGGLE,
|
|
|
|
SERVICE_TURN_OFF,
|
|
|
|
SERVICE_TURN_ON,
|
|
|
|
STATE_ON,
|
|
|
|
)
|
2020-07-22 15:55:49 +00:00
|
|
|
from homeassistant.core import (
|
|
|
|
Context,
|
|
|
|
CoreState,
|
|
|
|
HomeAssistant,
|
|
|
|
callback,
|
|
|
|
split_entity_id,
|
|
|
|
)
|
2021-02-21 13:54:36 +00:00
|
|
|
from homeassistant.exceptions import (
|
|
|
|
ConditionError,
|
|
|
|
ConditionErrorContainer,
|
|
|
|
ConditionErrorIndex,
|
|
|
|
HomeAssistantError,
|
|
|
|
)
|
2020-09-10 18:41:42 +00:00
|
|
|
from homeassistant.helpers import condition, extract_domain_configs, template
|
2019-03-21 05:56:46 +00:00
|
|
|
import homeassistant.helpers.config_validation as cv
|
2016-08-26 06:25:57 +00:00
|
|
|
from homeassistant.helpers.entity import ToggleEntity
|
|
|
|
from homeassistant.helpers.entity_component import EntityComponent
|
2018-11-28 12:16:43 +00:00
|
|
|
from homeassistant.helpers.restore_state import RestoreEntity
|
2020-07-11 00:00:57 +00:00
|
|
|
from homeassistant.helpers.script import (
|
2020-07-14 17:47:59 +00:00
|
|
|
ATTR_CUR,
|
|
|
|
ATTR_MAX,
|
2020-07-11 00:00:57 +00:00
|
|
|
CONF_MAX,
|
2020-09-02 09:05:14 +00:00
|
|
|
CONF_MAX_EXCEEDED,
|
2020-07-11 00:00:57 +00:00
|
|
|
Script,
|
|
|
|
)
|
2020-09-11 10:24:16 +00:00
|
|
|
from homeassistant.helpers.script_variables import ScriptVariables
|
2019-12-08 16:29:39 +00:00
|
|
|
from homeassistant.helpers.service import async_register_admin_service
|
2021-03-10 22:42:13 +00:00
|
|
|
from homeassistant.helpers.trace import trace_get, trace_path
|
2020-08-17 16:54:56 +00:00
|
|
|
from homeassistant.helpers.trigger import async_initialize_triggers
|
2019-09-24 21:57:05 +00:00
|
|
|
from homeassistant.helpers.typing import TemplateVarsType
|
2019-03-21 05:56:46 +00:00
|
|
|
from homeassistant.loader import bind_hass
|
2020-08-28 19:51:15 +00:00
|
|
|
from homeassistant.util.dt import parse_datetime
|
2015-01-16 07:32:27 +00:00
|
|
|
|
2021-03-04 13:16:24 +00:00
|
|
|
from .config import AutomationConfig, async_validate_config_item
|
|
|
|
|
2020-11-13 21:49:01 +00:00
|
|
|
# Not used except by packages to check config structure
|
2021-03-02 08:02:04 +00:00
|
|
|
from .config import PLATFORM_SCHEMA # noqa: F401
|
2020-11-12 10:58:28 +00:00
|
|
|
from .const import (
|
|
|
|
CONF_ACTION,
|
|
|
|
CONF_INITIAL_STATE,
|
|
|
|
CONF_TRIGGER,
|
2021-02-08 09:50:38 +00:00
|
|
|
CONF_TRIGGER_VARIABLES,
|
2020-11-12 10:58:28 +00:00
|
|
|
DEFAULT_INITIAL_STATE,
|
|
|
|
DOMAIN,
|
|
|
|
LOGGER,
|
|
|
|
)
|
|
|
|
from .helpers import async_get_blueprints
|
2021-03-22 18:19:38 +00:00
|
|
|
from .trace import trace_automation
|
2020-11-12 10:58:28 +00:00
|
|
|
|
2019-09-20 15:23:34 +00:00
|
|
|
# mypy: allow-untyped-calls, allow-untyped-defs
|
2019-08-12 03:38:18 +00:00
|
|
|
# mypy: no-check-untyped-defs, no-warn-return-any
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
2015-01-16 07:32:27 +00:00
|
|
|
|
2016-10-05 04:20:48 +00:00
|
|
|
|
2020-01-08 09:36:11 +00:00
|
|
|
CONF_SKIP_CONDITION = "skip_condition"
|
2020-08-02 02:31:47 +00:00
|
|
|
CONF_STOP_ACTIONS = "stop_actions"
|
|
|
|
DEFAULT_STOP_ACTIONS = True
|
2015-01-16 07:32:27 +00:00
|
|
|
|
2020-06-01 22:19:00 +00:00
|
|
|
EVENT_AUTOMATION_RELOADED = "automation_reloaded"
|
2020-06-02 00:18:40 +00:00
|
|
|
EVENT_AUTOMATION_TRIGGERED = "automation_triggered"
|
2020-06-01 22:19:00 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
ATTR_LAST_TRIGGERED = "last_triggered"
|
2020-08-28 15:02:12 +00:00
|
|
|
ATTR_SOURCE = "source"
|
2019-07-31 19:25:30 +00:00
|
|
|
ATTR_VARIABLES = "variables"
|
|
|
|
SERVICE_TRIGGER = "trigger"
|
2016-08-26 06:25:57 +00:00
|
|
|
|
2021-03-04 13:16:24 +00:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
2019-09-24 21:57:05 +00:00
|
|
|
AutomationActionType = Callable[[HomeAssistant, TemplateVarsType], Awaitable[None]]
|
|
|
|
|
2020-11-02 14:00:13 +00:00
|
|
|
|
2017-07-16 17:14:46 +00:00
|
|
|
@bind_hass
|
2017-04-04 06:11:39 +00:00
|
|
|
def is_on(hass, entity_id):
|
2016-08-26 06:25:57 +00:00
|
|
|
"""
|
|
|
|
Return true if specified automation entity_id is on.
|
|
|
|
|
2017-04-04 06:11:39 +00:00
|
|
|
Async friendly.
|
2016-08-26 06:25:57 +00:00
|
|
|
"""
|
2017-04-04 06:11:39 +00:00
|
|
|
return hass.states.is_state(entity_id, STATE_ON)
|
2016-08-26 06:25:57 +00:00
|
|
|
|
|
|
|
|
2020-01-30 00:19:13 +00:00
|
|
|
@callback
|
2021-03-17 22:34:25 +00:00
|
|
|
def automations_with_entity(hass: HomeAssistant, entity_id: str) -> list[str]:
|
2020-01-30 00:19:13 +00:00
|
|
|
"""Return all automations that reference the entity."""
|
|
|
|
if DOMAIN not in hass.data:
|
|
|
|
return []
|
|
|
|
|
|
|
|
component = hass.data[DOMAIN]
|
|
|
|
|
2020-05-01 14:37:25 +00:00
|
|
|
return [
|
|
|
|
automation_entity.entity_id
|
|
|
|
for automation_entity in component.entities
|
|
|
|
if entity_id in automation_entity.referenced_entities
|
|
|
|
]
|
2020-01-30 00:19:13 +00:00
|
|
|
|
|
|
|
|
|
|
|
@callback
|
2021-03-17 22:34:25 +00:00
|
|
|
def entities_in_automation(hass: HomeAssistant, entity_id: str) -> list[str]:
|
2020-01-30 00:19:13 +00:00
|
|
|
"""Return all entities in a scene."""
|
|
|
|
if DOMAIN not in hass.data:
|
|
|
|
return []
|
|
|
|
|
|
|
|
component = hass.data[DOMAIN]
|
|
|
|
|
|
|
|
automation_entity = component.get_entity(entity_id)
|
|
|
|
|
|
|
|
if automation_entity is None:
|
|
|
|
return []
|
|
|
|
|
2020-02-05 15:52:21 +00:00
|
|
|
return list(automation_entity.referenced_entities)
|
2020-01-30 00:19:13 +00:00
|
|
|
|
|
|
|
|
|
|
|
@callback
|
2021-03-17 22:34:25 +00:00
|
|
|
def automations_with_device(hass: HomeAssistant, device_id: str) -> list[str]:
|
2020-01-30 00:19:13 +00:00
|
|
|
"""Return all automations that reference the device."""
|
|
|
|
if DOMAIN not in hass.data:
|
|
|
|
return []
|
|
|
|
|
|
|
|
component = hass.data[DOMAIN]
|
|
|
|
|
2020-05-01 14:37:25 +00:00
|
|
|
return [
|
|
|
|
automation_entity.entity_id
|
|
|
|
for automation_entity in component.entities
|
|
|
|
if device_id in automation_entity.referenced_devices
|
|
|
|
]
|
2020-01-30 00:19:13 +00:00
|
|
|
|
|
|
|
|
|
|
|
@callback
|
2021-03-17 22:34:25 +00:00
|
|
|
def devices_in_automation(hass: HomeAssistant, entity_id: str) -> list[str]:
|
2020-01-30 00:19:13 +00:00
|
|
|
"""Return all devices in a scene."""
|
|
|
|
if DOMAIN not in hass.data:
|
|
|
|
return []
|
|
|
|
|
|
|
|
component = hass.data[DOMAIN]
|
|
|
|
|
|
|
|
automation_entity = component.get_entity(entity_id)
|
|
|
|
|
|
|
|
if automation_entity is None:
|
|
|
|
return []
|
|
|
|
|
2020-02-05 15:52:21 +00:00
|
|
|
return list(automation_entity.referenced_devices)
|
2020-01-30 00:19:13 +00:00
|
|
|
|
|
|
|
|
2018-09-04 19:16:24 +00:00
|
|
|
async def async_setup(hass, config):
|
2021-03-04 13:16:24 +00:00
|
|
|
"""Set up all automations."""
|
2021-03-10 22:42:13 +00:00
|
|
|
# Local import to avoid circular import
|
2020-11-12 10:58:28 +00:00
|
|
|
hass.data[DOMAIN] = component = EntityComponent(LOGGER, DOMAIN, hass)
|
2021-03-10 22:42:13 +00:00
|
|
|
|
2020-11-20 14:57:57 +00:00
|
|
|
# To register the automation blueprints
|
|
|
|
async_get_blueprints(hass)
|
|
|
|
|
2020-11-25 14:10:04 +00:00
|
|
|
if not await _async_process_config(hass, config, component):
|
|
|
|
await async_get_blueprints(hass).async_populate()
|
2015-09-14 05:25:42 +00:00
|
|
|
|
2020-01-23 01:46:12 +00:00
|
|
|
async def trigger_service_handler(entity, service_call):
|
2021-03-04 13:16:24 +00:00
|
|
|
"""Handle forced automation trigger, e.g. from frontend."""
|
2020-01-23 01:46:12 +00:00
|
|
|
await entity.async_trigger(
|
2021-03-22 07:22:32 +00:00
|
|
|
{**service_call.data[ATTR_VARIABLES], "trigger": {"platform": None}},
|
2020-01-23 01:46:12 +00:00
|
|
|
skip_condition=service_call.data[CONF_SKIP_CONDITION],
|
|
|
|
context=service_call.context,
|
|
|
|
)
|
|
|
|
|
|
|
|
component.async_register_entity_service(
|
|
|
|
SERVICE_TRIGGER,
|
|
|
|
{
|
|
|
|
vol.Optional(ATTR_VARIABLES, default={}): dict,
|
|
|
|
vol.Optional(CONF_SKIP_CONDITION, default=True): bool,
|
|
|
|
},
|
|
|
|
trigger_service_handler,
|
|
|
|
)
|
|
|
|
component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle")
|
|
|
|
component.async_register_entity_service(SERVICE_TURN_ON, {}, "async_turn_on")
|
2020-08-02 02:31:47 +00:00
|
|
|
component.async_register_entity_service(
|
|
|
|
SERVICE_TURN_OFF,
|
|
|
|
{vol.Optional(CONF_STOP_ACTIONS, default=DEFAULT_STOP_ACTIONS): cv.boolean},
|
|
|
|
"async_turn_off",
|
|
|
|
)
|
2015-09-14 05:25:42 +00:00
|
|
|
|
2018-09-04 19:16:24 +00:00
|
|
|
async def reload_service_handler(service_call):
|
2016-09-04 15:15:52 +00:00
|
|
|
"""Remove all automations and load new ones from config."""
|
2018-09-04 19:16:24 +00:00
|
|
|
conf = await component.async_prepare_reload()
|
2016-09-04 15:15:52 +00:00
|
|
|
if conf is None:
|
|
|
|
return
|
2020-11-02 14:00:13 +00:00
|
|
|
async_get_blueprints(hass).async_reset_cache()
|
2018-09-04 19:16:24 +00:00
|
|
|
await _async_process_config(hass, conf, component)
|
2020-06-01 22:19:00 +00:00
|
|
|
hass.bus.async_fire(EVENT_AUTOMATION_RELOADED, context=service_call.context)
|
2016-09-04 15:15:52 +00:00
|
|
|
|
2019-12-07 20:17:30 +00:00
|
|
|
async_register_admin_service(
|
2020-02-23 21:38:05 +00:00
|
|
|
hass, DOMAIN, SERVICE_RELOAD, reload_service_handler, schema=vol.Schema({})
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2016-10-04 05:39:27 +00:00
|
|
|
|
2015-09-15 05:05:40 +00:00
|
|
|
return True
|
2015-01-16 07:32:27 +00:00
|
|
|
|
|
|
|
|
2018-11-28 12:16:43 +00:00
|
|
|
class AutomationEntity(ToggleEntity, RestoreEntity):
|
2016-08-26 06:25:57 +00:00
|
|
|
"""Entity to show status of entity."""
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
automation_id,
|
|
|
|
name,
|
2020-02-05 15:52:21 +00:00
|
|
|
trigger_config,
|
2019-07-31 19:25:30 +00:00
|
|
|
cond_func,
|
2020-01-30 00:19:13 +00:00
|
|
|
action_script,
|
2019-07-31 19:25:30 +00:00
|
|
|
initial_state,
|
2020-09-10 18:41:42 +00:00
|
|
|
variables,
|
2021-02-08 09:50:38 +00:00
|
|
|
trigger_variables,
|
2021-03-04 13:16:24 +00:00
|
|
|
raw_config,
|
2019-07-31 19:25:30 +00:00
|
|
|
):
|
2016-08-26 06:25:57 +00:00
|
|
|
"""Initialize an automation entity."""
|
2017-05-10 01:44:00 +00:00
|
|
|
self._id = automation_id
|
2016-08-26 06:25:57 +00:00
|
|
|
self._name = name
|
2020-02-05 15:52:21 +00:00
|
|
|
self._trigger_config = trigger_config
|
2016-10-01 21:11:07 +00:00
|
|
|
self._async_detach_triggers = None
|
2016-08-26 06:25:57 +00:00
|
|
|
self._cond_func = cond_func
|
2020-01-30 00:19:13 +00:00
|
|
|
self.action_script = action_script
|
2020-08-02 02:31:47 +00:00
|
|
|
self.action_script.change_listener = self.async_write_ha_state
|
2017-03-01 04:33:19 +00:00
|
|
|
self._initial_state = initial_state
|
2019-06-08 06:08:22 +00:00
|
|
|
self._is_enabled = False
|
2021-03-17 22:34:25 +00:00
|
|
|
self._referenced_entities: set[str] | None = None
|
|
|
|
self._referenced_devices: set[str] | None = None
|
2020-11-12 10:58:28 +00:00
|
|
|
self._logger = LOGGER
|
2020-09-11 10:24:16 +00:00
|
|
|
self._variables: ScriptVariables = variables
|
2021-02-08 09:50:38 +00:00
|
|
|
self._trigger_variables: ScriptVariables = trigger_variables
|
2021-03-04 13:16:24 +00:00
|
|
|
self._raw_config = raw_config
|
2016-08-26 06:25:57 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def name(self):
|
|
|
|
"""Name of the automation."""
|
|
|
|
return self._name
|
|
|
|
|
2020-01-27 07:01:35 +00:00
|
|
|
@property
|
|
|
|
def unique_id(self):
|
|
|
|
"""Return unique ID."""
|
|
|
|
return self._id
|
|
|
|
|
2016-08-26 06:25:57 +00:00
|
|
|
@property
|
|
|
|
def should_poll(self):
|
|
|
|
"""No polling needed for automation entities."""
|
|
|
|
return False
|
|
|
|
|
|
|
|
@property
|
2021-03-21 09:38:24 +00:00
|
|
|
def extra_state_attributes(self):
|
2016-08-26 06:25:57 +00:00
|
|
|
"""Return the entity state attributes."""
|
2020-07-14 17:47:59 +00:00
|
|
|
attrs = {
|
2020-08-28 19:51:15 +00:00
|
|
|
ATTR_LAST_TRIGGERED: self.action_script.last_triggered,
|
2020-07-14 17:47:59 +00:00
|
|
|
ATTR_MODE: self.action_script.script_mode,
|
2020-08-02 02:31:47 +00:00
|
|
|
ATTR_CUR: self.action_script.runs,
|
2020-07-14 17:47:59 +00:00
|
|
|
}
|
|
|
|
if self.action_script.supports_max:
|
|
|
|
attrs[ATTR_MAX] = self.action_script.max_runs
|
2021-03-16 21:37:26 +00:00
|
|
|
if self._id is not None:
|
|
|
|
attrs[CONF_ID] = self._id
|
2020-07-14 17:47:59 +00:00
|
|
|
return attrs
|
2016-08-26 06:25:57 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def is_on(self) -> bool:
|
|
|
|
"""Return True if entity is on."""
|
2019-07-31 19:25:30 +00:00
|
|
|
return self._async_detach_triggers is not None or self._is_enabled
|
2016-08-26 06:25:57 +00:00
|
|
|
|
2020-02-05 15:52:21 +00:00
|
|
|
@property
|
|
|
|
def referenced_devices(self):
|
|
|
|
"""Return a set of referenced devices."""
|
|
|
|
if self._referenced_devices is not None:
|
|
|
|
return self._referenced_devices
|
|
|
|
|
|
|
|
referenced = self.action_script.referenced_devices
|
|
|
|
|
|
|
|
if self._cond_func is not None:
|
|
|
|
for conf in self._cond_func.config:
|
|
|
|
referenced |= condition.async_extract_devices(conf)
|
|
|
|
|
|
|
|
for conf in self._trigger_config:
|
|
|
|
device = _trigger_extract_device(conf)
|
|
|
|
if device is not None:
|
|
|
|
referenced.add(device)
|
|
|
|
|
|
|
|
self._referenced_devices = referenced
|
|
|
|
return referenced
|
|
|
|
|
|
|
|
@property
|
|
|
|
def referenced_entities(self):
|
|
|
|
"""Return a set of referenced entities."""
|
|
|
|
if self._referenced_entities is not None:
|
|
|
|
return self._referenced_entities
|
|
|
|
|
|
|
|
referenced = self.action_script.referenced_entities
|
|
|
|
|
|
|
|
if self._cond_func is not None:
|
|
|
|
for conf in self._cond_func.config:
|
|
|
|
referenced |= condition.async_extract_entities(conf)
|
|
|
|
|
|
|
|
for conf in self._trigger_config:
|
|
|
|
for entity_id in _trigger_extract_entities(conf):
|
|
|
|
referenced.add(entity_id)
|
|
|
|
|
|
|
|
self._referenced_entities = referenced
|
|
|
|
return referenced
|
|
|
|
|
2018-09-04 19:16:24 +00:00
|
|
|
async def async_added_to_hass(self) -> None:
|
2017-03-04 23:19:01 +00:00
|
|
|
"""Startup with initial state or previous state."""
|
2018-11-28 12:16:43 +00:00
|
|
|
await super().async_added_to_hass()
|
2019-06-08 19:48:37 +00:00
|
|
|
|
2020-07-22 15:55:49 +00:00
|
|
|
self._logger = logging.getLogger(
|
|
|
|
f"{__name__}.{split_entity_id(self.entity_id)[1]}"
|
|
|
|
)
|
|
|
|
self.action_script.update_logger(self._logger)
|
|
|
|
|
2019-06-08 19:48:37 +00:00
|
|
|
state = await self.async_get_last_state()
|
|
|
|
if state:
|
|
|
|
enable_automation = state.state == STATE_ON
|
2019-07-31 19:25:30 +00:00
|
|
|
last_triggered = state.attributes.get("last_triggered")
|
2019-07-11 03:42:38 +00:00
|
|
|
if last_triggered is not None:
|
2020-08-28 19:51:15 +00:00
|
|
|
self.action_script.last_triggered = parse_datetime(last_triggered)
|
2020-07-22 15:55:49 +00:00
|
|
|
self._logger.debug(
|
2019-07-31 19:25:30 +00:00
|
|
|
"Loaded automation %s with state %s from state "
|
|
|
|
" storage last state %s",
|
|
|
|
self.entity_id,
|
|
|
|
enable_automation,
|
|
|
|
state,
|
|
|
|
)
|
2019-06-08 19:48:37 +00:00
|
|
|
else:
|
|
|
|
enable_automation = DEFAULT_INITIAL_STATE
|
2020-07-22 15:55:49 +00:00
|
|
|
self._logger.debug(
|
2020-07-05 21:04:19 +00:00
|
|
|
"Automation %s not in state storage, state %s from default is used",
|
2019-07-31 19:25:30 +00:00
|
|
|
self.entity_id,
|
|
|
|
enable_automation,
|
|
|
|
)
|
2019-06-08 19:48:37 +00:00
|
|
|
|
2017-04-04 06:11:39 +00:00
|
|
|
if self._initial_state is not None:
|
|
|
|
enable_automation = self._initial_state
|
2020-07-22 15:55:49 +00:00
|
|
|
self._logger.debug(
|
2019-07-31 19:25:30 +00:00
|
|
|
"Automation %s initial state %s overridden from "
|
|
|
|
"config initial_state",
|
|
|
|
self.entity_id,
|
|
|
|
enable_automation,
|
|
|
|
)
|
2017-03-24 22:52:14 +00:00
|
|
|
|
2019-06-08 06:08:22 +00:00
|
|
|
if enable_automation:
|
2018-09-04 19:16:24 +00:00
|
|
|
await self.async_enable()
|
2017-03-24 22:52:14 +00:00
|
|
|
|
2019-09-20 15:23:34 +00:00
|
|
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
2016-10-01 21:11:07 +00:00
|
|
|
"""Turn the entity on and update the state."""
|
2018-09-04 19:16:24 +00:00
|
|
|
await self.async_enable()
|
2016-08-26 06:25:57 +00:00
|
|
|
|
2019-09-20 15:23:34 +00:00
|
|
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
2016-08-26 06:25:57 +00:00
|
|
|
"""Turn the entity off."""
|
2020-08-02 02:31:47 +00:00
|
|
|
if CONF_STOP_ACTIONS in kwargs:
|
|
|
|
await self.async_disable(kwargs[CONF_STOP_ACTIONS])
|
|
|
|
else:
|
|
|
|
await self.async_disable()
|
2016-10-01 08:22:13 +00:00
|
|
|
|
2020-09-10 18:41:42 +00:00
|
|
|
async def async_trigger(self, run_variables, context=None, skip_condition=False):
|
2016-10-04 05:39:27 +00:00
|
|
|
"""Trigger automation.
|
2016-08-26 06:25:57 +00:00
|
|
|
|
2016-10-04 05:39:27 +00:00
|
|
|
This method is a coroutine.
|
|
|
|
"""
|
2021-03-04 13:16:24 +00:00
|
|
|
reason = ""
|
|
|
|
if "trigger" in run_variables and "description" in run_variables["trigger"]:
|
|
|
|
reason = f' by {run_variables["trigger"]["description"]}'
|
|
|
|
self._logger.debug("Automation triggered%s", reason)
|
|
|
|
|
2021-03-12 07:12:26 +00:00
|
|
|
# Create a new context referring to the old context.
|
|
|
|
parent_id = None if context is None else context.id
|
|
|
|
trigger_context = Context(parent_id=parent_id)
|
|
|
|
|
2021-03-04 13:16:24 +00:00
|
|
|
with trace_automation(
|
2021-03-12 07:12:26 +00:00
|
|
|
self.hass, self.unique_id, self._raw_config, trigger_context
|
2021-03-04 13:16:24 +00:00
|
|
|
) as automation_trace:
|
|
|
|
if self._variables:
|
|
|
|
try:
|
|
|
|
variables = self._variables.async_render(self.hass, run_variables)
|
|
|
|
except template.TemplateError as err:
|
|
|
|
self._logger.error("Error rendering variables: %s", err)
|
|
|
|
automation_trace.set_error(err)
|
|
|
|
return
|
|
|
|
else:
|
|
|
|
variables = run_variables
|
|
|
|
automation_trace.set_variables(variables)
|
|
|
|
|
2021-03-06 11:57:21 +00:00
|
|
|
# Prepare tracing the evaluation of the automation's conditions
|
|
|
|
automation_trace.set_condition_trace(trace_get())
|
|
|
|
|
2021-03-04 13:16:24 +00:00
|
|
|
if (
|
|
|
|
not skip_condition
|
|
|
|
and self._cond_func is not None
|
|
|
|
and not self._cond_func(variables)
|
|
|
|
):
|
|
|
|
self._logger.debug(
|
|
|
|
"Conditions not met, aborting automation. Condition summary: %s",
|
2021-03-06 11:57:21 +00:00
|
|
|
trace_get(clear=False),
|
2021-03-04 13:16:24 +00:00
|
|
|
)
|
2020-09-11 10:24:16 +00:00
|
|
|
return
|
2021-03-06 11:57:21 +00:00
|
|
|
|
|
|
|
# Prepare tracing the execution of the automation's actions
|
|
|
|
automation_trace.set_action_trace(trace_get())
|
2021-03-04 13:16:24 +00:00
|
|
|
|
|
|
|
self.async_set_context(trigger_context)
|
|
|
|
event_data = {
|
|
|
|
ATTR_NAME: self._name,
|
|
|
|
ATTR_ENTITY_ID: self.entity_id,
|
|
|
|
}
|
|
|
|
if "trigger" in variables and "description" in variables["trigger"]:
|
|
|
|
event_data[ATTR_SOURCE] = variables["trigger"]["description"]
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def started_action():
|
|
|
|
self.hass.bus.async_fire(
|
|
|
|
EVENT_AUTOMATION_TRIGGERED, event_data, context=trigger_context
|
|
|
|
)
|
2020-01-30 00:19:13 +00:00
|
|
|
|
2021-03-04 13:16:24 +00:00
|
|
|
try:
|
2021-03-06 11:57:21 +00:00
|
|
|
with trace_path("action"):
|
2021-03-04 13:16:24 +00:00
|
|
|
await self.action_script.async_run(
|
|
|
|
variables, trigger_context, started_action
|
|
|
|
)
|
|
|
|
except (vol.Invalid, HomeAssistantError) as err:
|
|
|
|
self._logger.error(
|
|
|
|
"Error while executing automation %s: %s",
|
|
|
|
self.entity_id,
|
|
|
|
err,
|
|
|
|
)
|
|
|
|
automation_trace.set_error(err)
|
|
|
|
except Exception as err: # pylint: disable=broad-except
|
|
|
|
self._logger.exception("While executing automation %s", self.entity_id)
|
|
|
|
automation_trace.set_error(err)
|
2020-01-30 00:19:13 +00:00
|
|
|
|
2018-09-04 19:16:24 +00:00
|
|
|
async def async_will_remove_from_hass(self):
|
2020-01-05 12:09:17 +00:00
|
|
|
"""Remove listeners when removing automation from Home Assistant."""
|
2018-11-28 12:16:43 +00:00
|
|
|
await super().async_will_remove_from_hass()
|
2019-06-08 06:08:22 +00:00
|
|
|
await self.async_disable()
|
2016-09-04 15:15:52 +00:00
|
|
|
|
2018-09-04 19:16:24 +00:00
|
|
|
async def async_enable(self):
|
2016-10-04 05:39:27 +00:00
|
|
|
"""Enable this automation entity.
|
|
|
|
|
|
|
|
This method is a coroutine.
|
|
|
|
"""
|
2019-06-08 06:08:22 +00:00
|
|
|
if self._is_enabled:
|
2016-10-01 21:11:07 +00:00
|
|
|
return
|
|
|
|
|
2019-06-08 06:08:22 +00:00
|
|
|
self._is_enabled = True
|
|
|
|
|
|
|
|
# HomeAssistant is starting up
|
|
|
|
if self.hass.state != CoreState.not_running:
|
2020-04-24 21:13:39 +00:00
|
|
|
self._async_detach_triggers = await self._async_attach_triggers(False)
|
2019-06-08 06:08:22 +00:00
|
|
|
self.async_write_ha_state()
|
|
|
|
return
|
|
|
|
|
|
|
|
async def async_enable_automation(event):
|
|
|
|
"""Start automation on startup."""
|
|
|
|
# Don't do anything if no longer enabled or already attached
|
2019-07-31 19:25:30 +00:00
|
|
|
if not self._is_enabled or self._async_detach_triggers is not None:
|
2019-06-08 06:08:22 +00:00
|
|
|
return
|
|
|
|
|
2020-04-24 21:13:39 +00:00
|
|
|
self._async_detach_triggers = await self._async_attach_triggers(True)
|
2019-06-08 06:08:22 +00:00
|
|
|
|
|
|
|
self.hass.bus.async_listen_once(
|
2020-04-24 21:13:39 +00:00
|
|
|
EVENT_HOMEASSISTANT_STARTED, async_enable_automation
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2019-06-08 06:08:22 +00:00
|
|
|
self.async_write_ha_state()
|
|
|
|
|
2020-08-02 02:31:47 +00:00
|
|
|
async def async_disable(self, stop_actions=DEFAULT_STOP_ACTIONS):
|
2019-06-08 06:08:22 +00:00
|
|
|
"""Disable the automation entity."""
|
2020-08-02 02:31:47 +00:00
|
|
|
if not self._is_enabled and not self.action_script.runs:
|
2019-06-08 06:08:22 +00:00
|
|
|
return
|
|
|
|
|
|
|
|
self._is_enabled = False
|
|
|
|
|
|
|
|
if self._async_detach_triggers is not None:
|
|
|
|
self._async_detach_triggers()
|
|
|
|
self._async_detach_triggers = None
|
|
|
|
|
2020-08-02 02:31:47 +00:00
|
|
|
if stop_actions:
|
|
|
|
await self.action_script.async_stop()
|
2020-07-25 10:19:55 +00:00
|
|
|
|
2019-06-08 06:08:22 +00:00
|
|
|
self.async_write_ha_state()
|
2016-10-01 21:11:07 +00:00
|
|
|
|
2020-04-24 21:13:39 +00:00
|
|
|
async def _async_attach_triggers(
|
|
|
|
self, home_assistant_start: bool
|
2021-03-17 22:34:25 +00:00
|
|
|
) -> Callable[[], None] | None:
|
2020-02-05 15:52:21 +00:00
|
|
|
"""Set up the triggers."""
|
|
|
|
|
2021-01-20 21:13:21 +00:00
|
|
|
def log_cb(level, msg, **kwargs):
|
|
|
|
self._logger.log(level, "%s %s", msg, self._name, **kwargs)
|
2020-08-17 16:54:56 +00:00
|
|
|
|
2021-02-08 09:50:38 +00:00
|
|
|
variables = None
|
|
|
|
if self._trigger_variables:
|
|
|
|
try:
|
|
|
|
variables = self._trigger_variables.async_render(
|
2021-03-15 14:11:41 +00:00
|
|
|
self.hass, None, limited=True
|
2021-02-08 09:50:38 +00:00
|
|
|
)
|
|
|
|
except template.TemplateError as err:
|
|
|
|
self._logger.error("Error rendering trigger variables: %s", err)
|
|
|
|
return None
|
|
|
|
|
2020-08-17 16:54:56 +00:00
|
|
|
return await async_initialize_triggers(
|
2021-03-15 14:11:41 +00:00
|
|
|
self.hass,
|
2020-08-17 16:54:56 +00:00
|
|
|
self._trigger_config,
|
|
|
|
self.async_trigger,
|
|
|
|
DOMAIN,
|
|
|
|
self._name,
|
|
|
|
log_cb,
|
|
|
|
home_assistant_start,
|
2021-02-08 09:50:38 +00:00
|
|
|
variables,
|
2020-08-17 16:54:56 +00:00
|
|
|
)
|
2020-02-05 15:52:21 +00:00
|
|
|
|
2016-09-04 15:15:52 +00:00
|
|
|
|
2020-11-02 14:00:13 +00:00
|
|
|
async def _async_process_config(
|
|
|
|
hass: HomeAssistant,
|
2021-03-17 22:34:25 +00:00
|
|
|
config: dict[str, Any],
|
2020-11-02 14:00:13 +00:00
|
|
|
component: EntityComponent,
|
2020-11-25 14:10:04 +00:00
|
|
|
) -> bool:
|
2016-10-04 05:39:27 +00:00
|
|
|
"""Process config and add automations.
|
|
|
|
|
2020-11-25 14:10:04 +00:00
|
|
|
Returns if blueprints were used.
|
2016-10-04 05:39:27 +00:00
|
|
|
"""
|
2016-10-01 16:19:20 +00:00
|
|
|
entities = []
|
2020-11-25 14:10:04 +00:00
|
|
|
blueprints_used = False
|
2016-09-04 15:15:52 +00:00
|
|
|
|
|
|
|
for config_key in extract_domain_configs(config, DOMAIN):
|
2021-03-17 22:34:25 +00:00
|
|
|
conf: list[dict[str, Any] | blueprint.BlueprintInputs] = config[ # type: ignore
|
2020-11-02 14:00:13 +00:00
|
|
|
config_key
|
|
|
|
]
|
2016-09-04 15:15:52 +00:00
|
|
|
|
|
|
|
for list_no, config_block in enumerate(conf):
|
2021-03-04 13:16:24 +00:00
|
|
|
raw_config = None
|
2020-11-02 14:00:13 +00:00
|
|
|
if isinstance(config_block, blueprint.BlueprintInputs): # type: ignore
|
2020-11-25 14:10:04 +00:00
|
|
|
blueprints_used = True
|
2020-11-02 14:00:13 +00:00
|
|
|
blueprint_inputs = config_block
|
|
|
|
|
|
|
|
try:
|
2021-03-04 13:16:24 +00:00
|
|
|
raw_config = blueprint_inputs.async_substitute()
|
2020-11-02 14:00:13 +00:00
|
|
|
config_block = cast(
|
|
|
|
Dict[str, Any],
|
2021-03-04 13:16:24 +00:00
|
|
|
await async_validate_config_item(hass, raw_config),
|
2020-11-02 14:00:13 +00:00
|
|
|
)
|
|
|
|
except vol.Invalid as err:
|
2020-11-12 10:58:28 +00:00
|
|
|
LOGGER.error(
|
2020-11-02 14:00:13 +00:00
|
|
|
"Blueprint %s generated invalid automation with inputs %s: %s",
|
|
|
|
blueprint_inputs.blueprint.name,
|
|
|
|
blueprint_inputs.inputs,
|
|
|
|
humanize_error(config_block, err),
|
|
|
|
)
|
|
|
|
continue
|
2021-03-04 13:16:24 +00:00
|
|
|
else:
|
|
|
|
raw_config = cast(AutomationConfig, config_block).raw_config
|
2020-11-02 14:00:13 +00:00
|
|
|
|
2017-05-10 01:44:00 +00:00
|
|
|
automation_id = config_block.get(CONF_ID)
|
2019-08-23 16:53:33 +00:00
|
|
|
name = config_block.get(CONF_ALIAS) or f"{config_key} {list_no}"
|
2016-09-04 15:15:52 +00:00
|
|
|
|
2017-04-04 06:11:39 +00:00
|
|
|
initial_state = config_block.get(CONF_INITIAL_STATE)
|
2016-09-20 06:39:07 +00:00
|
|
|
|
2020-07-05 14:25:15 +00:00
|
|
|
action_script = Script(
|
|
|
|
hass,
|
|
|
|
config_block[CONF_ACTION],
|
|
|
|
name,
|
2020-08-12 16:39:05 +00:00
|
|
|
DOMAIN,
|
|
|
|
running_description="automation actions",
|
2020-07-05 14:25:15 +00:00
|
|
|
script_mode=config_block[CONF_MODE],
|
2020-07-11 00:00:57 +00:00
|
|
|
max_runs=config_block[CONF_MAX],
|
2020-09-02 09:05:14 +00:00
|
|
|
max_exceeded=config_block[CONF_MAX_EXCEEDED],
|
2020-11-12 10:58:28 +00:00
|
|
|
logger=LOGGER,
|
2020-09-10 18:41:42 +00:00
|
|
|
# We don't pass variables here
|
|
|
|
# Automation will already render them to use them in the condition
|
|
|
|
# and so will pass them on to the script.
|
2020-02-24 22:56:00 +00:00
|
|
|
)
|
2016-09-04 15:15:52 +00:00
|
|
|
|
|
|
|
if CONF_CONDITION in config_block:
|
2021-02-22 05:34:45 +00:00
|
|
|
cond_func = await _async_process_if(hass, name, config, config_block)
|
2016-09-04 15:15:52 +00:00
|
|
|
|
|
|
|
if cond_func is None:
|
|
|
|
continue
|
|
|
|
else:
|
2020-02-05 15:52:21 +00:00
|
|
|
cond_func = None
|
2019-07-31 19:25:30 +00:00
|
|
|
|
2021-02-08 09:50:38 +00:00
|
|
|
# Add trigger variables to variables
|
|
|
|
variables = None
|
|
|
|
if CONF_TRIGGER_VARIABLES in config_block:
|
|
|
|
variables = ScriptVariables(
|
|
|
|
dict(config_block[CONF_TRIGGER_VARIABLES].as_dict())
|
|
|
|
)
|
|
|
|
if CONF_VARIABLES in config_block:
|
|
|
|
if variables:
|
|
|
|
variables.variables.update(config_block[CONF_VARIABLES].as_dict())
|
|
|
|
else:
|
|
|
|
variables = config_block[CONF_VARIABLES]
|
|
|
|
|
2017-03-01 04:33:19 +00:00
|
|
|
entity = AutomationEntity(
|
2019-07-31 19:25:30 +00:00
|
|
|
automation_id,
|
|
|
|
name,
|
2020-02-05 15:52:21 +00:00
|
|
|
config_block[CONF_TRIGGER],
|
2019-07-31 19:25:30 +00:00
|
|
|
cond_func,
|
2020-01-30 00:19:13 +00:00
|
|
|
action_script,
|
2019-07-31 19:25:30 +00:00
|
|
|
initial_state,
|
2021-02-08 09:50:38 +00:00
|
|
|
variables,
|
|
|
|
config_block.get(CONF_TRIGGER_VARIABLES),
|
2021-03-04 13:16:24 +00:00
|
|
|
raw_config,
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2017-03-01 04:33:19 +00:00
|
|
|
|
2016-10-01 21:11:07 +00:00
|
|
|
entities.append(entity)
|
2016-09-04 15:15:52 +00:00
|
|
|
|
2016-11-06 17:26:40 +00:00
|
|
|
if entities:
|
2018-09-04 19:16:24 +00:00
|
|
|
await component.async_add_entities(entities)
|
2016-10-01 16:19:20 +00:00
|
|
|
|
2020-11-25 14:10:04 +00:00
|
|
|
return blueprints_used
|
|
|
|
|
2016-08-26 06:25:57 +00:00
|
|
|
|
2021-02-22 05:34:45 +00:00
|
|
|
async def _async_process_if(hass, name, config, p_config):
|
2016-03-07 19:20:07 +00:00
|
|
|
"""Process if checks."""
|
2020-02-05 15:52:21 +00:00
|
|
|
if_configs = p_config[CONF_CONDITION]
|
2015-09-15 15:56:06 +00:00
|
|
|
|
2015-09-15 05:51:28 +00:00
|
|
|
checks = []
|
2015-09-14 05:25:42 +00:00
|
|
|
for if_config in if_configs:
|
2016-04-28 10:03:57 +00:00
|
|
|
try:
|
2019-09-05 14:49:32 +00:00
|
|
|
checks.append(await condition.async_from_config(hass, if_config, False))
|
2016-04-28 10:03:57 +00:00
|
|
|
except HomeAssistantError as ex:
|
2020-11-12 10:58:28 +00:00
|
|
|
LOGGER.warning("Invalid condition: %s", ex)
|
2016-10-01 08:22:13 +00:00
|
|
|
return None
|
|
|
|
|
|
|
|
def if_action(variables=None):
|
|
|
|
"""AND all conditions."""
|
2021-02-19 12:15:30 +00:00
|
|
|
errors = []
|
2021-02-21 13:54:36 +00:00
|
|
|
for index, check in enumerate(checks):
|
2021-02-19 12:15:30 +00:00
|
|
|
try:
|
2021-03-06 11:57:21 +00:00
|
|
|
with trace_path(["condition", str(index)]):
|
2021-03-04 13:16:24 +00:00
|
|
|
if not check(hass, variables):
|
|
|
|
return False
|
2021-02-19 12:15:30 +00:00
|
|
|
except ConditionError as ex:
|
2021-02-21 13:54:36 +00:00
|
|
|
errors.append(
|
|
|
|
ConditionErrorIndex(
|
|
|
|
"condition", index=index, total=len(checks), error=ex
|
|
|
|
)
|
|
|
|
)
|
2021-02-19 12:15:30 +00:00
|
|
|
|
|
|
|
if errors:
|
2021-02-21 13:54:36 +00:00
|
|
|
LOGGER.warning(
|
2021-02-22 05:34:45 +00:00
|
|
|
"Error evaluating condition in '%s':\n%s",
|
|
|
|
name,
|
2021-02-21 13:54:36 +00:00
|
|
|
ConditionErrorContainer("condition", errors=errors),
|
|
|
|
)
|
2021-02-08 09:47:57 +00:00
|
|
|
return False
|
2015-09-15 05:51:28 +00:00
|
|
|
|
2021-02-19 12:15:30 +00:00
|
|
|
return True
|
|
|
|
|
2020-02-05 15:52:21 +00:00
|
|
|
if_action.config = if_configs
|
2015-09-14 05:25:42 +00:00
|
|
|
|
2020-02-05 15:52:21 +00:00
|
|
|
return if_action
|
2015-09-15 05:05:40 +00:00
|
|
|
|
2016-10-04 05:39:27 +00:00
|
|
|
|
2020-02-05 15:52:21 +00:00
|
|
|
@callback
|
2021-03-17 22:34:25 +00:00
|
|
|
def _trigger_extract_device(trigger_conf: dict) -> str | None:
|
2020-02-05 15:52:21 +00:00
|
|
|
"""Extract devices from a trigger config."""
|
|
|
|
if trigger_conf[CONF_PLATFORM] != "device":
|
|
|
|
return None
|
2016-08-26 06:25:57 +00:00
|
|
|
|
2020-02-05 15:52:21 +00:00
|
|
|
return trigger_conf[CONF_DEVICE_ID]
|
2015-09-14 05:25:42 +00:00
|
|
|
|
2016-08-26 06:25:57 +00:00
|
|
|
|
2020-02-05 15:52:21 +00:00
|
|
|
@callback
|
2021-03-17 22:34:25 +00:00
|
|
|
def _trigger_extract_entities(trigger_conf: dict) -> list[str]:
|
2020-02-05 15:52:21 +00:00
|
|
|
"""Extract entities from a trigger config."""
|
|
|
|
if trigger_conf[CONF_PLATFORM] in ("state", "numeric_state"):
|
|
|
|
return trigger_conf[CONF_ENTITY_ID]
|
2016-08-26 06:25:57 +00:00
|
|
|
|
2020-02-05 15:52:21 +00:00
|
|
|
if trigger_conf[CONF_PLATFORM] == "zone":
|
|
|
|
return trigger_conf[CONF_ENTITY_ID] + [trigger_conf[CONF_ZONE]]
|
2016-08-26 06:25:57 +00:00
|
|
|
|
2020-02-05 15:52:21 +00:00
|
|
|
if trigger_conf[CONF_PLATFORM] == "geo_location":
|
|
|
|
return [trigger_conf[CONF_ZONE]]
|
2016-08-26 06:25:57 +00:00
|
|
|
|
2020-02-05 15:52:21 +00:00
|
|
|
if trigger_conf[CONF_PLATFORM] == "sun":
|
2020-02-06 11:55:11 +00:00
|
|
|
return ["sun.sun"]
|
2016-08-26 06:25:57 +00:00
|
|
|
|
2020-02-05 15:52:21 +00:00
|
|
|
return []
|