diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index b65516130d9..37102cd3863 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -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 diff --git a/homeassistant/components/automation/event.py b/homeassistant/components/automation/event.py index 7b77dd04627..ae0f219a01a 100644 --- a/homeassistant/components/automation/event.py +++ b/homeassistant/components/automation/event.py @@ -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) diff --git a/homeassistant/components/automation/mqtt.py b/homeassistant/components/automation/mqtt.py index f774991c547..7897a9bc221 100644 --- a/homeassistant/components/automation/mqtt.py +++ b/homeassistant/components/automation/mqtt.py @@ -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) diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index 780ab4400b0..4d6cdc21190 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -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) diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index dbe74479070..0649834ff33 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -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 diff --git a/homeassistant/components/automation/sun.py b/homeassistant/components/automation/sun.py index faa628f572a..9892707a139 100644 --- a/homeassistant/components/automation/sun.py +++ b/homeassistant/components/automation/sun.py @@ -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) diff --git a/homeassistant/components/automation/template.py b/homeassistant/components/automation/template.py index 1ca0c679424..94f57dbbc02 100644 --- a/homeassistant/components/automation/template.py +++ b/homeassistant/components/automation/template.py @@ -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) diff --git a/homeassistant/components/automation/time.py b/homeassistant/components/automation/time.py index 91f196eaf3f..190a6519278 100644 --- a/homeassistant/components/automation/time.py +++ b/homeassistant/components/automation/time.py @@ -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) diff --git a/homeassistant/components/automation/zone.py b/homeassistant/components/automation/zone.py index 971257350e3..59812738692 100644 --- a/homeassistant/components/automation/zone.py +++ b/homeassistant/components/automation/zone.py @@ -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) diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index ae1dc471706..bfc637cf946 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -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) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 3c119eb456e..99384764b5b 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -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) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index e27f711afda..bf781e7e746 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -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): diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index cc10b134caf..b667436d9a6 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -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': { diff --git a/tests/components/automation/test_numeric_state.py b/tests/components/automation/test_numeric_state.py index 01ce5e7c4e6..fecd8474763 100644 --- a/tests/components/automation/test_numeric_state.py +++ b/tests/components/automation/test_numeric_state.py @@ -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 diff --git a/tests/components/automation/test_state.py b/tests/components/automation/test_state.py index 7132bf9e7a2..d6fe56453ee 100644 --- a/tests/components/automation/test_state.py +++ b/tests/components/automation/test_state.py @@ -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': { diff --git a/tests/components/automation/test_sun.py b/tests/components/automation/test_sun.py index 8058854aaa9..815c540eb3e 100644 --- a/tests/components/automation/test_sun.py +++ b/tests/components/automation/test_sun.py @@ -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': { diff --git a/tests/components/automation/test_template.py b/tests/components/automation/test_template.py index 327bedb23f2..1334608ecdb 100644 --- a/tests/components/automation/test_template.py +++ b/tests/components/automation/test_template.py @@ -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': { diff --git a/tests/components/automation/test_time.py b/tests/components/automation/test_time.py index edc5fb4cdc1..afdab681460 100644 --- a/tests/components/automation/test_time.py +++ b/tests/components/automation/test_time.py @@ -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': { diff --git a/tests/components/automation/test_zone.py b/tests/components/automation/test_zone.py index 8041eceaeb3..e454b8b5b8b 100644 --- a/tests/components/automation/test_zone.py +++ b/tests/components/automation/test_zone.py @@ -197,7 +197,7 @@ class TestAutomationZone(unittest.TestCase): 'event_type': 'test_event' }, 'condition': { - 'platform': 'zone', + 'condition': 'zone', 'entity_id': 'test.entity', 'zone': 'zone.test', },