From 746f4ac1585ac47be3bcc5d06d79b54da4a4e900 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 4 Sep 2018 21:16:24 +0200 Subject: [PATCH] Add context to scripts and automations (#16415) * Add context to script helper * Update script component * Add context to automations * Lint --- .../components/automation/__init__.py | 91 ++++++++----------- homeassistant/components/automation/event.py | 4 +- .../components/automation/homeassistant.py | 8 +- .../components/automation/numeric_state.py | 4 +- homeassistant/components/automation/state.py | 4 +- .../components/automation/template.py | 4 +- homeassistant/components/automation/zone.py | 4 +- homeassistant/components/script.py | 57 +++++------- homeassistant/helpers/script.py | 46 ++++++---- homeassistant/helpers/service.py | 4 +- tests/components/automation/test_event.py | 7 +- .../automation/test_numeric_state.py | 10 +- tests/components/automation/test_state.py | 6 +- tests/components/automation/test_template.py | 12 +-- tests/components/automation/test_zone.py | 6 +- tests/components/test_script.py | 13 ++- tests/helpers/test_script.py | 28 ++++-- 17 files changed, 164 insertions(+), 144 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index c6c0af90d15..43fd4cedb88 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -158,27 +158,26 @@ def async_reload(hass): return hass.services.async_call(DOMAIN, SERVICE_RELOAD) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the automation.""" component = EntityComponent(_LOGGER, DOMAIN, hass, group_name=GROUP_NAME_ALL_AUTOMATIONS) - yield from _async_process_config(hass, config, component) + await _async_process_config(hass, config, component) - @asyncio.coroutine - def trigger_service_handler(service_call): + async def trigger_service_handler(service_call): """Handle automation triggers.""" tasks = [] for entity in component.async_extract_from_service(service_call): tasks.append(entity.async_trigger( - service_call.data.get(ATTR_VARIABLES), True)) + service_call.data.get(ATTR_VARIABLES), + skip_condition=True, + context=service_call.context)) if tasks: - yield from asyncio.wait(tasks, loop=hass.loop) + await asyncio.wait(tasks, loop=hass.loop) - @asyncio.coroutine - def turn_onoff_service_handler(service_call): + async def turn_onoff_service_handler(service_call): """Handle automation turn on/off service calls.""" tasks = [] method = 'async_{}'.format(service_call.service) @@ -186,10 +185,9 @@ def async_setup(hass, config): tasks.append(getattr(entity, method)()) if tasks: - yield from asyncio.wait(tasks, loop=hass.loop) + await asyncio.wait(tasks, loop=hass.loop) - @asyncio.coroutine - def toggle_service_handler(service_call): + async def toggle_service_handler(service_call): """Handle automation toggle service calls.""" tasks = [] for entity in component.async_extract_from_service(service_call): @@ -199,15 +197,14 @@ def async_setup(hass, config): tasks.append(entity.async_turn_on()) if tasks: - yield from asyncio.wait(tasks, loop=hass.loop) + await asyncio.wait(tasks, loop=hass.loop) - @asyncio.coroutine - def reload_service_handler(service_call): + async def reload_service_handler(service_call): """Remove all automations and load new ones from config.""" - conf = yield from component.async_prepare_reload() + conf = await component.async_prepare_reload() if conf is None: return - yield from _async_process_config(hass, conf, component) + await _async_process_config(hass, conf, component) hass.services.async_register( DOMAIN, SERVICE_TRIGGER, trigger_service_handler, @@ -272,15 +269,14 @@ class AutomationEntity(ToggleEntity): """Return True if entity is on.""" return self._async_detach_triggers is not None - @asyncio.coroutine - def async_added_to_hass(self) -> None: + async def async_added_to_hass(self) -> None: """Startup with initial state or previous state.""" if self._initial_state is not None: enable_automation = self._initial_state _LOGGER.debug("Automation %s initial state %s from config " "initial_state", self.entity_id, enable_automation) else: - state = yield from async_get_last_state(self.hass, self.entity_id) + state = await async_get_last_state(self.hass, self.entity_id) if state: enable_automation = state.state == STATE_ON self._last_triggered = state.attributes.get('last_triggered') @@ -298,54 +294,50 @@ class AutomationEntity(ToggleEntity): # HomeAssistant is starting up if self.hass.state == CoreState.not_running: - @asyncio.coroutine - def async_enable_automation(event): + async def async_enable_automation(event): """Start automation on startup.""" - yield from self.async_enable() + await self.async_enable() self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_START, async_enable_automation) # HomeAssistant is running else: - yield from self.async_enable() + await self.async_enable() - @asyncio.coroutine - def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs) -> None: """Turn the entity on and update the state.""" if self.is_on: return - yield from self.async_enable() + await self.async_enable() - @asyncio.coroutine - def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs) -> None: """Turn the entity off.""" if not self.is_on: return self._async_detach_triggers() self._async_detach_triggers = None - yield from self.async_update_ha_state() + await self.async_update_ha_state() - @asyncio.coroutine - def async_trigger(self, variables, skip_condition=False): + async def async_trigger(self, variables, skip_condition=False, + context=None): """Trigger automation. This method is a coroutine. """ if skip_condition or self._cond_func(variables): - yield from self._async_action(self.entity_id, variables) + self.async_set_context(context) + await self._async_action(self.entity_id, variables, context) self._last_triggered = utcnow() - yield from self.async_update_ha_state() + await self.async_update_ha_state() - @asyncio.coroutine - def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self): """Remove listeners when removing automation from HASS.""" - yield from self.async_turn_off() + await self.async_turn_off() - @asyncio.coroutine - def async_enable(self): + async def async_enable(self): """Enable this automation entity. This method is a coroutine. @@ -353,9 +345,9 @@ class AutomationEntity(ToggleEntity): if self.is_on: return - self._async_detach_triggers = yield from self._async_attach_triggers( + self._async_detach_triggers = await self._async_attach_triggers( self.async_trigger) - yield from self.async_update_ha_state() + await self.async_update_ha_state() @property def device_state_attributes(self): @@ -368,8 +360,7 @@ class AutomationEntity(ToggleEntity): } -@asyncio.coroutine -def _async_process_config(hass, config, component): +async def _async_process_config(hass, config, component): """Process config and add automations. This method is a coroutine. @@ -411,20 +402,19 @@ def _async_process_config(hass, config, component): entities.append(entity) if entities: - yield from component.async_add_entities(entities) + await component.async_add_entities(entities) 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(entity_id, variables): + async def action(entity_id, variables, context): """Execute an action.""" _LOGGER.info('Executing %s', name) logbook.async_log_entry( hass, name, 'has been triggered', DOMAIN, entity_id) - yield from script_obj.async_run(variables) + await script_obj.async_run(variables, context) return action @@ -448,8 +438,7 @@ def _async_process_if(hass, config, p_config): return if_action -@asyncio.coroutine -def _async_process_trigger(hass, config, trigger_configs, name, action): +async def _async_process_trigger(hass, config, trigger_configs, name, action): """Set up the triggers. This method is a coroutine. @@ -457,13 +446,13 @@ def _async_process_trigger(hass, config, trigger_configs, name, action): removes = [] for conf in trigger_configs: - platform = yield from async_prepare_setup_platform( + platform = await async_prepare_setup_platform( hass, config, DOMAIN, conf.get(CONF_PLATFORM)) if platform is None: return None - remove = yield from platform.async_trigger(hass, conf, action) + remove = await platform.async_trigger(hass, conf, action) if not remove: _LOGGER.error("Error setting up trigger %s", name) diff --git a/homeassistant/components/automation/event.py b/homeassistant/components/automation/event.py index 7c035d7d1a5..e19a85edae6 100644 --- a/homeassistant/components/automation/event.py +++ b/homeassistant/components/automation/event.py @@ -45,11 +45,11 @@ def async_trigger(hass, config, action): # If event data doesn't match requested schema, skip event return - hass.async_run_job(action, { + hass.async_run_job(action({ 'trigger': { 'platform': 'event', 'event': event, }, - }) + }, context=event.context)) return hass.bus.async_listen(event_type, handle_event) diff --git a/homeassistant/components/automation/homeassistant.py b/homeassistant/components/automation/homeassistant.py index 74cf195bc61..b55d99f706a 100644 --- a/homeassistant/components/automation/homeassistant.py +++ b/homeassistant/components/automation/homeassistant.py @@ -32,12 +32,12 @@ def async_trigger(hass, config, action): @callback def hass_shutdown(event): """Execute when Home Assistant is shutting down.""" - hass.async_run_job(action, { + hass.async_run_job(action({ 'trigger': { 'platform': 'homeassistant', 'event': event, }, - }) + }, context=event.context)) return hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, hass_shutdown) @@ -45,11 +45,11 @@ def async_trigger(hass, config, action): # Automation are enabled while hass is starting up, fire right away # Check state because a config reload shouldn't trigger it. if hass.state == CoreState.starting: - hass.async_run_job(action, { + hass.async_run_job(action({ 'trigger': { 'platform': 'homeassistant', 'event': event, }, - }) + })) return lambda: None diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index b59271f25e5..f0dcbf0be57 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -66,7 +66,7 @@ def async_trigger(hass, config, action): @callback def call_action(): """Call action with right context.""" - hass.async_run_job(action, { + hass.async_run_job(action({ 'trigger': { 'platform': 'numeric_state', 'entity_id': entity, @@ -75,7 +75,7 @@ def async_trigger(hass, config, action): 'from_state': from_s, 'to_state': to_s, } - }) + }, context=to_s.context)) matching = check_numeric_state(entity, from_s, to_s) diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index 9243f960850..263d4158e25 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -43,7 +43,7 @@ def async_trigger(hass, config, action): @callback def call_action(): """Call action with right context.""" - hass.async_run_job(action, { + hass.async_run_job(action({ 'trigger': { 'platform': 'state', 'entity_id': entity, @@ -51,7 +51,7 @@ def async_trigger(hass, config, action): 'to_state': to_s, 'for': time_delta, } - }) + }, context=to_s.context)) # Ignore changes to state attributes if from/to is in use if (not match_all and from_s is not None and to_s is not None and diff --git a/homeassistant/components/automation/template.py b/homeassistant/components/automation/template.py index 0fcdeaae5e0..67a44f1a347 100644 --- a/homeassistant/components/automation/template.py +++ b/homeassistant/components/automation/template.py @@ -32,13 +32,13 @@ def async_trigger(hass, config, action): @callback def template_listener(entity_id, from_s, to_s): """Listen for state changes and calls action.""" - hass.async_run_job(action, { + hass.async_run_job(action({ 'trigger': { 'platform': 'template', 'entity_id': entity_id, 'from_state': from_s, 'to_state': to_s, }, - }) + }, context=to_s.context)) return async_track_template(hass, value_template, template_listener) diff --git a/homeassistant/components/automation/zone.py b/homeassistant/components/automation/zone.py index 61d846582cb..f30dfe753cb 100644 --- a/homeassistant/components/automation/zone.py +++ b/homeassistant/components/automation/zone.py @@ -51,7 +51,7 @@ def async_trigger(hass, config, action): # pylint: disable=too-many-boolean-expressions if event == EVENT_ENTER and not from_match and to_match or \ event == EVENT_LEAVE and from_match and not to_match: - hass.async_run_job(action, { + hass.async_run_job(action({ 'trigger': { 'platform': 'zone', 'entity_id': entity, @@ -60,7 +60,7 @@ def async_trigger(hass, config, action): 'zone': zone_state, 'event': event, }, - }) + }, context=to_s.context)) return async_track_state_change(hass, entity_id, zone_automation_listener, MATCH_ALL, MATCH_ALL) diff --git a/homeassistant/components/script.py b/homeassistant/components/script.py index a45f8ba8930..247ac07283e 100644 --- a/homeassistant/components/script.py +++ b/homeassistant/components/script.py @@ -63,11 +63,11 @@ def is_on(hass, entity_id): @bind_hass -def turn_on(hass, entity_id, variables=None): +def turn_on(hass, entity_id, variables=None, context=None): """Turn script on.""" _, object_id = split_entity_id(entity_id) - hass.services.call(DOMAIN, object_id, variables) + hass.services.call(DOMAIN, object_id, variables, context=context) @bind_hass @@ -97,45 +97,41 @@ def async_reload(hass): return hass.services.async_call(DOMAIN, SERVICE_RELOAD) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Load the scripts from the configuration.""" component = EntityComponent( _LOGGER, DOMAIN, hass, group_name=GROUP_NAME_ALL_SCRIPTS) - yield from _async_process_config(hass, config, component) + await _async_process_config(hass, config, component) - @asyncio.coroutine - def reload_service(service): + async def reload_service(service): """Call a service to reload scripts.""" - conf = yield from component.async_prepare_reload() + conf = await component.async_prepare_reload() if conf is None: return - yield from _async_process_config(hass, conf, component) + await _async_process_config(hass, conf, component) - @asyncio.coroutine - def turn_on_service(service): + async def turn_on_service(service): """Call a service to turn script on.""" # We could turn on script directly here, but we only want to offer # one way to do it. Otherwise no easy way to detect invocations. var = service.data.get(ATTR_VARIABLES) for script in component.async_extract_from_service(service): - yield from hass.services.async_call(DOMAIN, script.object_id, var) + await hass.services.async_call(DOMAIN, script.object_id, var, + context=service.context) - @asyncio.coroutine - def turn_off_service(service): + async def turn_off_service(service): """Cancel a script.""" # Stopping a script is ok to be done in parallel - yield from asyncio.wait( + await asyncio.wait( [script.async_turn_off() for script in component.async_extract_from_service(service)], loop=hass.loop) - @asyncio.coroutine - def toggle_service(service): + async def toggle_service(service): """Toggle a script.""" for script in component.async_extract_from_service(service): - yield from script.async_toggle() + await script.async_toggle(context=service.context) hass.services.async_register(DOMAIN, SERVICE_RELOAD, reload_service, schema=RELOAD_SERVICE_SCHEMA) @@ -149,18 +145,17 @@ def async_setup(hass, config): return True -@asyncio.coroutine -def _async_process_config(hass, config, component): - """Process group configuration.""" - @asyncio.coroutine - def service_handler(service): +async def _async_process_config(hass, config, component): + """Process script configuration.""" + async def service_handler(service): """Execute a service call to script.