Add description of what caused an automation trigger to fire (#39251)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Phil Bruckner 2020-08-28 10:02:12 -05:00 committed by GitHub
parent 5217139e0b
commit e6141ae558
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 119 additions and 35 deletions

View file

@ -59,11 +59,16 @@ async def async_attach_trigger(
config = TRIGGER_SCHEMA(config) config = TRIGGER_SCHEMA(config)
if config[CONF_TYPE] == "turn_on": if config[CONF_TYPE] == "turn_on":
entity_id = config[CONF_ENTITY_ID]
@callback @callback
def _handle_event(event: Event): def _handle_event(event: Event):
if event.data[ATTR_ENTITY_ID] == config[CONF_ENTITY_ID]: if event.data[ATTR_ENTITY_ID] == entity_id:
hass.async_run_job(action({"trigger": config}, context=event.context)) hass.async_run_job(
action,
{"trigger": {**config, "description": f"{DOMAIN} - {entity_id}"}},
event.context,
)
return hass.bus.async_listen(EVENT_TURN_ON, _handle_event) return hass.bus.async_listen(EVENT_TURN_ON, _handle_event)

View file

@ -81,6 +81,7 @@ EVENT_AUTOMATION_RELOADED = "automation_reloaded"
EVENT_AUTOMATION_TRIGGERED = "automation_triggered" EVENT_AUTOMATION_TRIGGERED = "automation_triggered"
ATTR_LAST_TRIGGERED = "last_triggered" ATTR_LAST_TRIGGERED = "last_triggered"
ATTR_SOURCE = "source"
ATTR_VARIABLES = "variables" ATTR_VARIABLES = "variables"
SERVICE_TRIGGER = "trigger" SERVICE_TRIGGER = "trigger"
@ -396,10 +397,14 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
self.async_set_context(trigger_context) self.async_set_context(trigger_context)
self._last_triggered = utcnow() self._last_triggered = utcnow()
self.async_write_ha_state() self.async_write_ha_state()
event_data = {
ATTR_NAME: self._name,
ATTR_ENTITY_ID: self.entity_id,
}
if "trigger" in variables and "description" in variables["trigger"]:
event_data[ATTR_SOURCE] = variables["trigger"]["description"]
self.hass.bus.async_fire( self.hass.bus.async_fire(
EVENT_AUTOMATION_TRIGGERED, EVENT_AUTOMATION_TRIGGERED, event_data, context=trigger_context
{ATTR_NAME: self._name, ATTR_ENTITY_ID: self.entity_id},
context=trigger_context,
) )
self._logger.info("Executing %s", self._name) self._logger.info("Executing %s", self._name)

View file

@ -2,7 +2,7 @@
from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME
from homeassistant.core import callback from homeassistant.core import callback
from . import DOMAIN, EVENT_AUTOMATION_TRIGGERED from . import ATTR_SOURCE, DOMAIN, EVENT_AUTOMATION_TRIGGERED
@callback @callback
@ -12,9 +12,13 @@ def async_describe_events(hass, async_describe_event): # type: ignore
@callback @callback
def async_describe_logbook_event(event): # type: ignore def async_describe_logbook_event(event): # type: ignore
"""Describe a logbook event.""" """Describe a logbook event."""
message = "has been triggered"
if ATTR_SOURCE in event.data:
message = f"{message} by {event.data[ATTR_SOURCE]}"
return { return {
"name": event.data.get(ATTR_NAME), "name": event.data.get(ATTR_NAME),
"message": "has been triggered", "message": message,
"source": event.data.get(ATTR_SOURCE),
"entity_id": event.data.get(ATTR_ENTITY_ID), "entity_id": event.data.get(ATTR_ENTITY_ID),
} }

View file

@ -67,7 +67,7 @@ async def async_attach_trigger(hass, config, action, automation_info):
and not to_match and not to_match
): ):
hass.async_run_job( hass.async_run_job(
action( action,
{ {
"trigger": { "trigger": {
"platform": "geo_location", "platform": "geo_location",
@ -77,10 +77,10 @@ async def async_attach_trigger(hass, config, action, automation_info):
"to_state": to_state, "to_state": to_state,
"zone": zone_state, "zone": zone_state,
"event": trigger_event, "event": trigger_event,
"description": f"geo_location - {source}",
} }
}, },
context=event.context, event.context,
)
) )
return hass.bus.async_listen(EVENT_STATE_CHANGED, state_change_listener) return hass.bus.async_listen(EVENT_STATE_CHANGED, state_change_listener)

