1
mirror of https://github.com/home-assistant/core synced 2024-10-04 07:58:43 +02:00

Make automation async

This commit is contained in:
Paulus Schoutsen 2016-10-01 01:22:13 -07:00
parent 16ff68ca84
commit 7ab7edd81c
19 changed files with 205 additions and 346 deletions

View File

@ -4,6 +4,7 @@ Allow to setup simple automation rules via the config file.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/automation/
"""
import asyncio
from functools import partial
import logging
import os
@ -23,6 +24,7 @@ from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.loader import get_platform
from homeassistant.util.dt import utcnow
import homeassistant.helpers.config_validation as cv
from homeassistant.util.async import run_coroutine_threadsafe
DOMAIN = 'automation'
ENTITY_ID_FORMAT = DOMAIN + '.{}'
@ -44,9 +46,6 @@ CONDITION_TYPE_OR = 'or'
DEFAULT_CONDITION_TYPE = CONDITION_TYPE_AND
DEFAULT_HIDE_ENTITY = False
METHOD_TRIGGER = 'trigger'
METHOD_IF_ACTION = 'if_action'
ATTR_LAST_TRIGGERED = 'last_triggered'
ATTR_VARIABLES = 'variables'
SERVICE_TRIGGER = 'trigger'
@ -55,21 +54,14 @@ SERVICE_RELOAD = 'reload'
_LOGGER = logging.getLogger(__name__)
def _platform_validator(method, schema):
"""Generate platform validator for different steps."""
def validator(config):
"""Validate it is a valid platform."""
platform = get_platform(DOMAIN, config[CONF_PLATFORM])
def _platform_validator(config):
"""Validate it is a valid platform."""
platform = get_platform(DOMAIN, config[CONF_PLATFORM])
if not hasattr(platform, method):
raise vol.Invalid('invalid method platform')
if not hasattr(platform, 'TRIGGER_SCHEMA'):
return config
if not hasattr(platform, schema):
return config
return getattr(platform, schema)(config)
return validator
return getattr(platform, 'TRIGGER_SCHEMA')(config)
_TRIGGER_SCHEMA = vol.All(
cv.ensure_list,
@ -78,33 +70,17 @@ _TRIGGER_SCHEMA = vol.All(
vol.Schema({
vol.Required(CONF_PLATFORM): cv.platform_validator(DOMAIN)
}, extra=vol.ALLOW_EXTRA),
_platform_validator(METHOD_TRIGGER, 'TRIGGER_SCHEMA')
_platform_validator
),
]
)
_CONDITION_SCHEMA = vol.Any(
CONDITION_USE_TRIGGER_VALUES,
vol.All(
cv.ensure_list,
[
vol.All(
vol.Schema({
CONF_PLATFORM: str,
CONF_CONDITION: str,
}, extra=vol.ALLOW_EXTRA),
cv.has_at_least_one_key(CONF_PLATFORM, CONF_CONDITION),
),
]
)
)
_CONDITION_SCHEMA = vol.All(cv.ensure_list, [cv.CONDITION_SCHEMA])
PLATFORM_SCHEMA = vol.Schema({
CONF_ALIAS: cv.string,
vol.Optional(CONF_HIDE_ENTITY, default=DEFAULT_HIDE_ENTITY): cv.boolean,
vol.Required(CONF_TRIGGER): _TRIGGER_SCHEMA,
vol.Required(CONF_CONDITION_TYPE, default=DEFAULT_CONDITION_TYPE):
vol.All(vol.Lower, vol.Any(CONDITION_TYPE_AND, CONDITION_TYPE_OR)),
vol.Optional(CONF_CONDITION): _CONDITION_SCHEMA,
vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA,
})
@ -165,7 +141,8 @@ def setup(hass, config):
"""Setup the automation."""
component = EntityComponent(_LOGGER, DOMAIN, hass)
success = _process_config(hass, config, component)
success = run_coroutine_threadsafe(
_async_process_config(hass, config, component), hass.loop).result()
if not success:
return False
@ -173,22 +150,27 @@ def setup(hass, config):
descriptions = conf_util.load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
@asyncio.coroutine
def trigger_service_handler(service_call):
"""Handle automation triggers."""
for entity in component.extract_from_service(service_call):
entity.trigger(service_call.data.get(ATTR_VARIABLES))
yield from entity.async_trigger(
service_call.data.get(ATTR_VARIABLES))
@asyncio.coroutine
def service_handler(service_call):
"""Handle automation service calls."""
method = 'async_{}'.format(service_call.service)
for entity in component.extract_from_service(service_call):
getattr(entity, service_call.service)()
yield from getattr(entity, method)()
def reload_service_handler(service_call):
"""Remove all automations and load new ones from config."""
conf = component.prepare_reload()
if conf is None:
return
_process_config(hass, conf, component)
run_coroutine_threadsafe(
_async_process_config(hass, conf, component), hass.loop).result()
hass.services.register(DOMAIN, SERVICE_TRIGGER, trigger_service_handler,
descriptions.get(SERVICE_TRIGGER),
@ -209,14 +191,16 @@ def setup(hass, config):
class AutomationEntity(ToggleEntity):
"""Entity to show status of entity."""
# pylint: disable=abstract-method
# pylint: disable=too-many-arguments, too-many-instance-attributes
def __init__(self, name, attach_triggers, cond_func, action, hidden):
def __init__(self, name, async_attach_triggers, cond_func, async_action,
hidden):
"""Initialize an automation entity."""
self._name = name
self._attach_triggers = attach_triggers
self._detach_triggers = attach_triggers(self.trigger)
self._async_attach_triggers = async_attach_triggers
self._async_detach_triggers = async_attach_triggers(self.async_trigger)
self._cond_func = cond_func
self._action = action
self._async_action = async_action
self._enabled = True
self._last_triggered = None
self._hidden = hidden
@ -248,39 +232,53 @@ class AutomationEntity(ToggleEntity):
"""Return True if entity is on."""
return self._enabled
def turn_on(self, **kwargs) -> None:
@asyncio.coroutine
def async_turn_on(self, **kwargs) -> None:
"""Turn the entity on."""
if self._enabled:
return
self._detach_triggers = self._attach_triggers(self.trigger)
self._async_detach_triggers = self._async_attach_triggers(
self.async_trigger)
self._enabled = True
self.update_ha_state()
yield from self.async_update_ha_state()
def turn_off(self, **kwargs) -> None:
@asyncio.coroutine
def async_turn_off(self, **kwargs) -> None:
"""Turn the entity off."""
if not self._enabled:
return
self._detach_triggers()
self._detach_triggers = None
self._async_detach_triggers()
self._async_detach_triggers = None
self._enabled = False
self.update_ha_state()
yield from self.async_update_ha_state()
def trigger(self, variables):
@asyncio.coroutine
def async_toggle(self):
"""Toggle the state of the entity."""
if self._enabled:
yield from self.async_turn_off()
else:
yield from self.async_turn_on()
@asyncio.coroutine
def async_trigger(self, variables):
"""Trigger automation."""
if self._cond_func(variables):
self._action(variables)
yield from self._async_action(variables)
self._last_triggered = utcnow()
self.update_ha_state()
yield from self.async_update_ha_state()
def remove(self):
"""Remove automation from HASS."""
self.turn_off()
run_coroutine_threadsafe(self.async_turn_off(),
self.hass.loop).result()
super().remove()
def _process_config(hass, config, component):
@asyncio.coroutine
def _async_process_config(hass, config, component):
"""Process config and add automations."""
success = False
@ -293,10 +291,11 @@ def _process_config(hass, config, component):
hidden = config_block[CONF_HIDE_ENTITY]
action = _get_action(hass, config_block.get(CONF_ACTION, {}), name)
action = _async_get_action(hass, config_block.get(CONF_ACTION, {}),
name)
if CONF_CONDITION in config_block:
cond_func = _process_if(hass, config, config_block)
cond_func = _async_process_if(hass, config, config_block)
if cond_func is None:
continue
@ -305,101 +304,68 @@ def _process_config(hass, config, component):
"""Condition will always pass."""
return True
attach_triggers = partial(_process_trigger, hass, config,
config_block.get(CONF_TRIGGER, []), name)
entity = AutomationEntity(name, attach_triggers, cond_func, action,
hidden)
component.add_entities((entity,))
async_attach_triggers = partial(
_async_process_trigger, hass, config,
config_block.get(CONF_TRIGGER, []), name)
entity = AutomationEntity(name, async_attach_triggers, cond_func,
action, hidden)
yield from hass.loop.run_in_executor(
None, component.add_entities, [entity])
success = True
return success
def _get_action(hass, config, name):
def _async_get_action(hass, config, name):
"""Return an action based on a configuration."""
script_obj = script.Script(hass, config, name)
@asyncio.coroutine
def action(variables=None):
"""Action to be executed."""
_LOGGER.info('Executing %s', name)
logbook.log_entry(hass, name, 'has been triggered', DOMAIN)
script_obj.run(variables)
logbook.async_log_entry(hass, name, 'has been triggered', DOMAIN)
yield from script_obj.async_run(variables)
return action
def _process_if(hass, config, p_config):
def _async_process_if(hass, config, p_config):
"""Process if checks."""
cond_type = p_config.get(CONF_CONDITION_TYPE,
DEFAULT_CONDITION_TYPE).lower()
# Deprecated since 0.19 - 5/5/2016
if cond_type != DEFAULT_CONDITION_TYPE:
_LOGGER.warning('Using condition_type: "or" is deprecated. Please use '
'"condition: or" instead.')
if_configs = p_config.get(CONF_CONDITION)
use_trigger = if_configs == CONDITION_USE_TRIGGER_VALUES
if use_trigger:
if_configs = p_config[CONF_TRIGGER]
checks = []
for if_config in if_configs:
# Deprecated except for used by use_trigger_values
# since 0.19 - 5/5/2016
if CONF_PLATFORM in if_config:
if not use_trigger:
_LOGGER.warning("Please switch your condition configuration "
"to use 'condition' instead of 'platform'.")
if_config = dict(if_config)
if_config[CONF_CONDITION] = if_config.pop(CONF_PLATFORM)
# To support use_trigger_values with state trigger accepting
# multiple entity_ids to monitor.
if_entity_id = if_config.get(ATTR_ENTITY_ID)
if isinstance(if_entity_id, list) and len(if_entity_id) == 1:
if_config[ATTR_ENTITY_ID] = if_entity_id[0]
try:
checks.append(condition.from_config(if_config))
checks.append(condition.async_from_config(if_config, False))
except HomeAssistantError as ex:
# Invalid conditions are allowed if we base it on trigger
if use_trigger:
_LOGGER.warning('Ignoring invalid condition: %s', ex)
else:
_LOGGER.warning('Invalid condition: %s', ex)
return None
_LOGGER.warning('Invalid condition: %s', ex)
return None
if cond_type == CONDITION_TYPE_AND:
def if_action(variables=None):
"""AND all conditions."""
return all(check(hass, variables) for check in checks)
else:
def if_action(variables=None):
"""OR all conditions."""
return any(check(hass, variables) for check in checks)
def if_action(variables=None):
"""AND all conditions."""
return all(check(hass, variables) for check in checks)
return if_action
def _process_trigger(hass, config, trigger_configs, name, action):
def _async_process_trigger(hass, config, trigger_configs, name, action):
"""Setup the triggers."""
removes = []
for conf in trigger_configs:
platform = _resolve_platform(METHOD_TRIGGER, hass, config,
conf.get(CONF_PLATFORM))
platform = prepare_setup_platform(hass, config, DOMAIN,
conf.get(CONF_PLATFORM))
if platform is None:
continue
return None
remove = platform.trigger(hass, conf, action)
remove = platform.async_trigger(hass, conf, action)
if not remove:
_LOGGER.error("Error setting up rule %s", name)
_LOGGER.error("Error setting up trigger %s", name)
continue
_LOGGER.info("Initialized rule %s", name)
_LOGGER.info("Initialized trigger %s", name)
removes.append(remove)
if not removes:
@ -411,17 +377,3 @@ def _process_trigger(hass, config, trigger_configs, name, action):
remove()
return remove_triggers
def _resolve_platform(method, hass, config, platform):
"""Find the automation platform."""
if platform is None:
return None
platform = prepare_setup_platform(hass, config, DOMAIN, platform)
if platform is None or not hasattr(platform, method):
_LOGGER.error("Unknown automation platform specified for %s: %s",
method, platform)
return None
return platform

View File

@ -24,7 +24,7 @@ TRIGGER_SCHEMA = vol.Schema({
})
def trigger(hass, config, action):
def async_trigger(hass, config, action):
"""Listen for events based on configuration."""
event_type = config.get(CONF_EVENT_TYPE)
event_data = config.get(CONF_EVENT_DATA)
@ -41,4 +41,4 @@ def trigger(hass, config, action):
},
})
return hass.bus.listen(event_type, handle_event)
return hass.bus.async_listen(event_type, handle_event)

View File

@ -22,7 +22,7 @@ TRIGGER_SCHEMA = vol.Schema({
})
def trigger(hass, config, action):
def async_trigger(hass, config, action):
"""Listen for state changes based on configuration."""
topic = config.get(CONF_TOPIC)
payload = config.get(CONF_PAYLOAD)
@ -40,4 +40,4 @@ def trigger(hass, config, action):
}
})
return mqtt.subscribe(hass, topic, mqtt_automation_listener)
return mqtt.async_subscribe(hass, topic, mqtt_automation_listener)

View File

@ -12,7 +12,7 @@ import voluptuous as vol
from homeassistant.const import (
CONF_VALUE_TEMPLATE, CONF_PLATFORM, CONF_ENTITY_ID,
CONF_BELOW, CONF_ABOVE)
from homeassistant.helpers.event import track_state_change
from homeassistant.helpers.event import async_track_state_change
from homeassistant.helpers import condition, config_validation as cv
TRIGGER_SCHEMA = vol.All(vol.Schema({
@ -26,7 +26,7 @@ TRIGGER_SCHEMA = vol.All(vol.Schema({
_LOGGER = logging.getLogger(__name__)
def trigger(hass, config, action):
def async_trigger(hass, config, action):
"""Listen for state changes based on configuration."""
entity_id = config.get(CONF_ENTITY_ID)
below = config.get(CONF_BELOW)
@ -66,4 +66,4 @@ def trigger(hass, config, action):
hass.async_add_job(action, variables)
return track_state_change(hass, entity_id, state_automation_listener)
return async_track_state_change(hass, entity_id, state_automation_listener)

View File

@ -12,7 +12,6 @@ from homeassistant.const import MATCH_ALL, CONF_PLATFORM
from homeassistant.helpers.event import (
async_track_state_change, async_track_point_in_utc_time)
import homeassistant.helpers.config_validation as cv
from homeassistant.util.async import run_callback_threadsafe
CONF_ENTITY_ID = "entity_id"
CONF_FROM = "from"
@ -35,7 +34,7 @@ TRIGGER_SCHEMA = vol.All(
)
def trigger(hass, config, action):
def async_trigger(hass, config, action):
"""Listen for state changes based on configuration."""
entity_id = config.get(CONF_ENTITY_ID)
from_state = config.get(CONF_FROM, MATCH_ALL)
@ -98,8 +97,4 @@ def trigger(hass, config, action):
if async_remove_state_for_listener is not None:
async_remove_state_for_listener()
def remove():
"""Remove state listeners."""
run_callback_threadsafe(hass.loop, async_remove).result()
return remove
return async_remove

View File

@ -12,7 +12,7 @@ import voluptuous as vol
from homeassistant.const import (
CONF_EVENT, CONF_OFFSET, CONF_PLATFORM, SUN_EVENT_SUNRISE)
from homeassistant.helpers.event import track_sunrise, track_sunset
from homeassistant.helpers.event import async_track_sunrise, async_track_sunset
import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ['sun']
@ -26,7 +26,7 @@ TRIGGER_SCHEMA = vol.Schema({
})
def trigger(hass, config, action):
def async_trigger(hass, config, action):
"""Listen for events based on configuration."""
event = config.get(CONF_EVENT)
offset = config.get(CONF_OFFSET)
@ -44,6 +44,6 @@ def trigger(hass, config, action):
# Do something to call action
if event == SUN_EVENT_SUNRISE:
return track_sunrise(hass, call_action, offset)
return async_track_sunrise(hass, call_action, offset)
else:
return track_sunset(hass, call_action, offset)
return async_track_sunset(hass, call_action, offset)

View File

@ -11,7 +11,7 @@ import voluptuous as vol
from homeassistant.const import CONF_VALUE_TEMPLATE, CONF_PLATFORM
from homeassistant.helpers import condition
from homeassistant.helpers.event import track_state_change
from homeassistant.helpers.event import async_track_state_change
import homeassistant.helpers.config_validation as cv
@ -23,7 +23,7 @@ TRIGGER_SCHEMA = IF_ACTION_SCHEMA = vol.Schema({
})
def trigger(hass, config, action):
def async_trigger(hass, config, action):
"""Listen for state changes based on configuration."""
value_template = config.get(CONF_VALUE_TEMPLATE)
value_template.hass = hass
@ -51,5 +51,5 @@ def trigger(hass, config, action):
elif not template_result:
already_triggered = False
return track_state_change(hass, value_template.extract_entities(),
state_changed_listener)
return async_track_state_change(hass, value_template.extract_entities(),
state_changed_listener)

View File

@ -11,7 +11,7 @@ import voluptuous as vol
from homeassistant.const import CONF_AFTER, CONF_PLATFORM
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.event import track_time_change
from homeassistant.helpers.event import async_track_time_change
CONF_HOURS = "hours"
CONF_MINUTES = "minutes"
@ -29,7 +29,7 @@ TRIGGER_SCHEMA = vol.All(vol.Schema({
CONF_SECONDS, CONF_AFTER))
def trigger(hass, config, action):
def async_trigger(hass, config, action):
"""Listen for state changes based on configuration."""
if CONF_AFTER in config:
after = config.get(CONF_AFTER)
@ -49,5 +49,5 @@ def trigger(hass, config, action):
},
})
return track_time_change(hass, time_automation_listener,
hour=hours, minute=minutes, second=seconds)
return async_track_time_change(hass, time_automation_listener,
hour=hours, minute=minutes, second=seconds)

View File

@ -9,7 +9,7 @@ import voluptuous as vol
from homeassistant.const import (
CONF_EVENT, CONF_ENTITY_ID, CONF_ZONE, MATCH_ALL, CONF_PLATFORM)
from homeassistant.helpers.event import track_state_change
from homeassistant.helpers.event import async_track_state_change
from homeassistant.helpers import (
condition, config_validation as cv, location)
@ -26,7 +26,7 @@ TRIGGER_SCHEMA = vol.Schema({
})
def trigger(hass, config, action):
def async_trigger(hass, config, action):
"""Listen for state changes based on configuration."""
entity_id = config.get(CONF_ENTITY_ID)
zone_entity_id = config.get(CONF_ZONE)
@ -60,5 +60,5 @@ def trigger(hass, config, action):
},
})
return track_state_change(hass, entity_id, zone_automation_listener,
MATCH_ALL, MATCH_ALL)
return async_track_state_change(hass, entity_id, zone_automation_listener,
MATCH_ALL, MATCH_ALL)

View File

@ -1,5 +1,6 @@
"""Offer reusable conditions."""
from datetime import timedelta
import functools as ft
import logging
import sys
@ -30,6 +31,7 @@ _LOGGER = logging.getLogger(__name__)
def _threaded_factory(async_factory):
"""Helper method to create threaded versions of async factories."""
@ft.wraps(async_factory)
def factory(config, config_validation=True):
"""Threaded factory."""
async_check = async_factory(config, config_validation)

View File

@ -24,13 +24,20 @@ def generate_entity_id(entity_id_format: str, name: Optional[str],
current_ids: Optional[List[str]]=None,
hass: Optional[HomeAssistant]=None) -> str:
"""Generate a unique entity ID based on given entity IDs or used IDs."""
name = (name or DEVICE_DEFAULT_NAME).lower()
if current_ids is None:
if hass is None:
raise ValueError("Missing required parameter currentids or hass")
current_ids = hass.states.entity_ids()
return async_generate_entity_id(entity_id_format, name, current_ids)
def async_generate_entity_id(entity_id_format: str, name: Optional[str],
current_ids: Optional[List[str]]=None) -> str:
"""Generate a unique entity ID based on given entity IDs or used IDs."""
name = (name or DEVICE_DEFAULT_NAME).lower()
return ensure_unique_string(
entity_id_format.format(slugify(name)), current_ids)

View File

@ -3,30 +3,36 @@ import asyncio
import functools as ft
from datetime import timedelta
from ..core import HomeAssistant
from ..const import (
ATTR_NOW, EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL)
from ..util import dt as dt_util
from ..util.async import run_callback_threadsafe
# PyLint does not like the use of _threaded_factory
# pylint: disable=invalid-name
def track_state_change(hass, entity_ids, action, from_state=None,
to_state=None):
"""Track specific state changes.
entity_ids, from_state and to_state can be string or list.
Use list to match multiple.
def _threaded_factory(async_factory):
"""Convert an async event helper to a threaded one."""
@ft.wraps(async_factory)
def factory(*args, **kwargs):
"""Call async event helper safely."""
hass = args[0]
Returns a function that can be called to remove the listener.
"""
async_unsub = run_callback_threadsafe(
hass.loop, async_track_state_change, hass, entity_ids, action,
from_state, to_state).result()
if not isinstance(hass, HomeAssistant):
raise TypeError('First parameter needs to be a hass instance')
def remove():
"""Remove listener."""
run_callback_threadsafe(hass.loop, async_unsub).result()
async_remove = run_callback_threadsafe(
hass.loop, ft.partial(async_factory, *args, **kwargs)).result()
return remove
def remove():
"""Threadsafe removal."""
run_callback_threadsafe(hass.loop, async_remove).result()
return remove
return factory
def async_track_state_change(hass, entity_ids, action, from_state=None,
@ -77,7 +83,10 @@ def async_track_state_change(hass, entity_ids, action, from_state=None,
return hass.bus.async_listen(EVENT_STATE_CHANGED, state_change_listener)
def track_point_in_time(hass, action, point_in_time):
track_state_change = _threaded_factory(async_track_state_change)
def async_track_point_in_time(hass, action, point_in_time):
"""Add a listener that fires once after a spefic point in time."""
utc_point_in_time = dt_util.as_utc(point_in_time)
@ -87,20 +96,11 @@ def track_point_in_time(hass, action, point_in_time):
"""Convert passed in UTC now to local now."""
hass.async_add_job(action, dt_util.as_local(utc_now))
return track_point_in_utc_time(hass, utc_converter, utc_point_in_time)
return async_track_point_in_utc_time(hass, utc_converter,
utc_point_in_time)
def track_point_in_utc_time(hass, action, point_in_time):
"""Add a listener that fires once after a specific point in UTC time."""
async_unsub = run_callback_threadsafe(
hass.loop, async_track_point_in_utc_time, hass, action, point_in_time
).result()
def remove():
"""Remove listener."""
run_callback_threadsafe(hass.loop, async_unsub).result()
return remove
track_point_in_time = _threaded_factory(async_track_point_in_time)
def async_track_point_in_utc_time(hass, action, point_in_time):
@ -133,7 +133,10 @@ def async_track_point_in_utc_time(hass, action, point_in_time):
return async_unsub
def track_sunrise(hass, action, offset=None):
track_point_in_utc_time = _threaded_factory(async_track_point_in_utc_time)
def async_track_sunrise(hass, action, offset=None):
"""Add a listener that will fire a specified offset from sunrise daily."""
from homeassistant.components import sun
offset = offset or timedelta()
@ -147,6 +150,7 @@ def track_sunrise(hass, action, offset=None):
return next_time
@ft.wraps(action)
@asyncio.coroutine
def sunrise_automation_listener(now):
"""Called when it's time for action."""
@ -155,18 +159,20 @@ def track_sunrise(hass, action, offset=None):
hass, sunrise_automation_listener, next_rise())
hass.async_add_job(action)
remove = run_callback_threadsafe(
hass.loop, async_track_point_in_utc_time, hass,
sunrise_automation_listener, next_rise()).result()
remove = async_track_point_in_utc_time(
hass, sunrise_automation_listener, next_rise())
def remove_listener():
"""Remove sunset listener."""
run_callback_threadsafe(hass.loop, remove).result()
remove()
return remove_listener
def track_sunset(hass, action, offset=None):
track_sunrise = _threaded_factory(async_track_sunrise)
def async_track_sunset(hass, action, offset=None):
"""Add a listener that will fire a specified offset from sunset daily."""
from homeassistant.components import sun
offset = offset or timedelta()
@ -180,6 +186,7 @@ def track_sunset(hass, action, offset=None):
return next_time
@ft.wraps(action)
@asyncio.coroutine
def sunset_automation_listener(now):
"""Called when it's time for action."""
@ -188,20 +195,23 @@ def track_sunset(hass, action, offset=None):
hass, sunset_automation_listener, next_set())
hass.async_add_job(action)
remove = run_callback_threadsafe(
hass.loop, async_track_point_in_utc_time, hass,
sunset_automation_listener, next_set()).result()
remove = async_track_point_in_utc_time(
hass, sunset_automation_listener, next_set())
def remove_listener():
"""Remove sunset listener."""
run_callback_threadsafe(hass.loop, remove).result()
remove()
return remove_listener
track_sunset = _threaded_factory(async_track_sunset)
# pylint: disable=too-many-arguments
def track_utc_time_change(hass, action, year=None, month=None, day=None,
hour=None, minute=None, second=None, local=False):
def async_track_utc_time_change(hass, action, year=None, month=None, day=None,
hour=None, minute=None, second=None,
local=False):
"""Add a listener that will fire if time matches a pattern."""
# We do not have to wrap the function with time pattern matching logic
# if no pattern given
@ -211,7 +221,7 @@ def track_utc_time_change(hass, action, year=None, month=None, day=None,
"""Fire every time event that comes in."""
action(event.data[ATTR_NOW])
return hass.bus.listen(EVENT_TIME_CHANGED, time_change_listener)
return hass.bus.async_listen(EVENT_TIME_CHANGED, time_change_listener)
pmp = _process_time_match
year, month, day = pmp(year), pmp(month), pmp(day)
@ -237,15 +247,22 @@ def track_utc_time_change(hass, action, year=None, month=None, day=None,
hass.async_add_job(action, now)
return hass.bus.listen(EVENT_TIME_CHANGED, pattern_time_change_listener)
return hass.bus.async_listen(EVENT_TIME_CHANGED,
pattern_time_change_listener)
track_utc_time_change = _threaded_factory(async_track_utc_time_change)
# pylint: disable=too-many-arguments
def track_time_change(hass, action, year=None, month=None, day=None,
hour=None, minute=None, second=None):
def async_track_time_change(hass, action, year=None, month=None, day=None,
hour=None, minute=None, second=None):
"""Add a listener that will fire if UTC time matches a pattern."""
return track_utc_time_change(hass, action, year, month, day, hour, minute,
second, local=True)
return async_track_utc_time_change(hass, action, year, month, day, hour,
minute, second, local=True)
track_time_change = _threaded_factory(async_track_time_change)
def _process_state_match(parameter):

View File

@ -2,7 +2,7 @@
import unittest
from unittest.mock import patch
from homeassistant.bootstrap import _setup_component
from homeassistant.bootstrap import setup_component
import homeassistant.components.automation as automation
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.exceptions import HomeAssistantError
@ -31,7 +31,7 @@ class TestAutomation(unittest.TestCase):
def test_service_data_not_a_dict(self):
"""Test service data not dict."""
assert not _setup_component(self.hass, automation.DOMAIN, {
assert not setup_component(self.hass, automation.DOMAIN, {
automation.DOMAIN: {
'trigger': {
'platform': 'event',
@ -46,7 +46,7 @@ class TestAutomation(unittest.TestCase):
def test_service_specify_data(self):
"""Test service data."""
assert _setup_component(self.hass, automation.DOMAIN, {
assert setup_component(self.hass, automation.DOMAIN, {
automation.DOMAIN: {
'alias': 'hello',
'trigger': {
@ -77,7 +77,7 @@ class TestAutomation(unittest.TestCase):
def test_service_specify_entity_id(self):
"""Test service data."""
assert _setup_component(self.hass, automation.DOMAIN, {
assert setup_component(self.hass, automation.DOMAIN, {
automation.DOMAIN: {
'trigger': {
'platform': 'event',
@ -98,7 +98,7 @@ class TestAutomation(unittest.TestCase):
def test_service_specify_entity_id_list(self):
"""Test service data."""
assert _setup_component(self.hass, automation.DOMAIN, {
assert setup_component(self.hass, automation.DOMAIN, {
automation.DOMAIN: {
'trigger': {
'platform': 'event',
@ -119,7 +119,7 @@ class TestAutomation(unittest.TestCase):
def test_two_triggers(self):
"""Test triggers."""
assert _setup_component(self.hass, automation.DOMAIN, {
assert setup_component(self.hass, automation.DOMAIN, {
automation.DOMAIN: {
'trigger': [
{
@ -147,7 +147,7 @@ class TestAutomation(unittest.TestCase):
def test_two_conditions_with_and(self):
"""Test two and conditions."""
entity_id = 'test.entity'
assert _setup_component(self.hass, automation.DOMAIN, {
assert setup_component(self.hass, automation.DOMAIN, {
automation.DOMAIN: {
'trigger': [
{
@ -188,123 +188,9 @@ class TestAutomation(unittest.TestCase):
self.hass.block_till_done()
self.assertEqual(1, len(self.calls))
def test_two_conditions_with_or(self):
"""Test two or conditions."""
entity_id = 'test.entity'
assert _setup_component(self.hass, automation.DOMAIN, {
automation.DOMAIN: {
'trigger': [
{
'platform': 'event',
'event_type': 'test_event',
},
],
'condition_type': 'OR',
'condition': [
{
'platform': 'state',
'entity_id': entity_id,
'state': '200'
},
{
'platform': 'numeric_state',
'entity_id': entity_id,
'below': 150
}
],
'action': {
'service': 'test.automation',
}
}
})
self.hass.states.set(entity_id, 200)
self.hass.bus.fire('test_event')
self.hass.block_till_done()
self.assertEqual(1, len(self.calls))
self.hass.states.set(entity_id, 100)
self.hass.bus.fire('test_event')
self.hass.block_till_done()
self.assertEqual(2, len(self.calls))
self.hass.states.set(entity_id, 250)
self.hass.bus.fire('test_event')
self.hass.block_till_done()
self.assertEqual(2, len(self.calls))
def test_using_trigger_as_condition(self):
"""Test triggers as condition."""
entity_id = 'test.entity'
assert _setup_component(self.hass, automation.DOMAIN, {
automation.DOMAIN: {
'trigger': [
{
'platform': 'state',
'entity_id': entity_id,
'from': '120',
'state': '100'
},
{
'platform': 'numeric_state',
'entity_id': entity_id,
'below': 150
}
],
'condition': 'use_trigger_values',
'action': {
'service': 'test.automation',
}
}
})
self.hass.states.set(entity_id, 100)
self.hass.block_till_done()
self.assertEqual(1, len(self.calls))
self.hass.states.set(entity_id, 120)
self.hass.block_till_done()
self.assertEqual(1, len(self.calls))
self.hass.states.set(entity_id, 100)
self.hass.block_till_done()
self.assertEqual(2, len(self.calls))
self.hass.states.set(entity_id, 151)
self.hass.block_till_done()
self.assertEqual(2, len(self.calls))
def test_using_trigger_as_condition_with_invalid_condition(self):
"""Event is not a valid condition."""
entity_id = 'test.entity'
self.hass.states.set(entity_id, 100)
assert _setup_component(self.hass, automation.DOMAIN, {
automation.DOMAIN: {
'trigger': [
{
'platform': 'event',
'event_type': 'test_event',
},
{
'platform': 'numeric_state',
'entity_id': entity_id,
'below': 150
}
],
'condition': 'use_trigger_values',
'action': {
'service': 'test.automation',
}
}
})
self.hass.bus.fire('test_event')
self.hass.block_till_done()
self.assertEqual(1, len(self.calls))
def test_automation_list_setting(self):
"""Event is not a valid condition."""
self.assertTrue(_setup_component(self.hass, automation.DOMAIN, {
self.assertTrue(setup_component(self.hass, automation.DOMAIN, {
automation.DOMAIN: [{
'trigger': {
'platform': 'event',
@ -335,7 +221,7 @@ class TestAutomation(unittest.TestCase):
def test_automation_calling_two_actions(self):
"""Test if we can call two actions from automation definition."""
self.assertTrue(_setup_component(self.hass, automation.DOMAIN, {
self.assertTrue(setup_component(self.hass, automation.DOMAIN, {
automation.DOMAIN: {
'trigger': {
'platform': 'event',
@ -366,7 +252,7 @@ class TestAutomation(unittest.TestCase):
assert self.hass.states.get(entity_id) is None
assert not automation.is_on(self.hass, entity_id)
assert _setup_component(self.hass, automation.DOMAIN, {
assert setup_component(self.hass, automation.DOMAIN, {
automation.DOMAIN: {
'alias': 'hello',
'trigger': {
@ -433,7 +319,7 @@ class TestAutomation(unittest.TestCase):
})
def test_reload_config_service(self, mock_load_yaml):
"""Test the reload config service."""
assert _setup_component(self.hass, automation.DOMAIN, {
assert setup_component(self.hass, automation.DOMAIN, {
automation.DOMAIN: {
'alias': 'hello',
'trigger': {
@ -483,7 +369,7 @@ class TestAutomation(unittest.TestCase):
})
def test_reload_config_when_invalid_config(self, mock_load_yaml):
"""Test the reload config service handling invalid config."""
assert _setup_component(self.hass, automation.DOMAIN, {
assert setup_component(self.hass, automation.DOMAIN, {
automation.DOMAIN: {
'alias': 'hello',
'trigger': {
@ -517,7 +403,7 @@ class TestAutomation(unittest.TestCase):
def test_reload_config_handles_load_fails(self):
"""Test the reload config service."""
assert _setup_component(self.hass, automation.DOMAIN, {
assert setup_component(self.hass, automation.DOMAIN, {
automation.DOMAIN: {
'alias': 'hello',
'trigger': {

View File

@ -499,7 +499,7 @@ class TestAutomationNumericState(unittest.TestCase):
'event_type': 'test_event',
},
'condition': {
'platform': 'numeric_state',
'condition': 'numeric_state',
'entity_id': entity_id,
'above': test_state,
'below': test_state + 2

View File

@ -213,7 +213,7 @@ class TestAutomationState(unittest.TestCase):
'event_type': 'test_event',
},
'condition': [{
'platform': 'state',
'condition': 'state',
'entity_id': entity_id,
'state': test_state
}],
@ -360,7 +360,7 @@ class TestAutomationState(unittest.TestCase):
'event_type': 'test_event',
},
'condition': {
'platform': 'state',
'condition': 'state',
'entity_id': 'test.entity',
'state': 'on',
'for': {

View File

@ -172,7 +172,7 @@ class TestAutomationSun(unittest.TestCase):
'event_type': 'test_event',
},
'condition': {
'platform': 'sun',
'condition': 'sun',
'before': 'sunrise',
},
'action': {
@ -208,7 +208,7 @@ class TestAutomationSun(unittest.TestCase):
'event_type': 'test_event',
},
'condition': {
'platform': 'sun',
'condition': 'sun',
'after': 'sunrise',
},
'action': {
@ -244,7 +244,7 @@ class TestAutomationSun(unittest.TestCase):
'event_type': 'test_event',
},
'condition': {
'platform': 'sun',
'condition': 'sun',
'before': 'sunrise',
'before_offset': '+1:00:00'
},
@ -281,7 +281,7 @@ class TestAutomationSun(unittest.TestCase):
'event_type': 'test_event',
},
'condition': {
'platform': 'sun',
'condition': 'sun',
'after': 'sunrise',
'after_offset': '+1:00:00'
},
@ -319,7 +319,7 @@ class TestAutomationSun(unittest.TestCase):
'event_type': 'test_event',
},
'condition': {
'platform': 'sun',
'condition': 'sun',
'after': 'sunrise',
'before': 'sunset'
},
@ -365,7 +365,7 @@ class TestAutomationSun(unittest.TestCase):
'event_type': 'test_event',
},
'condition': {
'platform': 'sun',
'condition': 'sun',
'after': 'sunset',
},
'action': {

View File

@ -339,7 +339,7 @@ class TestAutomationTemplate(unittest.TestCase):
'event_type': 'test_event',
},
'condition': [{
'platform': 'template',
'condition': 'template',
'value_template': '{{ is_state("test.entity", "world") }}'
}],
'action': {

View File

@ -250,7 +250,7 @@ class TestAutomationTime(unittest.TestCase):
'event_type': 'test_event'
},
'condition': {
'platform': 'time',
'condition': 'time',
'before': '10:00',
},
'action': {
@ -285,7 +285,7 @@ class TestAutomationTime(unittest.TestCase):
'event_type': 'test_event'
},
'condition': {
'platform': 'time',
'condition': 'time',
'after': '10:00',
},
'action': {
@ -320,7 +320,7 @@ class TestAutomationTime(unittest.TestCase):
'event_type': 'test_event'
},
'condition': {
'platform': 'time',
'condition': 'time',
'weekday': 'mon',
},
'action': {
@ -356,7 +356,7 @@ class TestAutomationTime(unittest.TestCase):
'event_type': 'test_event'
},
'condition': {
'platform': 'time',
'condition': 'time',
'weekday': ['mon', 'tue'],
},
'action': {

View File

@ -197,7 +197,7 @@ class TestAutomationZone(unittest.TestCase):
'event_type': 'test_event'
},
'condition': {
'platform': 'zone',
'condition': 'zone',
'entity_id': 'test.entity',
'zone': 'zone.test',
},