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:
parent
8e9c73eb18
commit
b900005d1e
8 changed files with 214 additions and 19 deletions
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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."""
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue