diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index f44d044ecfa..4a2df399e0a 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -16,7 +16,8 @@ from homeassistant.core import CoreState from homeassistant.loader import bind_hass from homeassistant.const import ( ATTR_ENTITY_ID, CONF_PLATFORM, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, - SERVICE_TOGGLE, SERVICE_RELOAD, EVENT_HOMEASSISTANT_START, CONF_ID) + SERVICE_TOGGLE, SERVICE_RELOAD, EVENT_HOMEASSISTANT_START, CONF_ID, + EVENT_AUTOMATION_TRIGGERED, ATTR_NAME) from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import extract_domain_configs, script, condition from homeassistant.helpers.entity import ToggleEntity @@ -286,6 +287,10 @@ class AutomationEntity(ToggleEntity, RestoreEntity): """ if skip_condition or self._cond_func(variables): self.async_set_context(context) + self.hass.bus.async_fire(EVENT_AUTOMATION_TRIGGERED, { + ATTR_NAME: self._name, + ATTR_ENTITY_ID: self.entity_id, + }, context=context) await self._async_action(self.entity_id, variables, context) self._last_triggered = utcnow() await self.async_update_ha_state() @@ -370,8 +375,6 @@ def _async_get_action(hass, config, name): async def action(entity_id, variables, context): """Execute an action.""" _LOGGER.info('Executing %s', name) - hass.components.logbook.async_log_entry( - name, 'has been triggered', DOMAIN, entity_id) await script_obj.async_run(variables, context) return action diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index 84d92361541..72a4a8155e2 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -76,10 +76,14 @@ async def async_setup(hass, config): """Service handler for scan.""" image_entities = component.async_extract_from_service(service) - update_task = [entity.async_update_ha_state(True) for - entity in image_entities] - if update_task: - await asyncio.wait(update_task, loop=hass.loop) + update_tasks = [] + for entity in image_entities: + entity.async_set_context(service.context) + update_tasks.append( + entity.async_update_ha_state(True)) + + if update_tasks: + await asyncio.wait(update_tasks, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_SCAN, async_scan_service, diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index b6f434a82ad..78da5733a06 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -17,7 +17,8 @@ from homeassistant.const import ( ATTR_DOMAIN, ATTR_ENTITY_ID, ATTR_HIDDEN, ATTR_NAME, ATTR_SERVICE, CONF_EXCLUDE, CONF_INCLUDE, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_LOGBOOK_ENTRY, EVENT_STATE_CHANGED, - HTTP_BAD_REQUEST, STATE_NOT_HOME, STATE_OFF, STATE_ON) + EVENT_AUTOMATION_TRIGGERED, EVENT_SCRIPT_STARTED, HTTP_BAD_REQUEST, + STATE_NOT_HOME, STATE_OFF, STATE_ON) from homeassistant.core import ( DOMAIN as HA_DOMAIN, State, callback, split_entity_id) from homeassistant.components.alexa.smart_home import EVENT_ALEXA_SMART_HOME @@ -316,6 +317,28 @@ def humanify(hass, events): 'context_user_id': event.context.user_id } + elif event.event_type == EVENT_AUTOMATION_TRIGGERED: + yield { + 'when': event.time_fired, + 'name': event.data.get(ATTR_NAME), + 'message': "has been triggered", + 'domain': 'automation', + 'entity_id': event.data.get(ATTR_ENTITY_ID), + 'context_id': event.context.id, + 'context_user_id': event.context.user_id + } + + elif event.event_type == EVENT_SCRIPT_STARTED: + yield { + 'when': event.time_fired, + 'name': event.data.get(ATTR_NAME), + 'message': 'started', + 'domain': 'script', + 'entity_id': event.data.get(ATTR_ENTITY_ID), + 'context_id': event.context.id, + 'context_user_id': event.context.user_id + } + def _get_related_entity_ids(session, entity_filter): from homeassistant.components.recorder.models import States diff --git a/homeassistant/components/script.py b/homeassistant/components/script.py index 16c9f65420c..54490af3cfa 100644 --- a/homeassistant/components/script.py +++ b/homeassistant/components/script.py @@ -14,7 +14,8 @@ import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, - SERVICE_TOGGLE, SERVICE_RELOAD, STATE_ON, CONF_ALIAS) + SERVICE_TOGGLE, SERVICE_RELOAD, STATE_ON, CONF_ALIAS, + EVENT_SCRIPT_STARTED, ATTR_NAME) from homeassistant.loader import bind_hass from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent @@ -170,8 +171,14 @@ class ScriptEntity(ToggleEntity): async def async_turn_on(self, **kwargs): """Turn the script on.""" + context = kwargs.get('context') + self.async_set_context(context) + self.hass.bus.async_fire(EVENT_SCRIPT_STARTED, { + ATTR_NAME: self.script.name, + ATTR_ENTITY_ID: self.entity_id, + }, context=context) await self.script.async_run( - kwargs.get(ATTR_VARIABLES), kwargs.get('context')) + kwargs.get(ATTR_VARIABLES), context) async def async_turn_off(self, **kwargs): """Turn script off.""" diff --git a/homeassistant/const.py b/homeassistant/const.py index eb53140339a..b4a94d318f6 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -170,6 +170,8 @@ EVENT_SERVICE_REMOVED = 'service_removed' EVENT_LOGBOOK_ENTRY = 'logbook_entry' EVENT_THEMES_UPDATED = 'themes_updated' EVENT_TIMER_OUT_OF_SYNC = 'timer_out_of_sync' +EVENT_AUTOMATION_TRIGGERED = 'automation_triggered' +EVENT_SCRIPT_STARTED = 'script_started' # #### DEVICE CLASSES #### DEVICE_CLASS_BATTERY = 'battery' diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 28d4c0979c4..a01b48b9190 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -1,15 +1,16 @@ """The tests for the automation component.""" import asyncio from datetime import timedelta -from unittest.mock import patch +from unittest.mock import patch, Mock import pytest -from homeassistant.core import State, CoreState +from homeassistant.core import State, CoreState, Context from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation from homeassistant.const import ( - ATTR_ENTITY_ID, STATE_ON, STATE_OFF, EVENT_HOMEASSISTANT_START) + ATTR_NAME, ATTR_ENTITY_ID, STATE_ON, STATE_OFF, + EVENT_HOMEASSISTANT_START, EVENT_AUTOMATION_TRIGGERED) from homeassistant.exceptions import HomeAssistantError import homeassistant.util.dt as dt_util @@ -342,6 +343,66 @@ async def test_automation_calling_two_actions(hass, calls): assert calls[1].data['position'] == 1 +async def test_shared_context(hass, calls): + """Test that the shared context is passed down the chain.""" + assert await async_setup_component(hass, automation.DOMAIN, { + automation.DOMAIN: [ + { + 'alias': 'hello', + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event', + }, + 'action': {'event': 'test_event2'} + }, + { + 'alias': 'bye', + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event2', + }, + 'action': { + 'service': 'test.automation', + } + } + ] + }) + + context = Context() + automation_mock = Mock() + event_mock = Mock() + + hass.bus.async_listen('test_event2', automation_mock) + hass.bus.async_listen(EVENT_AUTOMATION_TRIGGERED, event_mock) + hass.bus.async_fire('test_event', context=context) + await hass.async_block_till_done() + + # Ensure events was fired + assert automation_mock.call_count == 1 + assert event_mock.call_count == 2 + + # Ensure context carries through the event + args, kwargs = automation_mock.call_args + assert args[0].context == context + + for call in event_mock.call_args_list: + args, kwargs = call + assert args[0].context == context + # Ensure event data has all attributes set + assert args[0].data.get(ATTR_NAME) is not None + assert args[0].data.get(ATTR_ENTITY_ID) is not None + + # Ensure the automation state shares the same context + state = hass.states.get('automation.hello') + assert state is not None + assert state.context == context + + # Ensure the service call from the second automation + # shares the same context + assert len(calls) == 1 + assert calls[0].context == context + + async def test_services(hass, calls): """Test the automation services for turning entities on/off.""" entity_id = 'automation.hello' diff --git a/tests/components/test_logbook.py b/tests/components/test_logbook.py index b530c3dac3c..321a16ae64e 100644 --- a/tests/components/test_logbook.py +++ b/tests/components/test_logbook.py @@ -10,9 +10,10 @@ import voluptuous as vol from homeassistant.components import sun import homeassistant.core as ha from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_SERVICE, + ATTR_ENTITY_ID, ATTR_SERVICE, ATTR_NAME, EVENT_STATE_CHANGED, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - ATTR_HIDDEN, STATE_NOT_HOME, STATE_ON, STATE_OFF) + EVENT_AUTOMATION_TRIGGERED, EVENT_SCRIPT_STARTED, ATTR_HIDDEN, + STATE_NOT_HOME, STATE_ON, STATE_OFF) import homeassistant.util.dt as dt_util from homeassistant.components import logbook, recorder from homeassistant.components.alexa.smart_home import EVENT_ALEXA_SMART_HOME @@ -751,7 +752,55 @@ async def test_humanify_homekit_changed_event(hass): assert event1['entity_id'] == 'lock.front_door' assert event2['name'] == 'HomeKit' - assert event1['domain'] == DOMAIN_HOMEKIT + assert event2['domain'] == DOMAIN_HOMEKIT assert event2['message'] == \ 'send command set_cover_position to 75 for Window' assert event2['entity_id'] == 'cover.window' + + +async def test_humanify_automation_triggered_event(hass): + """Test humanifying Automation Trigger event.""" + event1, event2 = list(logbook.humanify(hass, [ + ha.Event(EVENT_AUTOMATION_TRIGGERED, { + ATTR_ENTITY_ID: 'automation.hello', + ATTR_NAME: 'Hello Automation', + }), + ha.Event(EVENT_AUTOMATION_TRIGGERED, { + ATTR_ENTITY_ID: 'automation.bye', + ATTR_NAME: 'Bye Automation', + }), + ])) + + assert event1['name'] == 'Hello Automation' + assert event1['domain'] == 'automation' + assert event1['message'] == 'has been triggered' + assert event1['entity_id'] == 'automation.hello' + + assert event2['name'] == 'Bye Automation' + assert event2['domain'] == 'automation' + assert event2['message'] == 'has been triggered' + assert event2['entity_id'] == 'automation.bye' + + +async def test_humanify_script_started_event(hass): + """Test humanifying Script Run event.""" + event1, event2 = list(logbook.humanify(hass, [ + ha.Event(EVENT_SCRIPT_STARTED, { + ATTR_ENTITY_ID: 'script.hello', + ATTR_NAME: 'Hello Script' + }), + ha.Event(EVENT_SCRIPT_STARTED, { + ATTR_ENTITY_ID: 'script.bye', + ATTR_NAME: 'Bye Script' + }), + ])) + + assert event1['name'] == 'Hello Script' + assert event1['domain'] == 'script' + assert event1['message'] == 'started' + assert event1['entity_id'] == 'script.hello' + + assert event2['name'] == 'Bye Script' + assert event2['domain'] == 'script' + assert event2['message'] == 'started' + assert event2['entity_id'] == 'script.bye' diff --git a/tests/components/test_script.py b/tests/components/test_script.py index 5b7d0dfb70f..790d5c2e844 100644 --- a/tests/components/test_script.py +++ b/tests/components/test_script.py @@ -1,15 +1,16 @@ """The tests for the Script component.""" # pylint: disable=protected-access import unittest -from unittest.mock import patch +from unittest.mock import patch, Mock from homeassistant.components import script from homeassistant.components.script import DOMAIN from homeassistant.const import ( - ATTR_ENTITY_ID, SERVICE_RELOAD, SERVICE_TOGGLE, SERVICE_TURN_OFF) + ATTR_ENTITY_ID, ATTR_NAME, SERVICE_RELOAD, SERVICE_TOGGLE, + SERVICE_TURN_OFF, SERVICE_TURN_ON, EVENT_SCRIPT_STARTED) from homeassistant.core import Context, callback, split_entity_id from homeassistant.loader import bind_hass -from homeassistant.setup import setup_component +from homeassistant.setup import setup_component, async_setup_component from tests.common import get_test_home_assistant @@ -254,3 +255,48 @@ class TestScriptComponent(unittest.TestCase): assert self.hass.states.get("script.test2") is not None assert self.hass.services.has_service(script.DOMAIN, 'test2') + + +async def test_shared_context(hass): + """Test that the shared context is passed down the chain.""" + event = 'test_event' + context = Context() + + event_mock = Mock() + run_mock = Mock() + + hass.bus.async_listen(event, event_mock) + hass.bus.async_listen(EVENT_SCRIPT_STARTED, run_mock) + + assert await async_setup_component(hass, 'script', { + 'script': { + 'test': { + 'sequence': [ + {'event': event} + ] + } + } + }) + + await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_ID}, + context=context) + await hass.async_block_till_done() + + assert event_mock.call_count == 1 + assert run_mock.call_count == 1 + + args, kwargs = run_mock.call_args + assert args[0].context == context + # Ensure event data has all attributes set + assert args[0].data.get(ATTR_NAME) == 'test' + assert args[0].data.get(ATTR_ENTITY_ID) == 'script.test' + + # Ensure context carries through the event + args, kwargs = event_mock.call_args + assert args[0].context == context + + # Ensure the script state shares the same context + state = hass.states.get('script.test') + assert state is not None + assert state.context == context