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:
parent
5217139e0b
commit
e6141ae558
18 changed files with 119 additions and 35 deletions
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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}",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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})
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue