2016-04-28 10:03:57 +00:00
|
|
|
"""Offer reusable conditions."""
|
2019-09-05 14:49:32 +00:00
|
|
|
import asyncio
|
2020-01-30 00:19:13 +00:00
|
|
|
from collections import deque
|
2019-01-20 23:03:12 +00:00
|
|
|
from datetime import datetime, timedelta
|
2016-10-01 08:22:13 +00:00
|
|
|
import functools as ft
|
2016-04-28 10:03:57 +00:00
|
|
|
import logging
|
2020-09-06 22:36:01 +00:00
|
|
|
import re
|
2016-04-28 10:03:57 +00:00
|
|
|
import sys
|
2020-08-19 18:01:27 +00:00
|
|
|
from typing import Any, Callable, Container, List, Optional, Set, Union, cast
|
2016-04-28 10:03:57 +00:00
|
|
|
|
2017-05-09 07:03:34 +00:00
|
|
|
from homeassistant.components import zone as zone_cmp
|
2019-10-02 22:58:14 +00:00
|
|
|
from homeassistant.components.device_automation import (
|
|
|
|
async_get_device_automation_platform,
|
|
|
|
)
|
2016-04-28 10:03:57 +00:00
|
|
|
from homeassistant.const import (
|
2019-07-31 19:25:30 +00:00
|
|
|
ATTR_GPS_ACCURACY,
|
|
|
|
ATTR_LATITUDE,
|
|
|
|
ATTR_LONGITUDE,
|
2019-10-02 22:58:14 +00:00
|
|
|
CONF_ABOVE,
|
|
|
|
CONF_AFTER,
|
2020-08-19 18:01:27 +00:00
|
|
|
CONF_ATTRIBUTE,
|
2019-10-02 22:58:14 +00:00
|
|
|
CONF_BEFORE,
|
|
|
|
CONF_BELOW,
|
|
|
|
CONF_CONDITION,
|
2020-01-30 00:19:13 +00:00
|
|
|
CONF_DEVICE_ID,
|
2019-09-24 21:57:05 +00:00
|
|
|
CONF_DOMAIN,
|
2019-07-31 19:25:30 +00:00
|
|
|
CONF_ENTITY_ID,
|
|
|
|
CONF_STATE,
|
2019-10-02 22:58:14 +00:00
|
|
|
CONF_VALUE_TEMPLATE,
|
2019-07-31 19:25:30 +00:00
|
|
|
CONF_WEEKDAY,
|
2019-10-02 22:58:14 +00:00
|
|
|
CONF_ZONE,
|
2019-07-31 19:25:30 +00:00
|
|
|
STATE_UNAVAILABLE,
|
|
|
|
STATE_UNKNOWN,
|
2019-10-02 22:58:14 +00:00
|
|
|
SUN_EVENT_SUNRISE,
|
|
|
|
SUN_EVENT_SUNSET,
|
|
|
|
WEEKDAYS,
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2020-01-30 00:19:13 +00:00
|
|
|
from homeassistant.core import HomeAssistant, State, callback
|
2021-02-08 09:47:57 +00:00
|
|
|
from homeassistant.exceptions import ConditionError, HomeAssistantError, TemplateError
|
2016-04-28 10:03:57 +00:00
|
|
|
import homeassistant.helpers.config_validation as cv
|
2017-05-09 07:03:34 +00:00
|
|
|
from homeassistant.helpers.sun import get_astral_event_date
|
2019-12-09 15:42:10 +00:00
|
|
|
from homeassistant.helpers.template import Template
|
|
|
|
from homeassistant.helpers.typing import ConfigType, TemplateVarsType
|
2018-03-11 17:01:12 +00:00
|
|
|
from homeassistant.util.async_ import run_callback_threadsafe
|
2019-12-09 15:42:10 +00:00
|
|
|
import homeassistant.util.dt as dt_util
|
2016-04-28 10:03:57 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
FROM_CONFIG_FORMAT = "{}_from_config"
|
|
|
|
ASYNC_FROM_CONFIG_FORMAT = "async_{}_from_config"
|
2016-04-28 10:03:57 +00:00
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2020-09-06 22:36:01 +00:00
|
|
|
INPUT_ENTITY_ID = re.compile(
|
|
|
|
r"^input_(?:select|text|number|boolean|datetime)\.(?!.+__)(?!_)[\da-z_]+(?<!_)$"
|
|
|
|
)
|
|
|
|
|
2019-09-24 21:57:05 +00:00
|
|
|
ConditionCheckerType = Callable[[HomeAssistant, TemplateVarsType], bool]
|
|
|
|
|
2016-10-01 06:11:57 +00:00
|
|
|
|
2019-09-05 14:49:32 +00:00
|
|
|
async def async_from_config(
|
2020-09-06 14:55:06 +00:00
|
|
|
hass: HomeAssistant,
|
|
|
|
config: Union[ConfigType, Template],
|
|
|
|
config_validation: bool = True,
|
2019-09-24 21:57:05 +00:00
|
|
|
) -> ConditionCheckerType:
|
2016-10-01 06:11:57 +00:00
|
|
|
"""Turn a condition configuration into a method.
|
|
|
|
|
|
|
|
Should be run on the event loop.
|
|
|
|
"""
|
2020-09-06 14:55:06 +00:00
|
|
|
if isinstance(config, Template):
|
|
|
|
# We got a condition template, wrap it in a configuration to pass along.
|
|
|
|
config = {
|
|
|
|
CONF_CONDITION: "template",
|
|
|
|
CONF_VALUE_TEMPLATE: config,
|
|
|
|
}
|
|
|
|
|
|
|
|
condition = config.get(CONF_CONDITION)
|
2016-10-01 06:11:57 +00:00
|
|
|
for fmt in (ASYNC_FROM_CONFIG_FORMAT, FROM_CONFIG_FORMAT):
|
2020-09-06 14:55:06 +00:00
|
|
|
factory = getattr(sys.modules[__name__], fmt.format(condition), None)
|
2016-10-01 06:11:57 +00:00
|
|
|
|
|
|
|
if factory:
|
|
|
|
break
|
2016-04-28 10:03:57 +00:00
|
|
|
|
|
|
|
if factory is None:
|
2020-09-06 14:55:06 +00:00
|
|
|
raise HomeAssistantError(f'Invalid condition "{condition}" specified {config}')
|
2016-04-28 10:03:57 +00:00
|
|
|
|
2019-09-05 14:49:32 +00:00
|
|
|
# Check for partials to properly determine if coroutine function
|
|
|
|
check_factory = factory
|
|
|
|
while isinstance(check_factory, ft.partial):
|
|
|
|
check_factory = check_factory.func
|
2016-04-28 10:03:57 +00:00
|
|
|
|
2019-09-05 14:49:32 +00:00
|
|
|
if asyncio.iscoroutinefunction(check_factory):
|
2019-09-24 21:57:05 +00:00
|
|
|
return cast(
|
|
|
|
ConditionCheckerType, await factory(hass, config, config_validation)
|
|
|
|
)
|
|
|
|
return cast(ConditionCheckerType, factory(config, config_validation))
|
2016-10-01 06:11:57 +00:00
|
|
|
|
|
|
|
|
2019-09-05 14:49:32 +00:00
|
|
|
async def async_and_from_config(
|
|
|
|
hass: HomeAssistant, config: ConfigType, config_validation: bool = True
|
2019-09-24 21:57:05 +00:00
|
|
|
) -> ConditionCheckerType:
|
2016-04-28 10:03:57 +00:00
|
|
|
"""Create multi condition matcher using 'AND'."""
|
|
|
|
if config_validation:
|
|
|
|
config = cv.AND_CONDITION_SCHEMA(config)
|
2019-09-05 14:49:32 +00:00
|
|
|
checks = [
|
|
|
|
await async_from_config(hass, entry, False) for entry in config["conditions"]
|
|
|
|
]
|
2016-04-28 10:03:57 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
def if_and_condition(
|
|
|
|
hass: HomeAssistant, variables: TemplateVarsType = None
|
|
|
|
) -> bool:
|
2016-04-28 10:03:57 +00:00
|
|
|
"""Test and condition."""
|
2016-10-01 06:11:57 +00:00
|
|
|
try:
|
|
|
|
for check in checks:
|
2016-05-03 05:05:09 +00:00
|
|
|
if not check(hass, variables):
|
|
|
|
return False
|
2016-10-01 06:11:57 +00:00
|
|
|
except Exception as ex: # pylint: disable=broad-except
|
2017-05-02 16:18:47 +00:00
|
|
|
_LOGGER.warning("Error during and-condition: %s", ex)
|
2016-10-01 06:11:57 +00:00
|
|
|
return False
|
2016-05-03 05:05:09 +00:00
|
|
|
|
|
|
|
return True
|
2016-04-28 10:03:57 +00:00
|
|
|
|
|
|
|
return if_and_condition
|
|
|
|
|
|
|
|
|
2019-09-05 14:49:32 +00:00
|
|
|
async def async_or_from_config(
|
|
|
|
hass: HomeAssistant, config: ConfigType, config_validation: bool = True
|
2019-09-24 21:57:05 +00:00
|
|
|
) -> ConditionCheckerType:
|
2016-04-28 10:03:57 +00:00
|
|
|
"""Create multi condition matcher using 'OR'."""
|
|
|
|
if config_validation:
|
|
|
|
config = cv.OR_CONDITION_SCHEMA(config)
|
2019-09-05 14:49:32 +00:00
|
|
|
checks = [
|
|
|
|
await async_from_config(hass, entry, False) for entry in config["conditions"]
|
|
|
|
]
|
2016-04-28 10:03:57 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
def if_or_condition(
|
|
|
|
hass: HomeAssistant, variables: TemplateVarsType = None
|
|
|
|
) -> bool:
|
2016-04-28 10:03:57 +00:00
|
|
|
"""Test and condition."""
|
2016-10-01 06:11:57 +00:00
|
|
|
try:
|
|
|
|
for check in checks:
|
2016-05-03 05:05:09 +00:00
|
|
|
if check(hass, variables):
|
|
|
|
return True
|
2016-10-01 06:11:57 +00:00
|
|
|
except Exception as ex: # pylint: disable=broad-except
|
2017-05-02 16:18:47 +00:00
|
|
|
_LOGGER.warning("Error during or-condition: %s", ex)
|
2016-05-03 05:05:09 +00:00
|
|
|
|
|
|
|
return False
|
2016-04-28 10:03:57 +00:00
|
|
|
|
|
|
|
return if_or_condition
|
|
|
|
|
|
|
|
|
2020-04-24 16:40:23 +00:00
|
|
|
async def async_not_from_config(
|
|
|
|
hass: HomeAssistant, config: ConfigType, config_validation: bool = True
|
|
|
|
) -> ConditionCheckerType:
|
|
|
|
"""Create multi condition matcher using 'NOT'."""
|
|
|
|
if config_validation:
|
|
|
|
config = cv.NOT_CONDITION_SCHEMA(config)
|
|
|
|
checks = [
|
|
|
|
await async_from_config(hass, entry, False) for entry in config["conditions"]
|
|
|
|
]
|
|
|
|
|
|
|
|
def if_not_condition(
|
|
|
|
hass: HomeAssistant, variables: TemplateVarsType = None
|
|
|
|
) -> bool:
|
|
|
|
"""Test not condition."""
|
|
|
|
try:
|
|
|
|
for check in checks:
|
|
|
|
if check(hass, variables):
|
|
|
|
return False
|
|
|
|
except Exception as ex: # pylint: disable=broad-except
|
|
|
|
_LOGGER.warning("Error during not-condition: %s", ex)
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
return if_not_condition
|
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
def numeric_state(
|
|
|
|
hass: HomeAssistant,
|
|
|
|
entity: Union[None, str, State],
|
2020-09-06 18:04:07 +00:00
|
|
|
below: Optional[Union[float, str]] = None,
|
|
|
|
above: Optional[Union[float, str]] = None,
|
2019-07-31 19:25:30 +00:00
|
|
|
value_template: Optional[Template] = None,
|
|
|
|
variables: TemplateVarsType = None,
|
|
|
|
) -> bool:
|
2016-04-28 10:03:57 +00:00
|
|
|
"""Test a numeric state condition."""
|
2020-04-17 18:33:58 +00:00
|
|
|
return run_callback_threadsafe(
|
|
|
|
hass.loop,
|
|
|
|
async_numeric_state,
|
|
|
|
hass,
|
|
|
|
entity,
|
|
|
|
below,
|
|
|
|
above,
|
|
|
|
value_template,
|
|
|
|
variables,
|
|
|
|
).result()
|
2019-07-31 19:25:30 +00:00
|
|
|
|
|
|
|
|
|
|
|
def async_numeric_state(
|
|
|
|
hass: HomeAssistant,
|
|
|
|
entity: Union[None, str, State],
|
2020-09-06 18:04:07 +00:00
|
|
|
below: Optional[Union[float, str]] = None,
|
|
|
|
above: Optional[Union[float, str]] = None,
|
2019-07-31 19:25:30 +00:00
|
|
|
value_template: Optional[Template] = None,
|
|
|
|
variables: TemplateVarsType = None,
|
2020-08-19 18:01:27 +00:00
|
|
|
attribute: Optional[str] = None,
|
2019-07-31 19:25:30 +00:00
|
|
|
) -> bool:
|
2016-09-30 19:57:24 +00:00
|
|
|
"""Test a numeric state condition."""
|
2021-02-08 09:47:57 +00:00
|
|
|
if entity is None:
|
|
|
|
raise ConditionError("No entity specified")
|
|
|
|
|
2016-04-28 10:03:57 +00:00
|
|
|
if isinstance(entity, str):
|
2021-02-08 09:47:57 +00:00
|
|
|
entity_id = entity
|
2016-04-28 10:03:57 +00:00
|
|
|
entity = hass.states.get(entity)
|
|
|
|
|
2021-02-08 09:47:57 +00:00
|
|
|
if entity is None:
|
|
|
|
raise ConditionError(f"Unknown entity {entity_id}")
|
|
|
|
else:
|
|
|
|
entity_id = entity.entity_id
|
|
|
|
|
|
|
|
if attribute is not None and attribute not in entity.attributes:
|
|
|
|
raise ConditionError(
|
|
|
|
f"Attribute '{attribute}' (of entity {entity_id}) does not exist"
|
|
|
|
)
|
2016-04-28 10:03:57 +00:00
|
|
|
|
2020-08-19 18:01:27 +00:00
|
|
|
value: Any = None
|
2016-04-28 10:03:57 +00:00
|
|
|
if value_template is None:
|
2020-08-19 18:01:27 +00:00
|
|
|
if attribute is None:
|
|
|
|
value = entity.state
|
|
|
|
else:
|
|
|
|
value = entity.attributes.get(attribute)
|
2016-04-28 10:03:57 +00:00
|
|
|
else:
|
|
|
|
variables = dict(variables or {})
|
2019-07-31 19:25:30 +00:00
|
|
|
variables["state"] = entity
|
2016-04-28 10:03:57 +00:00
|
|
|
try:
|
2016-09-30 19:57:24 +00:00
|
|
|
value = value_template.async_render(variables)
|
2016-04-28 10:03:57 +00:00
|
|
|
except TemplateError as ex:
|
2021-02-08 09:47:57 +00:00
|
|
|
raise ConditionError(f"Template error: {ex}") from ex
|
2016-04-28 10:03:57 +00:00
|
|
|
|
2018-02-20 16:02:27 +00:00
|
|
|
if value in (STATE_UNAVAILABLE, STATE_UNKNOWN):
|
2021-02-08 09:47:57 +00:00
|
|
|
raise ConditionError("State is not available")
|
2018-02-20 16:02:27 +00:00
|
|
|
|
2016-04-28 10:03:57 +00:00
|
|
|
try:
|
2019-01-20 23:03:12 +00:00
|
|
|
fvalue = float(value)
|
2021-02-08 09:47:57 +00:00
|
|
|
except ValueError as ex:
|
|
|
|
raise ConditionError(
|
|
|
|
f"Entity {entity_id} state '{value}' cannot be processed as a number"
|
|
|
|
) from ex
|
2016-04-28 10:03:57 +00:00
|
|
|
|
2020-09-06 18:04:07 +00:00
|
|
|
if below is not None:
|
|
|
|
if isinstance(below, str):
|
|
|
|
below_entity = hass.states.get(below)
|
2021-02-08 09:47:57 +00:00
|
|
|
if not below_entity or below_entity.state in (
|
|
|
|
STATE_UNAVAILABLE,
|
|
|
|
STATE_UNKNOWN,
|
2020-09-06 18:04:07 +00:00
|
|
|
):
|
2021-02-08 09:47:57 +00:00
|
|
|
raise ConditionError(f"The below entity {below} is not available")
|
|
|
|
if fvalue >= float(below_entity.state):
|
2020-09-06 18:04:07 +00:00
|
|
|
return False
|
|
|
|
elif fvalue >= below:
|
|
|
|
return False
|
2016-04-28 10:03:57 +00:00
|
|
|
|
2020-09-06 18:04:07 +00:00
|
|
|
if above is not None:
|
|
|
|
if isinstance(above, str):
|
|
|
|
above_entity = hass.states.get(above)
|
2021-02-08 09:47:57 +00:00
|
|
|
if not above_entity or above_entity.state in (
|
|
|
|
STATE_UNAVAILABLE,
|
|
|
|
STATE_UNKNOWN,
|
2020-09-06 18:04:07 +00:00
|
|
|
):
|
2021-02-08 09:47:57 +00:00
|
|
|
raise ConditionError(f"The above entity {above} is not available")
|
|
|
|
if fvalue <= float(above_entity.state):
|
2020-09-06 18:04:07 +00:00
|
|
|
return False
|
|
|
|
elif fvalue <= above:
|
|
|
|
return False
|
2016-04-28 10:03:57 +00:00
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
def async_numeric_state_from_config(
|
|
|
|
config: ConfigType, config_validation: bool = True
|
2019-09-24 21:57:05 +00:00
|
|
|
) -> ConditionCheckerType:
|
2016-04-28 10:03:57 +00:00
|
|
|
"""Wrap action method with state based condition."""
|
|
|
|
if config_validation:
|
|
|
|
config = cv.NUMERIC_STATE_CONDITION_SCHEMA(config)
|
2020-06-15 20:54:19 +00:00
|
|
|
entity_ids = config.get(CONF_ENTITY_ID, [])
|
2020-08-19 18:01:27 +00:00
|
|
|
attribute = config.get(CONF_ATTRIBUTE)
|
2016-04-28 10:03:57 +00:00
|
|
|
below = config.get(CONF_BELOW)
|
|
|
|
above = config.get(CONF_ABOVE)
|
|
|
|
value_template = config.get(CONF_VALUE_TEMPLATE)
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
def if_numeric_state(
|
|
|
|
hass: HomeAssistant, variables: TemplateVarsType = None
|
|
|
|
) -> bool:
|
2016-04-28 10:03:57 +00:00
|
|
|
"""Test numeric state condition."""
|
2016-09-28 04:29:55 +00:00
|
|
|
if value_template is not None:
|
|
|
|
value_template.hass = hass
|
|
|
|
|
2020-06-15 20:54:19 +00:00
|
|
|
return all(
|
|
|
|
async_numeric_state(
|
2020-08-19 18:01:27 +00:00
|
|
|
hass, entity_id, below, above, value_template, variables, attribute
|
2020-06-15 20:54:19 +00:00
|
|
|
)
|
|
|
|
for entity_id in entity_ids
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2016-04-28 10:03:57 +00:00
|
|
|
|
|
|
|
return if_numeric_state
|
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
def state(
|
|
|
|
hass: HomeAssistant,
|
|
|
|
entity: Union[None, str, State],
|
2020-10-05 10:53:12 +00:00
|
|
|
req_state: Any,
|
2019-07-31 19:25:30 +00:00
|
|
|
for_period: Optional[timedelta] = None,
|
2020-08-19 18:01:27 +00:00
|
|
|
attribute: Optional[str] = None,
|
2019-07-31 19:25:30 +00:00
|
|
|
) -> bool:
|
2017-02-02 05:44:05 +00:00
|
|
|
"""Test if state matches requirements.
|
|
|
|
|
|
|
|
Async friendly.
|
|
|
|
"""
|
2016-04-28 10:03:57 +00:00
|
|
|
if isinstance(entity, str):
|
|
|
|
entity = hass.states.get(entity)
|
|
|
|
|
2020-08-19 18:01:27 +00:00
|
|
|
if entity is None or (attribute is not None and attribute not in entity.attributes):
|
2016-04-28 10:03:57 +00:00
|
|
|
return False
|
2020-08-19 18:01:27 +00:00
|
|
|
|
2019-01-20 23:03:12 +00:00
|
|
|
assert isinstance(entity, State)
|
2016-04-28 10:03:57 +00:00
|
|
|
|
2020-09-06 22:36:01 +00:00
|
|
|
if attribute is None:
|
2020-10-05 10:53:12 +00:00
|
|
|
value: Any = entity.state
|
2020-09-06 22:36:01 +00:00
|
|
|
else:
|
2020-10-05 10:53:12 +00:00
|
|
|
value = entity.attributes.get(attribute)
|
2020-09-06 22:36:01 +00:00
|
|
|
|
2020-10-05 10:53:12 +00:00
|
|
|
if not isinstance(req_state, list):
|
2020-06-15 22:53:13 +00:00
|
|
|
req_state = [req_state]
|
|
|
|
|
2020-09-06 22:36:01 +00:00
|
|
|
is_state = False
|
|
|
|
for req_state_value in req_state:
|
|
|
|
state_value = req_state_value
|
2020-10-05 10:53:12 +00:00
|
|
|
if (
|
|
|
|
isinstance(req_state_value, str)
|
|
|
|
and INPUT_ENTITY_ID.match(req_state_value) is not None
|
|
|
|
):
|
2020-09-06 22:36:01 +00:00
|
|
|
state_entity = hass.states.get(req_state_value)
|
|
|
|
if not state_entity:
|
|
|
|
continue
|
|
|
|
state_value = state_entity.state
|
|
|
|
is_state = value == state_value
|
|
|
|
if is_state:
|
|
|
|
break
|
2016-04-28 10:03:57 +00:00
|
|
|
|
|
|
|
if for_period is None or not is_state:
|
|
|
|
return is_state
|
|
|
|
|
|
|
|
return dt_util.utcnow() - for_period > entity.last_changed
|
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
def state_from_config(
|
|
|
|
config: ConfigType, config_validation: bool = True
|
2019-09-24 21:57:05 +00:00
|
|
|
) -> ConditionCheckerType:
|
2016-04-28 10:03:57 +00:00
|
|
|
"""Wrap action method with state based condition."""
|
|
|
|
if config_validation:
|
|
|
|
config = cv.STATE_CONDITION_SCHEMA(config)
|
2020-06-15 20:54:19 +00:00
|
|
|
entity_ids = config.get(CONF_ENTITY_ID, [])
|
2020-06-15 22:53:13 +00:00
|
|
|
req_states: Union[str, List[str]] = config.get(CONF_STATE, [])
|
2019-07-31 19:25:30 +00:00
|
|
|
for_period = config.get("for")
|
2020-08-19 18:01:27 +00:00
|
|
|
attribute = config.get(CONF_ATTRIBUTE)
|
2016-04-28 10:03:57 +00:00
|
|
|
|
2020-06-15 22:53:13 +00:00
|
|
|
if not isinstance(req_states, list):
|
|
|
|
req_states = [req_states]
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
def if_state(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool:
|
2016-04-28 10:03:57 +00:00
|
|
|
"""Test if condition."""
|
2020-06-15 20:54:19 +00:00
|
|
|
return all(
|
2020-08-19 18:01:27 +00:00
|
|
|
state(hass, entity_id, req_states, for_period, attribute)
|
|
|
|
for entity_id in entity_ids
|
2020-06-15 20:54:19 +00:00
|
|
|
)
|
2016-04-28 10:03:57 +00:00
|
|
|
|
|
|
|
return if_state
|
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
def sun(
|
|
|
|
hass: HomeAssistant,
|
|
|
|
before: Optional[str] = None,
|
|
|
|
after: Optional[str] = None,
|
|
|
|
before_offset: Optional[timedelta] = None,
|
|
|
|
after_offset: Optional[timedelta] = None,
|
|
|
|
) -> bool:
|
2016-04-28 10:03:57 +00:00
|
|
|
"""Test if current time matches sun requirements."""
|
2017-05-09 07:03:34 +00:00
|
|
|
utcnow = dt_util.utcnow()
|
|
|
|
today = dt_util.as_local(utcnow).date()
|
2016-04-28 10:03:57 +00:00
|
|
|
before_offset = before_offset or timedelta(0)
|
|
|
|
after_offset = after_offset or timedelta(0)
|
|
|
|
|
2019-08-16 15:34:56 +00:00
|
|
|
sunrise_today = get_astral_event_date(hass, SUN_EVENT_SUNRISE, today)
|
|
|
|
sunset_today = get_astral_event_date(hass, SUN_EVENT_SUNSET, today)
|
|
|
|
|
|
|
|
sunrise = sunrise_today
|
|
|
|
sunset = sunset_today
|
|
|
|
if today > dt_util.as_local(
|
|
|
|
cast(datetime, sunrise_today)
|
|
|
|
).date() and SUN_EVENT_SUNRISE in (before, after):
|
|
|
|
tomorrow = dt_util.as_local(utcnow + timedelta(days=1)).date()
|
|
|
|
sunrise_tomorrow = get_astral_event_date(hass, SUN_EVENT_SUNRISE, tomorrow)
|
|
|
|
sunrise = sunrise_tomorrow
|
|
|
|
|
|
|
|
if today > dt_util.as_local(
|
|
|
|
cast(datetime, sunset_today)
|
|
|
|
).date() and SUN_EVENT_SUNSET in (before, after):
|
|
|
|
tomorrow = dt_util.as_local(utcnow + timedelta(days=1)).date()
|
|
|
|
sunset_tomorrow = get_astral_event_date(hass, SUN_EVENT_SUNSET, tomorrow)
|
|
|
|
sunset = sunset_tomorrow
|
2017-05-09 07:03:34 +00:00
|
|
|
|
2018-07-17 17:34:29 +00:00
|
|
|
if sunrise is None and SUN_EVENT_SUNRISE in (before, after):
|
2017-05-09 07:03:34 +00:00
|
|
|
# There is no sunrise today
|
|
|
|
return False
|
|
|
|
|
2018-07-17 17:34:29 +00:00
|
|
|
if sunset is None and SUN_EVENT_SUNSET in (before, after):
|
2017-05-09 07:03:34 +00:00
|
|
|
# There is no sunset today
|
|
|
|
return False
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
if before == SUN_EVENT_SUNRISE and utcnow > cast(datetime, sunrise) + before_offset:
|
2016-04-28 10:03:57 +00:00
|
|
|
return False
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
if before == SUN_EVENT_SUNSET and utcnow > cast(datetime, sunset) + before_offset:
|
2016-04-28 10:03:57 +00:00
|
|
|
return False
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
if after == SUN_EVENT_SUNRISE and utcnow < cast(datetime, sunrise) + after_offset:
|
2016-04-28 10:03:57 +00:00
|
|
|
return False
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
if after == SUN_EVENT_SUNSET and utcnow < cast(datetime, sunset) + after_offset:
|
2016-04-28 10:03:57 +00:00
|
|
|
return False
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
def sun_from_config(
|
|
|
|
config: ConfigType, config_validation: bool = True
|
2019-09-24 21:57:05 +00:00
|
|
|
) -> ConditionCheckerType:
|
2016-04-28 10:03:57 +00:00
|
|
|
"""Wrap action method with sun based condition."""
|
|
|
|
if config_validation:
|
|
|
|
config = cv.SUN_CONDITION_SCHEMA(config)
|
2019-07-31 19:25:30 +00:00
|
|
|
before = config.get("before")
|
|
|
|
after = config.get("after")
|
|
|
|
before_offset = config.get("before_offset")
|
|
|
|
after_offset = config.get("after_offset")
|
2016-04-28 10:03:57 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
def time_if(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool:
|
2016-04-28 10:03:57 +00:00
|
|
|
"""Validate time based if-condition."""
|
|
|
|
return sun(hass, before, after, before_offset, after_offset)
|
|
|
|
|
|
|
|
return time_if
|
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
def template(
|
|
|
|
hass: HomeAssistant, value_template: Template, variables: TemplateVarsType = None
|
|
|
|
) -> bool:
|
2016-09-28 04:29:55 +00:00
|
|
|
"""Test if template condition matches."""
|
2020-04-17 18:33:58 +00:00
|
|
|
return run_callback_threadsafe(
|
|
|
|
hass.loop, async_template, hass, value_template, variables
|
|
|
|
).result()
|
2016-09-28 04:29:55 +00:00
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
def async_template(
|
|
|
|
hass: HomeAssistant, value_template: Template, variables: TemplateVarsType = None
|
|
|
|
) -> bool:
|
2016-04-28 10:03:57 +00:00
|
|
|
"""Test if template condition matches."""
|
|
|
|
try:
|
2020-11-25 14:10:04 +00:00
|
|
|
value: str = value_template.async_render(variables, parse_result=False)
|
2016-04-28 10:03:57 +00:00
|
|
|
except TemplateError as ex:
|
2017-05-02 16:18:47 +00:00
|
|
|
_LOGGER.error("Error during template condition: %s", ex)
|
2016-04-28 10:03:57 +00:00
|
|
|
return False
|
|
|
|
|
2020-11-25 14:10:04 +00:00
|
|
|
return value.lower() == "true"
|
2016-04-28 10:03:57 +00:00
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
def async_template_from_config(
|
|
|
|
config: ConfigType, config_validation: bool = True
|
2019-09-24 21:57:05 +00:00
|
|
|
) -> ConditionCheckerType:
|
2016-04-28 10:03:57 +00:00
|
|
|
"""Wrap action method with state based condition."""
|
|
|
|
if config_validation:
|
|
|
|
config = cv.TEMPLATE_CONDITION_SCHEMA(config)
|
2019-01-20 23:03:12 +00:00
|
|
|
value_template = cast(Template, config.get(CONF_VALUE_TEMPLATE))
|
2016-04-28 10:03:57 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
def template_if(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool:
|
2016-04-28 10:03:57 +00:00
|
|
|
"""Validate template based if-condition."""
|
2016-09-28 04:29:55 +00:00
|
|
|
value_template.hass = hass
|
2016-09-25 20:33:01 +00:00
|
|
|
|
2016-10-01 06:11:57 +00:00
|
|
|
return async_template(hass, value_template, variables)
|
2016-04-28 10:03:57 +00:00
|
|
|
|
|
|
|
return template_if
|
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
def time(
|
2020-09-06 14:06:09 +00:00
|
|
|
hass: HomeAssistant,
|
|
|
|
before: Optional[Union[dt_util.dt.time, str]] = None,
|
|
|
|
after: Optional[Union[dt_util.dt.time, str]] = None,
|
2019-07-31 19:25:30 +00:00
|
|
|
weekday: Union[None, str, Container[str]] = None,
|
|
|
|
) -> bool:
|
2016-05-29 21:32:32 +00:00
|
|
|
"""Test if local time condition matches.
|
|
|
|
|
|
|
|
Handle the fact that time is continuous and we may be testing for
|
|
|
|
a period that crosses midnight. In that case it is easier to test
|
|
|
|
for the opposite. "(23:59 <= now < 00:01)" would be the same as
|
|
|
|
"not (00:01 <= now < 23:59)".
|
|
|
|
"""
|
2016-04-28 10:03:57 +00:00
|
|
|
now = dt_util.now()
|
|
|
|
now_time = now.time()
|
|
|
|
|
2016-05-29 21:32:32 +00:00
|
|
|
if after is None:
|
|
|
|
after = dt_util.dt.time(0)
|
2020-09-06 14:06:09 +00:00
|
|
|
elif isinstance(after, str):
|
|
|
|
after_entity = hass.states.get(after)
|
|
|
|
if not after_entity:
|
|
|
|
return False
|
|
|
|
after = dt_util.dt.time(
|
|
|
|
after_entity.attributes.get("hour", 23),
|
|
|
|
after_entity.attributes.get("minute", 59),
|
|
|
|
after_entity.attributes.get("second", 59),
|
|
|
|
)
|
|
|
|
|
2016-05-29 21:32:32 +00:00
|
|
|
if before is None:
|
|
|
|
before = dt_util.dt.time(23, 59, 59, 999999)
|
2020-09-06 14:06:09 +00:00
|
|
|
elif isinstance(before, str):
|
|
|
|
before_entity = hass.states.get(before)
|
|
|
|
if not before_entity:
|
|
|
|
return False
|
|
|
|
before = dt_util.dt.time(
|
|
|
|
before_entity.attributes.get("hour", 23),
|
|
|
|
before_entity.attributes.get("minute", 59),
|
|
|
|
before_entity.attributes.get("second", 59),
|
|
|
|
999999,
|
|
|
|
)
|
2016-04-28 10:03:57 +00:00
|
|
|
|
2016-05-29 21:32:32 +00:00
|
|
|
if after < before:
|
|
|
|
if not after <= now_time < before:
|
|
|
|
return False
|
|
|
|
else:
|
|
|
|
if before <= now_time < after:
|
|
|
|
return False
|
2016-04-28 10:03:57 +00:00
|
|
|
|
|
|
|
if weekday is not None:
|
|
|
|
now_weekday = WEEKDAYS[now.weekday()]
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
if (
|
|
|
|
isinstance(weekday, str)
|
|
|
|
and weekday != now_weekday
|
|
|
|
or now_weekday not in weekday
|
|
|
|
):
|
2016-04-28 10:03:57 +00:00
|
|
|
return False
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
def time_from_config(
|
|
|
|
config: ConfigType, config_validation: bool = True
|
2019-09-24 21:57:05 +00:00
|
|
|
) -> ConditionCheckerType:
|
2016-04-28 10:03:57 +00:00
|
|
|
"""Wrap action method with time based condition."""
|
|
|
|
if config_validation:
|
|
|
|
config = cv.TIME_CONDITION_SCHEMA(config)
|
2016-05-03 05:05:09 +00:00
|
|
|
before = config.get(CONF_BEFORE)
|
|
|
|
after = config.get(CONF_AFTER)
|
|
|
|
weekday = config.get(CONF_WEEKDAY)
|
2016-04-28 10:03:57 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
def time_if(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool:
|
2016-04-28 10:03:57 +00:00
|
|
|
"""Validate time based if-condition."""
|
2020-09-06 14:06:09 +00:00
|
|
|
return time(hass, before, after, weekday)
|
2016-04-28 10:03:57 +00:00
|
|
|
|
|
|
|
return time_if
|
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
def zone(
|
|
|
|
hass: HomeAssistant,
|
|
|
|
zone_ent: Union[None, str, State],
|
|
|
|
entity: Union[None, str, State],
|
|
|
|
) -> bool:
|
2016-09-30 19:57:24 +00:00
|
|
|
"""Test if zone-condition matches.
|
|
|
|
|
2017-02-02 05:44:05 +00:00
|
|
|
Async friendly.
|
2016-09-30 19:57:24 +00:00
|
|
|
"""
|
2016-04-28 10:03:57 +00:00
|
|
|
if isinstance(zone_ent, str):
|
|
|
|
zone_ent = hass.states.get(zone_ent)
|
|
|
|
|
|
|
|
if zone_ent is None:
|
|
|
|
return False
|
|
|
|
|
|
|
|
if isinstance(entity, str):
|
|
|
|
entity = hass.states.get(entity)
|
|
|
|
|
|
|
|
if entity is None:
|
|
|
|
return False
|
|
|
|
|
|
|
|
latitude = entity.attributes.get(ATTR_LATITUDE)
|
|
|
|
longitude = entity.attributes.get(ATTR_LONGITUDE)
|
|
|
|
|
|
|
|
if latitude is None or longitude is None:
|
|
|
|
return False
|
|
|
|
|
2020-01-22 20:36:25 +00:00
|
|
|
return zone_cmp.in_zone(
|
2019-07-31 19:25:30 +00:00
|
|
|
zone_ent, latitude, longitude, entity.attributes.get(ATTR_GPS_ACCURACY, 0)
|
|
|
|
)
|
2016-04-28 10:03:57 +00:00
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
def zone_from_config(
|
|
|
|
config: ConfigType, config_validation: bool = True
|
2019-09-24 21:57:05 +00:00
|
|
|
) -> ConditionCheckerType:
|
2016-04-28 10:03:57 +00:00
|
|
|
"""Wrap action method with zone based condition."""
|
|
|
|
if config_validation:
|
|
|
|
config = cv.ZONE_CONDITION_SCHEMA(config)
|
2020-06-15 20:54:19 +00:00
|
|
|
entity_ids = config.get(CONF_ENTITY_ID, [])
|
2020-06-15 22:53:13 +00:00
|
|
|
zone_entity_ids = config.get(CONF_ZONE, [])
|
2016-04-28 10:03:57 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
def if_in_zone(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool:
|
2016-04-28 10:03:57 +00:00
|
|
|
"""Test if condition."""
|
2020-06-15 22:53:13 +00:00
|
|
|
return all(
|
|
|
|
any(
|
|
|
|
zone(hass, zone_entity_id, entity_id)
|
|
|
|
for zone_entity_id in zone_entity_ids
|
|
|
|
)
|
|
|
|
for entity_id in entity_ids
|
|
|
|
)
|
2016-04-28 10:03:57 +00:00
|
|
|
|
|
|
|
return if_in_zone
|
2019-09-24 21:57:05 +00:00
|
|
|
|
|
|
|
|
|
|
|
async def async_device_from_config(
|
|
|
|
hass: HomeAssistant, config: ConfigType, config_validation: bool = True
|
|
|
|
) -> ConditionCheckerType:
|
|
|
|
"""Test a device condition."""
|
|
|
|
if config_validation:
|
|
|
|
config = cv.DEVICE_CONDITION_SCHEMA(config)
|
2019-10-02 22:58:14 +00:00
|
|
|
platform = await async_get_device_automation_platform(
|
|
|
|
hass, config[CONF_DOMAIN], "condition"
|
|
|
|
)
|
2019-09-24 21:57:05 +00:00
|
|
|
return cast(
|
|
|
|
ConditionCheckerType,
|
|
|
|
platform.async_condition_from_config(config, config_validation), # type: ignore
|
|
|
|
)
|
2019-10-02 22:58:14 +00:00
|
|
|
|
|
|
|
|
|
|
|
async def async_validate_condition_config(
|
2020-09-06 14:55:06 +00:00
|
|
|
hass: HomeAssistant, config: Union[ConfigType, Template]
|
|
|
|
) -> Union[ConfigType, Template]:
|
2019-10-02 22:58:14 +00:00
|
|
|
"""Validate config."""
|
2020-09-06 14:55:06 +00:00
|
|
|
if isinstance(config, Template):
|
|
|
|
return config
|
|
|
|
|
2019-10-02 22:58:14 +00:00
|
|
|
condition = config[CONF_CONDITION]
|
2020-04-30 22:15:53 +00:00
|
|
|
if condition in ("and", "not", "or"):
|
2019-10-02 22:58:14 +00:00
|
|
|
conditions = []
|
|
|
|
for sub_cond in config["conditions"]:
|
|
|
|
sub_cond = await async_validate_condition_config(hass, sub_cond)
|
|
|
|
conditions.append(sub_cond)
|
|
|
|
config["conditions"] = conditions
|
|
|
|
|
|
|
|
if condition == "device":
|
|
|
|
config = cv.DEVICE_CONDITION_SCHEMA(config)
|
2020-09-06 14:55:06 +00:00
|
|
|
assert not isinstance(config, Template)
|
2019-10-02 22:58:14 +00:00
|
|
|
platform = await async_get_device_automation_platform(
|
|
|
|
hass, config[CONF_DOMAIN], "condition"
|
|
|
|
)
|
|
|
|
return cast(ConfigType, platform.CONDITION_SCHEMA(config)) # type: ignore
|
|
|
|
|
|
|
|
return config
|
2020-01-30 00:19:13 +00:00
|
|
|
|
|
|
|
|
|
|
|
@callback
|
2020-09-13 20:05:45 +00:00
|
|
|
def async_extract_entities(config: Union[ConfigType, Template]) -> Set[str]:
|
2020-01-30 00:19:13 +00:00
|
|
|
"""Extract entities from a condition."""
|
2020-06-15 20:54:19 +00:00
|
|
|
referenced: Set[str] = set()
|
2020-01-30 00:19:13 +00:00
|
|
|
to_process = deque([config])
|
|
|
|
|
|
|
|
while to_process:
|
|
|
|
config = to_process.popleft()
|
2020-09-13 20:05:45 +00:00
|
|
|
if isinstance(config, Template):
|
|
|
|
continue
|
|
|
|
|
2020-01-30 00:19:13 +00:00
|
|
|
condition = config[CONF_CONDITION]
|
|
|
|
|
2020-04-30 22:15:53 +00:00
|
|
|
if condition in ("and", "not", "or"):
|
2020-01-30 00:19:13 +00:00
|
|
|
to_process.extend(config["conditions"])
|
|
|
|
continue
|
|
|
|
|
2020-06-15 20:54:19 +00:00
|
|
|
entity_ids = config.get(CONF_ENTITY_ID)
|
|
|
|
|
|
|
|
if isinstance(entity_ids, str):
|
|
|
|
entity_ids = [entity_ids]
|
2020-01-30 00:19:13 +00:00
|
|
|
|
2020-06-15 20:54:19 +00:00
|
|
|
if entity_ids is not None:
|
|
|
|
referenced.update(entity_ids)
|
2020-01-30 00:19:13 +00:00
|
|
|
|
|
|
|
return referenced
|
|
|
|
|
|
|
|
|
|
|
|
@callback
|
2020-09-13 20:05:45 +00:00
|
|
|
def async_extract_devices(config: Union[ConfigType, Template]) -> Set[str]:
|
2020-01-30 00:19:13 +00:00
|
|
|
"""Extract devices from a condition."""
|
|
|
|
referenced = set()
|
|
|
|
to_process = deque([config])
|
|
|
|
|
|
|
|
while to_process:
|
|
|
|
config = to_process.popleft()
|
2020-09-13 20:05:45 +00:00
|
|
|
if isinstance(config, Template):
|
|
|
|
continue
|
|
|
|
|
2020-01-30 00:19:13 +00:00
|
|
|
condition = config[CONF_CONDITION]
|
|
|
|
|
2020-04-30 22:15:53 +00:00
|
|
|
if condition in ("and", "not", "or"):
|
2020-01-30 00:19:13 +00:00
|
|
|
to_process.extend(config["conditions"])
|
|
|
|
continue
|
|
|
|
|
|
|
|
if condition != "device":
|
|
|
|
continue
|
|
|
|
|
|
|
|
device_id = config.get(CONF_DEVICE_ID)
|
|
|
|
|
|
|
|
if device_id is not None:
|
|
|
|
referenced.add(device_id)
|
|
|
|
|
|
|
|
return referenced
|