2016-04-28 10:03:57 +00:00
|
|
|
"""Offer reusable conditions."""
|
|
|
|
from datetime import timedelta
|
2016-10-01 08:22:13 +00:00
|
|
|
import functools as ft
|
2016-04-28 10:03:57 +00:00
|
|
|
import logging
|
|
|
|
import sys
|
|
|
|
|
2016-08-09 03:42:25 +00:00
|
|
|
from homeassistant.helpers.typing import ConfigType
|
2016-08-07 23:26:35 +00:00
|
|
|
|
2016-08-09 03:42:25 +00:00
|
|
|
from homeassistant.core import HomeAssistant
|
2017-05-09 07:03:34 +00:00
|
|
|
from homeassistant.components import zone as zone_cmp
|
2016-04-28 10:03:57 +00:00
|
|
|
from homeassistant.const import (
|
|
|
|
ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE,
|
|
|
|
CONF_ENTITY_ID, CONF_VALUE_TEMPLATE, CONF_CONDITION,
|
2016-05-03 05:05:09 +00:00
|
|
|
WEEKDAYS, CONF_STATE, CONF_ZONE, CONF_BEFORE,
|
|
|
|
CONF_AFTER, CONF_WEEKDAY, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET,
|
2018-02-20 16:02:27 +00:00
|
|
|
CONF_BELOW, CONF_ABOVE, STATE_UNAVAILABLE, STATE_UNKNOWN)
|
2016-04-28 10:03:57 +00:00
|
|
|
from homeassistant.exceptions import TemplateError, HomeAssistantError
|
|
|
|
import homeassistant.helpers.config_validation as cv
|
2017-05-09 07:03:34 +00:00
|
|
|
from homeassistant.helpers.sun import get_astral_event_date
|
2016-04-28 10:03:57 +00:00
|
|
|
import homeassistant.util.dt as dt_util
|
2016-09-28 04:29:55 +00:00
|
|
|
from homeassistant.util.async import run_callback_threadsafe
|
2016-04-28 10:03:57 +00:00
|
|
|
|
|
|
|
FROM_CONFIG_FORMAT = '{}_from_config'
|
2016-10-01 06:11:57 +00:00
|
|
|
ASYNC_FROM_CONFIG_FORMAT = 'async_{}_from_config'
|
2016-04-28 10:03:57 +00:00
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2016-10-01 06:11:57 +00:00
|
|
|
# PyLint does not like the use of _threaded_factory
|
|
|
|
# pylint: disable=invalid-name
|
2016-04-28 10:03:57 +00:00
|
|
|
|
2016-10-01 06:11:57 +00:00
|
|
|
|
|
|
|
def _threaded_factory(async_factory):
|
2017-05-02 16:18:47 +00:00
|
|
|
"""Create threaded versions of async factories."""
|
2016-10-01 08:22:13 +00:00
|
|
|
@ft.wraps(async_factory)
|
2016-10-01 06:11:57 +00:00
|
|
|
def factory(config, config_validation=True):
|
|
|
|
"""Threaded factory."""
|
|
|
|
async_check = async_factory(config, config_validation)
|
|
|
|
|
|
|
|
def condition_if(hass, variables=None):
|
|
|
|
"""Validate condition."""
|
|
|
|
return run_callback_threadsafe(
|
|
|
|
hass.loop, async_check, hass, variables,
|
|
|
|
).result()
|
|
|
|
|
|
|
|
return condition_if
|
|
|
|
|
|
|
|
return factory
|
|
|
|
|
|
|
|
|
2018-02-11 17:20:28 +00:00
|
|
|
def async_from_config(config: ConfigType, config_validation: bool = True):
|
2016-10-01 06:11:57 +00:00
|
|
|
"""Turn a condition configuration into a method.
|
|
|
|
|
|
|
|
Should be run on the event loop.
|
|
|
|
"""
|
|
|
|
for fmt in (ASYNC_FROM_CONFIG_FORMAT, FROM_CONFIG_FORMAT):
|
|
|
|
factory = getattr(
|
|
|
|
sys.modules[__name__],
|
|
|
|
fmt.format(config.get(CONF_CONDITION)), None)
|
|
|
|
|
|
|
|
if factory:
|
|
|
|
break
|
2016-04-28 10:03:57 +00:00
|
|
|
|
|
|
|
if factory is None:
|
|
|
|
raise HomeAssistantError('Invalid condition "{}" specified {}'.format(
|
|
|
|
config.get(CONF_CONDITION), config))
|
|
|
|
|
|
|
|
return factory(config, config_validation)
|
|
|
|
|
|
|
|
|
2016-10-01 06:11:57 +00:00
|
|
|
from_config = _threaded_factory(async_from_config)
|
|
|
|
|
|
|
|
|
2018-02-11 17:20:28 +00:00
|
|
|
def async_and_from_config(config: ConfigType, config_validation: bool = True):
|
2016-04-28 10:03:57 +00:00
|
|
|
"""Create multi condition matcher using 'AND'."""
|
|
|
|
if config_validation:
|
|
|
|
config = cv.AND_CONDITION_SCHEMA(config)
|
2016-10-01 06:11:57 +00:00
|
|
|
checks = None
|
2016-04-28 10:03:57 +00:00
|
|
|
|
2016-08-09 03:42:25 +00:00
|
|
|
def if_and_condition(hass: HomeAssistant,
|
2016-08-07 23:26:35 +00:00
|
|
|
variables=None) -> bool:
|
2016-04-28 10:03:57 +00:00
|
|
|
"""Test and condition."""
|
2016-10-01 06:11:57 +00:00
|
|
|
nonlocal checks
|
|
|
|
|
|
|
|
if checks is None:
|
|
|
|
checks = [async_from_config(entry, False) for entry
|
|
|
|
in config['conditions']]
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
2016-10-01 06:11:57 +00:00
|
|
|
and_from_config = _threaded_factory(async_and_from_config)
|
|
|
|
|
|
|
|
|
2018-02-11 17:20:28 +00:00
|
|
|
def async_or_from_config(config: ConfigType, config_validation: bool = True):
|
2016-04-28 10:03:57 +00:00
|
|
|
"""Create multi condition matcher using 'OR'."""
|
|
|
|
if config_validation:
|
|
|
|
config = cv.OR_CONDITION_SCHEMA(config)
|
2016-10-01 06:11:57 +00:00
|
|
|
checks = None
|
2016-04-28 10:03:57 +00:00
|
|
|
|
2016-08-09 03:42:25 +00:00
|
|
|
def if_or_condition(hass: HomeAssistant,
|
2016-08-07 23:26:35 +00:00
|
|
|
variables=None) -> bool:
|
2016-04-28 10:03:57 +00:00
|
|
|
"""Test and condition."""
|
2016-10-01 06:11:57 +00:00
|
|
|
nonlocal checks
|
|
|
|
|
|
|
|
if checks is None:
|
|
|
|
checks = [async_from_config(entry, False) for entry
|
|
|
|
in config['conditions']]
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
2016-10-01 06:11:57 +00:00
|
|
|
or_from_config = _threaded_factory(async_or_from_config)
|
|
|
|
|
|
|
|
|
2016-08-09 03:42:25 +00:00
|
|
|
def numeric_state(hass: HomeAssistant, entity, below=None, above=None,
|
2016-08-07 23:26:35 +00:00
|
|
|
value_template=None, variables=None):
|
2016-04-28 10:03:57 +00:00
|
|
|
"""Test a numeric state condition."""
|
2016-09-30 19:57:24 +00:00
|
|
|
return run_callback_threadsafe(
|
|
|
|
hass.loop, async_numeric_state, hass, entity, below, above,
|
|
|
|
value_template, variables,
|
|
|
|
).result()
|
|
|
|
|
|
|
|
|
|
|
|
def async_numeric_state(hass: HomeAssistant, entity, below=None, above=None,
|
|
|
|
value_template=None, variables=None):
|
|
|
|
"""Test a numeric state condition."""
|
2016-04-28 10:03:57 +00:00
|
|
|
if isinstance(entity, str):
|
|
|
|
entity = hass.states.get(entity)
|
|
|
|
|
|
|
|
if entity is None:
|
|
|
|
return False
|
|
|
|
|
|
|
|
if value_template is None:
|
|
|
|
value = entity.state
|
|
|
|
else:
|
|
|
|
variables = dict(variables or {})
|
|
|
|
variables['state'] = entity
|
|
|
|
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:
|
2016-08-07 23:26:35 +00:00
|
|
|
_LOGGER.error("Template error: %s", ex)
|
2016-04-28 10:03:57 +00:00
|
|
|
return False
|
|
|
|
|
2018-02-20 16:02:27 +00:00
|
|
|
if value in (STATE_UNAVAILABLE, STATE_UNKNOWN):
|
|
|
|
return False
|
|
|
|
|
2016-04-28 10:03:57 +00:00
|
|
|
try:
|
|
|
|
value = float(value)
|
|
|
|
except ValueError:
|
|
|
|
_LOGGER.warning("Value cannot be processed as a number: %s", value)
|
|
|
|
return False
|
|
|
|
|
2017-06-02 05:43:24 +00:00
|
|
|
if below is not None and value >= below:
|
2016-04-28 10:03:57 +00:00
|
|
|
return False
|
|
|
|
|
2017-06-02 05:43:24 +00:00
|
|
|
if above is not None and value <= above:
|
2016-04-28 10:03:57 +00:00
|
|
|
return False
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
2016-10-01 06:11:57 +00:00
|
|
|
def async_numeric_state_from_config(config, config_validation=True):
|
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)
|
|
|
|
entity_id = config.get(CONF_ENTITY_ID)
|
|
|
|
below = config.get(CONF_BELOW)
|
|
|
|
above = config.get(CONF_ABOVE)
|
|
|
|
value_template = config.get(CONF_VALUE_TEMPLATE)
|
|
|
|
|
|
|
|
def if_numeric_state(hass, variables=None):
|
|
|
|
"""Test numeric state condition."""
|
2016-09-28 04:29:55 +00:00
|
|
|
if value_template is not None:
|
|
|
|
value_template.hass = hass
|
|
|
|
|
2016-10-01 06:11:57 +00:00
|
|
|
return async_numeric_state(
|
|
|
|
hass, entity_id, below, above, value_template, variables)
|
2016-04-28 10:03:57 +00:00
|
|
|
|
|
|
|
return if_numeric_state
|
|
|
|
|
|
|
|
|
2016-10-01 06:11:57 +00:00
|
|
|
numeric_state_from_config = _threaded_factory(async_numeric_state_from_config)
|
|
|
|
|
|
|
|
|
2016-04-28 10:03:57 +00:00
|
|
|
def state(hass, entity, req_state, for_period=None):
|
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)
|
|
|
|
|
|
|
|
if entity is None:
|
|
|
|
return False
|
|
|
|
|
|
|
|
is_state = entity.state == req_state
|
|
|
|
|
|
|
|
if for_period is None or not is_state:
|
|
|
|
return is_state
|
|
|
|
|
|
|
|
return dt_util.utcnow() - for_period > entity.last_changed
|
|
|
|
|
|
|
|
|
|
|
|
def state_from_config(config, config_validation=True):
|
|
|
|
"""Wrap action method with state based condition."""
|
|
|
|
if config_validation:
|
|
|
|
config = cv.STATE_CONDITION_SCHEMA(config)
|
|
|
|
entity_id = config.get(CONF_ENTITY_ID)
|
|
|
|
req_state = config.get(CONF_STATE)
|
|
|
|
for_period = config.get('for')
|
|
|
|
|
|
|
|
def if_state(hass, variables=None):
|
|
|
|
"""Test if condition."""
|
|
|
|
return state(hass, entity_id, req_state, for_period)
|
|
|
|
|
|
|
|
return if_state
|
|
|
|
|
|
|
|
|
|
|
|
def sun(hass, before=None, after=None, before_offset=None, after_offset=None):
|
|
|
|
"""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)
|
|
|
|
|
2017-05-09 07:03:34 +00:00
|
|
|
sunrise = get_astral_event_date(hass, 'sunrise', today)
|
|
|
|
sunset = get_astral_event_date(hass, 'sunset', today)
|
|
|
|
|
|
|
|
if sunrise is None and (before == SUN_EVENT_SUNRISE or
|
|
|
|
after == SUN_EVENT_SUNRISE):
|
|
|
|
# There is no sunrise today
|
|
|
|
return False
|
|
|
|
|
|
|
|
if sunset is None and (before == SUN_EVENT_SUNSET or
|
|
|
|
after == SUN_EVENT_SUNSET):
|
|
|
|
# There is no sunset today
|
|
|
|
return False
|
|
|
|
|
|
|
|
if before == SUN_EVENT_SUNRISE and utcnow > sunrise + before_offset:
|
2016-04-28 10:03:57 +00:00
|
|
|
return False
|
|
|
|
|
2017-05-09 07:03:34 +00:00
|
|
|
elif before == SUN_EVENT_SUNSET and utcnow > sunset + before_offset:
|
2016-04-28 10:03:57 +00:00
|
|
|
return False
|
|
|
|
|
2017-05-09 07:03:34 +00:00
|
|
|
if after == SUN_EVENT_SUNRISE and utcnow < sunrise + after_offset:
|
2016-04-28 10:03:57 +00:00
|
|
|
return False
|
|
|
|
|
2017-05-09 07:03:34 +00:00
|
|
|
elif after == SUN_EVENT_SUNSET and utcnow < sunset + after_offset:
|
2016-04-28 10:03:57 +00:00
|
|
|
return False
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
def sun_from_config(config, config_validation=True):
|
|
|
|
"""Wrap action method with sun based condition."""
|
|
|
|
if config_validation:
|
|
|
|
config = cv.SUN_CONDITION_SCHEMA(config)
|
|
|
|
before = config.get('before')
|
|
|
|
after = config.get('after')
|
|
|
|
before_offset = config.get('before_offset')
|
|
|
|
after_offset = config.get('after_offset')
|
|
|
|
|
|
|
|
def time_if(hass, variables=None):
|
|
|
|
"""Validate time based if-condition."""
|
|
|
|
return sun(hass, before, after, before_offset, after_offset)
|
|
|
|
|
|
|
|
return time_if
|
|
|
|
|
|
|
|
|
|
|
|
def template(hass, value_template, variables=None):
|
2016-09-28 04:29:55 +00:00
|
|
|
"""Test if template condition matches."""
|
|
|
|
return run_callback_threadsafe(
|
|
|
|
hass.loop, async_template, hass, value_template, variables,
|
|
|
|
).result()
|
|
|
|
|
|
|
|
|
|
|
|
def async_template(hass, value_template, variables=None):
|
2016-04-28 10:03:57 +00:00
|
|
|
"""Test if template condition matches."""
|
|
|
|
try:
|
2016-09-28 04:29:55 +00:00
|
|
|
value = value_template.async_render(variables)
|
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
|
|
|
|
|
|
|
|
return value.lower() == 'true'
|
|
|
|
|
|
|
|
|
2016-10-01 06:11:57 +00:00
|
|
|
def async_template_from_config(config, config_validation=True):
|
2016-04-28 10:03:57 +00:00
|
|
|
"""Wrap action method with state based condition."""
|
|
|
|
if config_validation:
|
|
|
|
config = cv.TEMPLATE_CONDITION_SCHEMA(config)
|
|
|
|
value_template = config.get(CONF_VALUE_TEMPLATE)
|
|
|
|
|
|
|
|
def template_if(hass, variables=None):
|
|
|
|
"""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
|
|
|
|
|
|
|
|
|
2016-10-01 06:11:57 +00:00
|
|
|
template_from_config = _threaded_factory(async_template_from_config)
|
|
|
|
|
|
|
|
|
2016-04-28 10:03:57 +00:00
|
|
|
def time(before=None, after=None, weekday=None):
|
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)
|
|
|
|
if before is None:
|
|
|
|
before = dt_util.dt.time(23, 59, 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()]
|
|
|
|
|
|
|
|
if isinstance(weekday, str) and weekday != now_weekday or \
|
|
|
|
now_weekday not in weekday:
|
|
|
|
return False
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
def time_from_config(config, config_validation=True):
|
|
|
|
"""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
|
|
|
|
|
|
|
def time_if(hass, variables=None):
|
|
|
|
"""Validate time based if-condition."""
|
|
|
|
return time(before, after, weekday)
|
|
|
|
|
|
|
|
return time_if
|
|
|
|
|
|
|
|
|
|
|
|
def zone(hass, zone_ent, entity):
|
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
|
|
|
|
|
|
|
|
return zone_cmp.in_zone(zone_ent, latitude, longitude,
|
|
|
|
entity.attributes.get(ATTR_GPS_ACCURACY, 0))
|
|
|
|
|
|
|
|
|
|
|
|
def zone_from_config(config, config_validation=True):
|
|
|
|
"""Wrap action method with zone based condition."""
|
|
|
|
if config_validation:
|
|
|
|
config = cv.ZONE_CONDITION_SCHEMA(config)
|
|
|
|
entity_id = config.get(CONF_ENTITY_ID)
|
2016-05-03 05:05:09 +00:00
|
|
|
zone_entity_id = config.get(CONF_ZONE)
|
2016-04-28 10:03:57 +00:00
|
|
|
|
|
|
|
def if_in_zone(hass, variables=None):
|
|
|
|
"""Test if condition."""
|
|
|
|
return zone(hass, zone_entity_id, entity_id)
|
|
|
|
|
|
|
|
return if_in_zone
|