View file

@ -48,7 +48,13 @@ async def async_attach_trigger(
hass.async_run_job( hass.async_run_job(
action, action,
{"trigger": {"platform": platform_type, "event": event}}, {
"trigger": {
"platform": platform_type,
"event": event,
"description": f"event '{event.event_type}'",
}
},
event.context, event.context,
) )

View file

@ -31,7 +31,13 @@ async def async_attach_trigger(hass, config, action, automation_info):
"""Execute when Home Assistant is shutting down.""" """Execute when Home Assistant is shutting down."""
hass.async_run_job( hass.async_run_job(
action, action,
{"trigger": {"platform": "homeassistant", "event": event}}, {
"trigger": {
"platform": "homeassistant",
"event": event,
"description": "Home Assistant stopping",
}
},
event.context, event.context,
) )
@ -41,7 +47,14 @@ async def async_attach_trigger(hass, config, action, automation_info):
# Check state because a config reload shouldn't trigger it. # Check state because a config reload shouldn't trigger it.
if automation_info["home_assistant_start"]: if automation_info["home_assistant_start"]:
hass.async_run_job( hass.async_run_job(
action({"trigger": {"platform": "homeassistant", "event": event}}) action,
{
"trigger": {
"platform": "homeassistant",
"event": event,
"description": "Home Assistant starting",
}
},
) )
return lambda: None return lambda: None

View file

@ -117,6 +117,7 @@ async def async_attach_trigger(
"from_state": from_s, "from_state": from_s,
"to_state": to_s, "to_state": to_s,
"for": time_delta if not time_delta else period[entity], "for": time_delta if not time_delta else period[entity],
"description": f"numeric state of {entity}",
} }
}, },
to_s.context, to_s.context,

View file

@ -103,6 +103,7 @@ async def async_attach_trigger(
"to_state": to_s, "to_state": to_s,
"for": time_delta if not time_delta else period[entity], "for": time_delta if not time_delta else period[entity],
"attribute": attribute, "attribute": attribute,
"description": f"state of {entity}",
} }
}, },
event.context, event.context,

View file

