New Events and Context Fixes (#18765)

* Add new events for automation trigger and script run, fix context for image processing, add tests to ensure same context

* remove custom logbook entry for automation and add new automation event to logbook

* code review updates
This commit is contained in:
Jason Hunter 2018-12-04 03:45:17 -05:00 committed by Paulus Schoutsen
parent 8e9c73eb18
commit b900005d1e
8 changed files with 214 additions and 19 deletions

View file

@ -16,7 +16,8 @@ from homeassistant.core import CoreState
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, CONF_PLATFORM, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, 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.exceptions import HomeAssistantError
from homeassistant.helpers import extract_domain_configs, script, condition from homeassistant.helpers import extract_domain_configs, script, condition
from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity import ToggleEntity
@ -286,6 +287,10 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
""" """
if skip_condition or self._cond_func(variables): if skip_condition or self._cond_func(variables):
self.async_set_context(context) 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) await self._async_action(self.entity_id, variables, context)
self._last_triggered = utcnow() self._last_triggered = utcnow()
await self.async_update_ha_state() await self.async_update_ha_state()
@ -370,8 +375,6 @@ def _async_get_action(hass, config, name):
async def action(entity_id, variables, context): async def action(entity_id, variables, context):
"""Execute an action.""" """Execute an action."""
_LOGGER.info('Executing %s', name) _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) await script_obj.async_run(variables, context)
return action return action

View file

@ -76,10 +76,14 @@ async def async_setup(hass, config):
"""Service handler for scan.""" """Service handler for scan."""
image_entities = component.async_extract_from_service(service) image_entities = component.async_extract_from_service(service)
update_task = [entity.async_update_ha_state(True) for update_tasks = []
entity in image_entities] for entity in image_entities:
if update_task: entity.async_set_context(service.context)
await asyncio.wait(update_task, loop=hass.loop) update_tasks.append(
entity.async_update_ha_state(True))
if update_tasks:
await asyncio.wait(update_tasks, loop=hass.loop)
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_SCAN, async_scan_service, DOMAIN, SERVICE_SCAN, async_scan_service,

View file

@ -17,7 +17,8 @@ from homeassistant.const import (
ATTR_DOMAIN, ATTR_ENTITY_ID, ATTR_HIDDEN, ATTR_NAME, ATTR_SERVICE, ATTR_DOMAIN, ATTR_ENTITY_ID, ATTR_HIDDEN, ATTR_NAME, ATTR_SERVICE,
CONF_EXCLUDE, CONF_INCLUDE, EVENT_HOMEASSISTANT_START, CONF_EXCLUDE, CONF_INCLUDE, EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP, EVENT_LOGBOOK_ENTRY, EVENT_STATE_CHANGED, 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 ( from homeassistant.core import (
DOMAIN as HA_DOMAIN, State, callback, split_entity_id) DOMAIN as HA_DOMAIN, State, callback, split_entity_id)
from homeassistant.components.alexa.smart_home import EVENT_ALEXA_SMART_HOME 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 '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): def _get_related_entity_ids(session, entity_filter):
from homeassistant.components.recorder.models import States from homeassistant.components.recorder.models import States

View file

@ -14,7 +14,8 @@ import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, 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.loader import bind_hass
from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity import ToggleEntity
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
@ -170,8 +171,14 @@ class ScriptEntity(ToggleEntity):
async def async_turn_on(self, **kwargs): async def async_turn_on(self, **kwargs):
"""Turn the script on.""" """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( await self.script.async_run(
kwargs.get(ATTR_VARIABLES), kwargs.get('context')) kwargs.get(ATTR_VARIABLES), context)
async def async_turn_off(self, **kwargs): async def async_turn_off(self, **kwargs):
"""Turn script off.""" """Turn script off."""

View file

@ -170,6 +170,8 @@ EVENT_SERVICE_REMOVED = 'service_removed'
EVENT_LOGBOOK_ENTRY = 'logbook_entry' EVENT_LOGBOOK_ENTRY = 'logbook_entry'
EVENT_THEMES_UPDATED = 'themes_updated' EVENT_THEMES_UPDATED = 'themes_updated'
EVENT_TIMER_OUT_OF_SYNC = 'timer_out_of_sync' EVENT_TIMER_OUT_OF_SYNC = 'timer_out_of_sync'
EVENT_AUTOMATION_TRIGGERED = 'automation_triggered'
EVENT_SCRIPT_STARTED = 'script_started'
# #### DEVICE CLASSES #### # #### DEVICE CLASSES ####
DEVICE_CLASS_BATTERY = 'battery' DEVICE_CLASS_BATTERY = 'battery'

View file

@ -1,15 +1,16 @@
"""The tests for the automation component.""" """The tests for the automation component."""
import asyncio import asyncio
from datetime import timedelta from datetime import timedelta
from unittest.mock import patch from unittest.mock import patch, Mock
import pytest import pytest
from homeassistant.core import State, CoreState from homeassistant.core import State, CoreState, Context
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
import homeassistant.components.automation as automation import homeassistant.components.automation as automation
from homeassistant.const import ( 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 from homeassistant.exceptions import HomeAssistantError
import homeassistant.util.dt as dt_util 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 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): async def test_services(hass, calls):
"""Test the automation services for turning entities on/off.""" """Test the automation services for turning entities on/off."""
entity_id = 'automation.hello' entity_id = 'automation.hello'

View file

@ -10,9 +10,10 @@ import voluptuous as vol
from homeassistant.components import sun from homeassistant.components import sun
import homeassistant.core as ha import homeassistant.core as ha
from homeassistant.const import ( 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, 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 import homeassistant.util.dt as dt_util
from homeassistant.components import logbook, recorder from homeassistant.components import logbook, recorder
from homeassistant.components.alexa.smart_home import EVENT_ALEXA_SMART_HOME 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 event1['entity_id'] == 'lock.front_door'
assert event2['name'] == 'HomeKit' assert event2['name'] == 'HomeKit'
assert event1['domain'] == DOMAIN_HOMEKIT assert event2['domain'] == DOMAIN_HOMEKIT
assert event2['message'] == \ assert event2['message'] == \
'send command set_cover_position to 75 for Window' 'send command set_cover_position to 75 for Window'
assert event2['entity_id'] == 'cover.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'

View file

@ -1,15 +1,16 @@
"""The tests for the Script component.""" """The tests for the Script component."""
# pylint: disable=protected-access # pylint: disable=protected-access
import unittest import unittest
from unittest.mock import patch from unittest.mock import patch, Mock
from homeassistant.components import script from homeassistant.components import script
from homeassistant.components.script import DOMAIN from homeassistant.components.script import DOMAIN
from homeassistant.const import ( 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.core import Context, callback, split_entity_id
from homeassistant.loader import bind_hass 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 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.states.get("script.test2") is not None
assert self.hass.services.has_service(script.DOMAIN, 'test2') 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