@ -1,5 +1,6 @@
"""Offer time listening automation rules.""" """Offer time listening automation rules."""
from datetime import datetime from datetime import datetime
from functools import partial
import logging import logging
import voluptuous as vol import voluptuous as vol
@ -38,9 +39,12 @@ async def async_attach_trigger(hass, config, action, automation_info):
removes = [] removes = []
@callback @callback
def time_automation_listener(now): def time_automation_listener(description, now):
"""Listen for time changes and calls action.""" """Listen for time changes and calls action."""
hass.async_run_job(action, {"trigger": {"platform": "time", "now": now}}) hass.async_run_job(
action,
{"trigger": {"platform": "time", "now": now, "description": description}},
)
@callback @callback
def update_entity_trigger_event(event): def update_entity_trigger_event(event):
@ -81,13 +85,15 @@ async def async_attach_trigger(hass, config, action, automation_info):
# Only set up listener if time is now or in the future. # Only set up listener if time is now or in the future.
if trigger_dt >= dt_util.now(): if trigger_dt >= dt_util.now():
remove = async_track_point_in_time( remove = async_track_point_in_time(
hass, time_automation_listener, trigger_dt hass,
partial(time_automation_listener, f"time set in {entity_id}"),
trigger_dt,
) )
elif has_time: elif has_time:
# Else if it has time, then track time change. # Else if it has time, then track time change.
remove = async_track_time_change( remove = async_track_time_change(
hass, hass,
time_automation_listener, partial(time_automation_listener, f"time set in {entity_id}"),
hour=hour, hour=hour,
minute=minute, minute=minute,
second=second, second=second,
@ -108,7 +114,7 @@ async def async_attach_trigger(hass, config, action, automation_info):
removes.append( removes.append(
async_track_time_change( async_track_time_change(
hass, hass,
time_automation_listener, partial(time_automation_listener, "time"),
hour=at_time.hour, hour=at_time.hour,
minute=at_time.minute, minute=at_time.minute,
second=at_time.second, second=at_time.second,

View file

@ -75,7 +75,14 @@ async def async_attach_trigger(hass, config, action, automation_info):
def time_automation_listener(now): def time_automation_listener(now):
"""Listen for time changes and calls action.""" """Listen for time changes and calls action."""
hass.async_run_job( hass.async_run_job(
action, {"trigger": {"platform": "time_pattern", "now": now}} action,
{
"trigger": {
"platform": "time_pattern",
"now": now,
"description": "time pattern",
}
},
) )
return async_track_time_change( return async_track_time_change(

View file

@ -66,7 +66,11 @@ def _attach_trigger(
@callback @callback
def _handle_event(event: Event): def _handle_event(event: Event):
if event.data[ATTR_ENTITY_ID] == config[CONF_ENTITY_ID]: if event.data[ATTR_ENTITY_ID] == config[CONF_ENTITY_ID]:
hass.async_run_job(action({"trigger": config}, context=event.context)) hass.async_run_job(
action,
{"trigger": {**config, "description": event_type}},
event.context,
)
return hass.bus.async_listen(event_type, _handle_event) return hass.bus.async_listen(event_type, _handle_event)

View file

@ -50,6 +50,7 @@ async def async_attach_trigger(hass, config, action, automation_info):
CONF_NUMBER: number, CONF_NUMBER: number,
CONF_HELD_MORE_THAN: held_more_than, CONF_HELD_MORE_THAN: held_more_than,
CONF_HELD_LESS_THAN: held_less_than, CONF_HELD_LESS_THAN: held_less_than,
"description": f"litejet switch #{number}",
} }
}, },
) )

View file

@ -45,6 +45,7 @@ async def async_attach_trigger(hass, config, action, automation_info):
"topic": mqttmsg.topic, "topic": mqttmsg.topic,
"payload": mqttmsg.payload, "payload": mqttmsg.payload,
"qos": mqttmsg.qos, "qos": mqttmsg.qos,
"description": f"mqtt topic {mqttmsg.topic}",
} }
try: try:

View file

@ -31,12 +31,23 @@ async def async_attach_trigger(hass, config, action, automation_info):
"""Listen for events based on configuration.""" """Listen for events based on configuration."""
event = config.get(CONF_EVENT) event = config.get(CONF_EVENT)
offset = config.get(CONF_OFFSET) offset = config.get(CONF_OFFSET)
description = event
if offset:
description = f"{description} with offset"
@callback @callback
def call_action(): def call_action():
"""Call action with right context.""" """Call action with right context."""
hass.async_run_job( hass.async_run_job(
action, {"trigger": {"platform": "sun", "event": event, "offset": offset}} action,
{
"trigger": {
"platform": "sun",
"event": event,
"offset": offset,
"description": description,
}
},
) )
if event == SUN_EVENT_SUNRISE: if event == SUN_EVENT_SUNRISE:

View file

@ -62,6 +62,7 @@ async def async_attach_trigger(
"from_state": from_s, "from_state": from_s,
"to_state": to_s, "to_state": to_s,
"for": time_delta if not time_delta else period, "for": time_delta if not time_delta else period,
"description": f"{entity_id} via template",
} }
}, },
(to_s.context if to_s else None), (to_s.context if to_s else None),

View file

@ -30,6 +30,7 @@ async def _handle_webhook(action, hass, webhook_id, request):
result["data"] = await request.post() result["data"] = await request.post()
result["query"] = request.query result["query"] = request.query
result["description"] = "webhook"
hass.async_run_job(action, {"trigger": result}) hass.async_run_job(action, {"trigger": result})

View file

@ -1,7 +1,13 @@
"""Offer zone automation rules.""" """Offer zone automation rules."""
import voluptuous as vol import voluptuous as vol
from homeassistant.const import CONF_ENTITY_ID, CONF_EVENT, CONF_PLATFORM, CONF_ZONE from homeassistant.const import (
ATTR_FRIENDLY_NAME,
CONF_ENTITY_ID,
CONF_EVENT,
CONF_PLATFORM,
CONF_ZONE,
)
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers import condition, config_validation as cv, location from homeassistant.helpers import condition, config_validation as cv, location
from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.event import async_track_state_change_event
@ -12,6 +18,8 @@ EVENT_ENTER = "enter"
EVENT_LEAVE = "leave" EVENT_LEAVE = "leave"
DEFAULT_EVENT = EVENT_ENTER DEFAULT_EVENT = EVENT_ENTER
_EVENT_DESCRIPTION = {EVENT_ENTER: "entering", EVENT_LEAVE: "leaving"}
TRIGGER_SCHEMA = vol.Schema( TRIGGER_SCHEMA = vol.Schema(
{ {
vol.Required(CONF_PLATFORM): "zone", vol.Required(CONF_PLATFORM): "zone",
@ -56,6 +64,7 @@ async def async_attach_trigger(hass, config, action, automation_info):
and from_match and from_match
and not to_match and not to_match
): ):
description = f"{entity} {_EVENT_DESCRIPTION[event]} {zone_state.attributes[ATTR_FRIENDLY_NAME]}"
hass.async_run_job( hass.async_run_job(
action, action,
{ {
@ -66,6 +75,7 @@ async def async_attach_trigger(hass, config, action, automation_info):
"to_state": to_s, "to_state": to_s,
"zone": zone_state, "zone": zone_state,
"event": event, "event": event,
"description": description,
} }
}, },
to_s.context, to_s.context,

View file

@ -6,6 +6,7 @@ import pytest
from homeassistant.components import logbook from homeassistant.components import logbook
import homeassistant.components.automation as automation import homeassistant.components.automation as automation
from homeassistant.components.automation import ( from homeassistant.components.automation import (
ATTR_SOURCE,
DOMAIN, DOMAIN,
EVENT_AUTOMATION_RELOADED, EVENT_AUTOMATION_RELOADED,
EVENT_AUTOMATION_TRIGGERED, EVENT_AUTOMATION_TRIGGERED,
@ -324,6 +325,7 @@ async def test_shared_context(hass, calls):
# Ensure event data has all attributes set # Ensure event data has all attributes set
assert args[0].data.get(ATTR_NAME) is not None assert args[0].data.get(ATTR_NAME) is not None
assert args[0].data.get(ATTR_ENTITY_ID) is not None assert args[0].data.get(ATTR_ENTITY_ID) is not None
assert args[0].data.get(ATTR_SOURCE) is not None
# Ensure context set correctly for event fired by 'hello' automation # Ensure context set correctly for event fired by 'hello' automation
args, _ = first_automation_listener.call_args args, _ = first_automation_listener.call_args
@ -341,6 +343,7 @@ async def test_shared_context(hass, calls):
# Ensure event data has all attributes set # Ensure event data has all attributes set
assert args[0].data.get(ATTR_NAME) is not None assert args[0].data.get(ATTR_NAME) is not None
assert args[0].data.get(ATTR_ENTITY_ID) is not None assert args[0].data.get(ATTR_ENTITY_ID) is not None
assert args[0].data.get(ATTR_SOURCE) is not None
# Ensure the service call from the second automation # Ensure the service call from the second automation
# shares the same context # shares the same context
@ -1089,7 +1092,11 @@ async def test_logbook_humanify_automation_triggered_event(hass):
), ),
MockLazyEventPartialState( MockLazyEventPartialState(
EVENT_AUTOMATION_TRIGGERED, EVENT_AUTOMATION_TRIGGERED,
{ATTR_ENTITY_ID: "automation.bye", ATTR_NAME: "Bye Automation"}, {
ATTR_ENTITY_ID: "automation.bye",
ATTR_NAME: "Bye Automation",
ATTR_SOURCE: "source of trigger",
},
), ),
], ],
entity_attr_cache, entity_attr_cache,
@ -1104,5 +1111,5 @@ async def test_logbook_humanify_automation_triggered_event(hass):
assert event2["name"] == "Bye Automation" assert event2["name"] == "Bye Automation"
assert event2["domain"] == "automation" assert event2["domain"] == "automation"
assert event2["message"] == "has been triggered" assert event2["message"] == "has been triggered by source of trigger"
assert event2["entity_id"] == "automation.bye" assert event2["entity_id"] == "automation.bye"