1
mirror of https://github.com/home-assistant/core synced 2024-08-02 23:40:32 +02:00

Tweak automation tracing (#47721)

This commit is contained in:
Erik Montnemery 2021-03-10 23:42:13 +01:00 committed by GitHub
parent 15da1c4785
commit a9a9e1f199
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 1248 additions and 1212 deletions

View File

@ -1,21 +1,6 @@
"""Allow to set up simple automation rules via the config file."""
from collections import OrderedDict
from contextlib import contextmanager
import datetime as dt
from itertools import count
import logging
from typing import (
Any,
Awaitable,
Callable,
Deque,
Dict,
List,
Optional,
Set,
Union,
cast,
)
from typing import Any, Awaitable, Callable, Dict, List, Optional, Set, Union, cast
import voluptuous as vol
from voluptuous.humanize import humanize_error
@ -68,18 +53,13 @@ from homeassistant.helpers.script import (
)
from homeassistant.helpers.script_variables import ScriptVariables
from homeassistant.helpers.service import async_register_admin_service
from homeassistant.helpers.trace import (
TraceElement,
trace_get,
trace_id_set,
trace_path,
)
from homeassistant.helpers.trace import trace_get, trace_path
from homeassistant.helpers.trigger import async_initialize_triggers
from homeassistant.helpers.typing import TemplateVarsType
from homeassistant.loader import bind_hass
from homeassistant.util import dt as dt_util
from homeassistant.util.dt import parse_datetime
from . import websocket_api
from .config import AutomationConfig, async_validate_config_item
# Not used except by packages to check config structure
@ -94,6 +74,7 @@ from .const import (
LOGGER,
)
from .helpers import async_get_blueprints
from .trace import DATA_AUTOMATION_TRACE, trace_automation
# mypy: allow-untyped-calls, allow-untyped-defs
# mypy: no-check-untyped-defs, no-warn-return-any
@ -113,9 +94,6 @@ ATTR_SOURCE = "source"
ATTR_VARIABLES = "variables"
SERVICE_TRIGGER = "trigger"
DATA_AUTOMATION_TRACE = "automation_trace"
STORED_TRACES = 5 # Stored traces per automation
_LOGGER = logging.getLogger(__name__)
AutomationActionType = Callable[[HomeAssistant, TemplateVarsType], Awaitable[None]]
@ -194,9 +172,12 @@ def devices_in_automation(hass: HomeAssistant, entity_id: str) -> List[str]:
async def async_setup(hass, config):
"""Set up all automations."""
# Local import to avoid circular import
hass.data[DOMAIN] = component = EntityComponent(LOGGER, DOMAIN, hass)
hass.data.setdefault(DATA_AUTOMATION_TRACE, {})
websocket_api.async_setup(hass)
# To register the automation blueprints
async_get_blueprints(hass)
@ -243,167 +224,6 @@ async def async_setup(hass, config):
return True
class AutomationTrace:
"""Container for automation trace."""
_run_ids = count(0)
def __init__(
self,
unique_id: Optional[str],
config: Dict[str, Any],
trigger: Dict[str, Any],
context: Context,
):
"""Container for automation trace."""
self._action_trace: Optional[Dict[str, Deque[TraceElement]]] = None
self._condition_trace: Optional[Dict[str, Deque[TraceElement]]] = None
self._config: Dict[str, Any] = config
self._context: Context = context
self._error: Optional[Exception] = None
self._state: str = "running"
self.run_id: str = str(next(self._run_ids))
self._timestamp_finish: Optional[dt.datetime] = None
self._timestamp_start: dt.datetime = dt_util.utcnow()
self._trigger: Dict[str, Any] = trigger
self._unique_id: Optional[str] = unique_id
self._variables: Optional[Dict[str, Any]] = None
def set_action_trace(self, trace: Dict[str, Deque[TraceElement]]) -> None:
"""Set action trace."""
self._action_trace = trace
def set_condition_trace(self, trace: Dict[str, Deque[TraceElement]]) -> None:
"""Set condition trace."""
self._condition_trace = trace
def set_error(self, ex: Exception) -> None:
"""Set error."""
self._error = ex
def set_variables(self, variables: Dict[str, Any]) -> None:
"""Set variables."""
self._variables = variables
def finished(self) -> None:
"""Set finish time."""
self._timestamp_finish = dt_util.utcnow()
self._state = "stopped"
def as_dict(self) -> Dict[str, Any]:
"""Return dictionary version of this AutomationTrace."""
action_traces = {}
condition_traces = {}
if self._action_trace:
for key, trace_list in self._action_trace.items():
action_traces[key] = [item.as_dict() for item in trace_list]
if self._condition_trace:
for key, trace_list in self._condition_trace.items():
condition_traces[key] = [item.as_dict() for item in trace_list]
result = {
"action_trace": action_traces,
"condition_trace": condition_traces,
"config": self._config,
"context": self._context,
"run_id": self.run_id,
"state": self._state,
"timestamp": {
"start": self._timestamp_start,
"finish": self._timestamp_finish,
},
"trigger": self._trigger,
"unique_id": self._unique_id,
"variables": self._variables,
}
if self._error is not None:
result["error"] = str(self._error)
return result
def as_short_dict(self) -> Dict[str, Any]:
"""Return a brief dictionary version of this AutomationTrace."""
last_action = None
last_condition = None
if self._action_trace:
last_action = list(self._action_trace.keys())[-1]
if self._condition_trace:
last_condition = list(self._condition_trace.keys())[-1]
result = {
"last_action": last_action,
"last_condition": last_condition,
"run_id": self.run_id,
"state": self._state,
"timestamp": {
"start": self._timestamp_start,
"finish": self._timestamp_finish,
},
"trigger": self._trigger.get("description"),
"unique_id": self._unique_id,
}
if self._error is not None:
result["error"] = str(self._error)
if last_action is not None:
result["last_action"] = last_action
result["last_condition"] = last_condition
return result
class LimitedSizeDict(OrderedDict):
"""OrderedDict limited in size."""
def __init__(self, *args, **kwds):
"""Initialize OrderedDict limited in size."""
self.size_limit = kwds.pop("size_limit", None)
OrderedDict.__init__(self, *args, **kwds)
self._check_size_limit()
def __setitem__(self, key, value):
"""Set item and check dict size."""
OrderedDict.__setitem__(self, key, value)
self._check_size_limit()
def _check_size_limit(self):
"""Check dict size and evict items in FIFO order if needed."""
if self.size_limit is not None:
while len(self) > self.size_limit:
self.popitem(last=False)
@contextmanager
def trace_automation(hass, unique_id, config, trigger, context):
"""Trace action execution of automation with automation_id."""
automation_trace = AutomationTrace(unique_id, config, trigger, context)
trace_id_set((unique_id, automation_trace.run_id))
if unique_id:
automation_traces = hass.data[DATA_AUTOMATION_TRACE]
if unique_id not in automation_traces:
automation_traces[unique_id] = LimitedSizeDict(size_limit=STORED_TRACES)
automation_traces[unique_id][automation_trace.run_id] = automation_trace
try:
yield automation_trace
except Exception as ex: # pylint: disable=broad-except
if unique_id:
automation_trace.set_error(ex)
raise ex
finally:
if unique_id:
automation_trace.finished()
_LOGGER.debug(
"Automation finished. Summary:\n\ttrigger: %s\n\tcondition: %s\n\taction: %s",
automation_trace._trigger, # pylint: disable=protected-access
automation_trace._condition_trace, # pylint: disable=protected-access
automation_trace._action_trace, # pylint: disable=protected-access
)
class AutomationEntity(ToggleEntity, RestoreEntity):
"""Entity to show status of entity."""
@ -570,9 +390,8 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
reason = f' by {run_variables["trigger"]["description"]}'
self._logger.debug("Automation triggered%s", reason)
trigger = run_variables["trigger"] if "trigger" in run_variables else None
with trace_automation(
self.hass, self.unique_id, self._raw_config, trigger, context
self.hass, self.unique_id, self._raw_config, context
) as automation_trace:
if self._variables:
try:
@ -891,30 +710,3 @@ def _trigger_extract_entities(trigger_conf: dict) -> List[str]:
return ["sun.sun"]
return []
@callback
def get_debug_traces_for_automation(hass, automation_id, summary=False):
"""Return a serializable list of debug traces for an automation."""
traces = []
for trace in hass.data[DATA_AUTOMATION_TRACE].get(automation_id, {}).values():
if summary:
traces.append(trace.as_short_dict())
else:
traces.append(trace.as_dict())
return traces
@callback
def get_debug_traces(hass, summary=False):
"""Return a serializable list of debug traces."""
traces = {}
for automation_id in hass.data[DATA_AUTOMATION_TRACE]:
traces[automation_id] = get_debug_traces_for_automation(
hass, automation_id, summary
)
return traces

View File

@ -0,0 +1,206 @@
"""Trace support for automation."""
from collections import OrderedDict
from contextlib import contextmanager
import datetime as dt
from itertools import count
import logging
from typing import Any, Awaitable, Callable, Deque, Dict, Optional
from homeassistant.core import Context, HomeAssistant, callback
from homeassistant.helpers.trace import TraceElement, trace_id_set
from homeassistant.helpers.typing import TemplateVarsType
from homeassistant.util import dt as dt_util
DATA_AUTOMATION_TRACE = "automation_trace"
STORED_TRACES = 5 # Stored traces per automation
_LOGGER = logging.getLogger(__name__)
AutomationActionType = Callable[[HomeAssistant, TemplateVarsType], Awaitable[None]]
# mypy: allow-untyped-calls, allow-untyped-defs
# mypy: no-check-untyped-defs, no-warn-return-any
class AutomationTrace:
"""Container for automation trace."""
_run_ids = count(0)
def __init__(
self,
unique_id: Optional[str],
config: Dict[str, Any],
context: Context,
):
"""Container for automation trace."""
self._action_trace: Optional[Dict[str, Deque[TraceElement]]] = None
self._condition_trace: Optional[Dict[str, Deque[TraceElement]]] = None
self._config: Dict[str, Any] = config
self._context: Context = context
self._error: Optional[Exception] = None
self._state: str = "running"
self.run_id: str = str(next(self._run_ids))
self._timestamp_finish: Optional[dt.datetime] = None
self._timestamp_start: dt.datetime = dt_util.utcnow()
self._unique_id: Optional[str] = unique_id
self._variables: Optional[Dict[str, Any]] = None
def set_action_trace(self, trace: Dict[str, Deque[TraceElement]]) -> None:
"""Set action trace."""
self._action_trace = trace
def set_condition_trace(self, trace: Dict[str, Deque[TraceElement]]) -> None:
"""Set condition trace."""
self._condition_trace = trace
def set_error(self, ex: Exception) -> None:
"""Set error."""
self._error = ex
def set_variables(self, variables: Dict[str, Any]) -> None:
"""Set variables."""
self._variables = variables
def finished(self) -> None:
"""Set finish time."""
self._timestamp_finish = dt_util.utcnow()
self._state = "stopped"
def as_dict(self) -> Dict[str, Any]:
"""Return dictionary version of this AutomationTrace."""
result = self.as_short_dict()
action_traces = {}
condition_traces = {}
if self._action_trace:
for key, trace_list in self._action_trace.items():
action_traces[key] = [item.as_dict() for item in trace_list]
if self._condition_trace:
for key, trace_list in self._condition_trace.items():
condition_traces[key] = [item.as_dict() for item in trace_list]
result.update(
{
"action_trace": action_traces,
"condition_trace": condition_traces,
"config": self._config,
"context": self._context,
"variables": self._variables,
}
)
if self._error is not None:
result["error"] = str(self._error)
return result
def as_short_dict(self) -> Dict[str, Any]:
"""Return a brief dictionary version of this AutomationTrace."""
last_action = None
last_condition = None
trigger = None
if self._action_trace:
last_action = list(self._action_trace)[-1]
if self._condition_trace:
last_condition = list(self._condition_trace)[-1]
if self._variables:
trigger = self._variables.get("trigger", {}).get("description")
result = {
"last_action": last_action,
"last_condition": last_condition,
"run_id": self.run_id,
"state": self._state,
"timestamp": {
"start": self._timestamp_start,
"finish": self._timestamp_finish,
},
"trigger": trigger,
"unique_id": self._unique_id,
}
if self._error is not None:
result["error"] = str(self._error)
if last_action is not None:
result["last_action"] = last_action
result["last_condition"] = last_condition
return result
class LimitedSizeDict(OrderedDict):
"""OrderedDict limited in size."""
def __init__(self, *args, **kwds):
"""Initialize OrderedDict limited in size."""
self.size_limit = kwds.pop("size_limit", None)
OrderedDict.__init__(self, *args, **kwds)
self._check_size_limit()
def __setitem__(self, key, value):
"""Set item and check dict size."""
OrderedDict.__setitem__(self, key, value)
self._check_size_limit()
def _check_size_limit(self):
"""Check dict size and evict items in FIFO order if needed."""
if self.size_limit is not None:
while len(self) > self.size_limit:
self.popitem(last=False)
@contextmanager
def trace_automation(hass, unique_id, config, context):
"""Trace action execution of automation with automation_id."""
automation_trace = AutomationTrace(unique_id, config, context)
trace_id_set((unique_id, automation_trace.run_id))
if unique_id:
automation_traces = hass.data[DATA_AUTOMATION_TRACE]
if unique_id not in automation_traces:
automation_traces[unique_id] = LimitedSizeDict(size_limit=STORED_TRACES)
automation_traces[unique_id][automation_trace.run_id] = automation_trace
try:
yield automation_trace
except Exception as ex: # pylint: disable=broad-except
if unique_id:
automation_trace.set_error(ex)
raise ex
finally:
if unique_id:
automation_trace.finished()
@callback
def get_debug_trace(hass, automation_id, run_id):
"""Return a serializable debug trace."""
return hass.data[DATA_AUTOMATION_TRACE][automation_id][run_id]
@callback
def get_debug_traces_for_automation(hass, automation_id, summary=False):
"""Return a serializable list of debug traces for an automation."""
traces = []
for trace in hass.data[DATA_AUTOMATION_TRACE].get(automation_id, {}).values():
if summary:
traces.append(trace.as_short_dict())
else:
traces.append(trace.as_dict())
return traces
@callback
def get_debug_traces(hass, summary=False):
"""Return a serializable list of debug traces."""
traces = {}
for automation_id in hass.data[DATA_AUTOMATION_TRACE]:
traces[automation_id] = get_debug_traces_for_automation(
hass, automation_id, summary
)
return traces

View File

@ -0,0 +1,229 @@
"""Websocket API for automation."""
import voluptuous as vol
from homeassistant.components import websocket_api
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.dispatcher import (
DATA_DISPATCHER,
async_dispatcher_connect,
async_dispatcher_send,
)
from homeassistant.helpers.script import (
SCRIPT_BREAKPOINT_HIT,
SCRIPT_DEBUG_CONTINUE_ALL,
breakpoint_clear,
breakpoint_clear_all,
breakpoint_list,
breakpoint_set,
debug_continue,
debug_step,
debug_stop,
)
from .trace import get_debug_trace, get_debug_traces
# mypy: allow-untyped-calls, allow-untyped-defs
@callback
def async_setup(hass: HomeAssistant) -> None:
"""Set up the websocket API."""
websocket_api.async_register_command(hass, websocket_automation_trace_get)
websocket_api.async_register_command(hass, websocket_automation_trace_list)
websocket_api.async_register_command(hass, websocket_automation_breakpoint_clear)
websocket_api.async_register_command(hass, websocket_automation_breakpoint_list)
websocket_api.async_register_command(hass, websocket_automation_breakpoint_set)
websocket_api.async_register_command(hass, websocket_automation_debug_continue)
websocket_api.async_register_command(hass, websocket_automation_debug_step)
websocket_api.async_register_command(hass, websocket_automation_debug_stop)
websocket_api.async_register_command(hass, websocket_subscribe_breakpoint_events)
@callback
@websocket_api.require_admin
@websocket_api.websocket_command(
{
vol.Required("type"): "automation/trace/get",
vol.Required("automation_id"): str,
vol.Required("run_id"): str,
}
)
def websocket_automation_trace_get(hass, connection, msg):
"""Get automation traces."""
automation_id = msg["automation_id"]
run_id = msg["run_id"]
trace = get_debug_trace(hass, automation_id, run_id)
connection.send_result(msg["id"], trace)
@callback
@websocket_api.require_admin
@websocket_api.websocket_command({vol.Required("type"): "automation/trace/list"})
def websocket_automation_trace_list(hass, connection, msg):
"""Summarize automation traces."""
automation_traces = get_debug_traces(hass, summary=True)
connection.send_result(msg["id"], automation_traces)
@callback
@websocket_api.require_admin
@websocket_api.websocket_command(
{
vol.Required("type"): "automation/debug/breakpoint/set",
vol.Required("automation_id"): str,
vol.Required("node"): str,
vol.Optional("run_id"): str,
}
)
def websocket_automation_breakpoint_set(hass, connection, msg):
"""Set breakpoint."""
automation_id = msg["automation_id"]
node = msg["node"]
run_id = msg.get("run_id")
if (
SCRIPT_BREAKPOINT_HIT not in hass.data.get(DATA_DISPATCHER, {})
or not hass.data[DATA_DISPATCHER][SCRIPT_BREAKPOINT_HIT]
):
raise HomeAssistantError("No breakpoint subscription")
result = breakpoint_set(hass, automation_id, run_id, node)
connection.send_result(msg["id"], result)
@callback
@websocket_api.require_admin
@websocket_api.websocket_command(
{
vol.Required("type"): "automation/debug/breakpoint/clear",
vol.Required("automation_id"): str,
vol.Required("node"): str,
vol.Optional("run_id"): str,
}
)
def websocket_automation_breakpoint_clear(hass, connection, msg):
"""Clear breakpoint."""
automation_id = msg["automation_id"]
node = msg["node"]
run_id = msg.get("run_id")
result = breakpoint_clear(hass, automation_id, run_id, node)
connection.send_result(msg["id"], result)
@callback
@websocket_api.require_admin
@websocket_api.websocket_command(
{vol.Required("type"): "automation/debug/breakpoint/list"}
)
def websocket_automation_breakpoint_list(hass, connection, msg):
"""List breakpoints."""
breakpoints = breakpoint_list(hass)
for _breakpoint in breakpoints:
_breakpoint["automation_id"] = _breakpoint.pop("unique_id")
connection.send_result(msg["id"], breakpoints)
@callback
@websocket_api.require_admin
@websocket_api.websocket_command(
{vol.Required("type"): "automation/debug/breakpoint/subscribe"}
)
def websocket_subscribe_breakpoint_events(hass, connection, msg):
"""Subscribe to breakpoint events."""
@callback
def breakpoint_hit(automation_id, run_id, node):
"""Forward events to websocket."""
connection.send_message(
websocket_api.event_message(
msg["id"],
{
"automation_id": automation_id,
"run_id": run_id,
"node": node,
},
)
)
remove_signal = async_dispatcher_connect(
hass, SCRIPT_BREAKPOINT_HIT, breakpoint_hit
)
@callback
def unsub():
"""Unsubscribe from breakpoint events."""
remove_signal()
if (
SCRIPT_BREAKPOINT_HIT not in hass.data.get(DATA_DISPATCHER, {})
or not hass.data[DATA_DISPATCHER][SCRIPT_BREAKPOINT_HIT]
):
breakpoint_clear_all(hass)
async_dispatcher_send(hass, SCRIPT_DEBUG_CONTINUE_ALL)
connection.subscriptions[msg["id"]] = unsub
connection.send_message(websocket_api.result_message(msg["id"]))
@callback
@websocket_api.require_admin
@websocket_api.websocket_command(
{
vol.Required("type"): "automation/debug/continue",
vol.Required("automation_id"): str,
vol.Required("run_id"): str,
}
)
def websocket_automation_debug_continue(hass, connection, msg):
"""Resume execution of halted automation."""
automation_id = msg["automation_id"]
run_id = msg["run_id"]
result = debug_continue(hass, automation_id, run_id)
connection.send_result(msg["id"], result)
@callback
@websocket_api.require_admin
@websocket_api.websocket_command(
{
vol.Required("type"): "automation/debug/step",
vol.Required("automation_id"): str,
vol.Required("run_id"): str,
}
)
def websocket_automation_debug_step(hass, connection, msg):
"""Single step a halted automation."""
automation_id = msg["automation_id"]
run_id = msg["run_id"]
result = debug_step(hass, automation_id, run_id)
connection.send_result(msg["id"], result)
@callback
@websocket_api.require_admin
@websocket_api.websocket_command(
{
vol.Required("type"): "automation/debug/stop",
vol.Required("automation_id"): str,
vol.Required("run_id"): str,
}
)
def websocket_automation_debug_stop(hass, connection, msg):
"""Stop a halted automation."""
automation_id = msg["automation_id"]
run_id = msg["run_id"]
result = debug_stop(hass, automation_id, run_id)
connection.send_result(msg["id"], result)

View File

@ -2,13 +2,6 @@
from collections import OrderedDict
import uuid
import voluptuous as vol
from homeassistant.components import websocket_api
from homeassistant.components.automation import (
get_debug_traces,
get_debug_traces_for_automation,
)
from homeassistant.components.automation.config import (
DOMAIN,
PLATFORM_SCHEMA,
@ -16,25 +9,7 @@ from homeassistant.components.automation.config import (
)
from homeassistant.config import AUTOMATION_CONFIG_PATH
from homeassistant.const import CONF_ID, SERVICE_RELOAD
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, entity_registry
from homeassistant.helpers.dispatcher import (
DATA_DISPATCHER,
async_dispatcher_connect,
async_dispatcher_send,
)
from homeassistant.helpers.script import (
SCRIPT_BREAKPOINT_HIT,
SCRIPT_DEBUG_CONTINUE_ALL,
breakpoint_clear,
breakpoint_clear_all,
breakpoint_list,
breakpoint_set,
debug_continue,
debug_step,
debug_stop,
)
from . import ACTION_DELETE, EditIdBasedConfigView
@ -42,16 +17,6 @@ from . import ACTION_DELETE, EditIdBasedConfigView
async def async_setup(hass):
"""Set up the Automation config API."""
websocket_api.async_register_command(hass, websocket_automation_trace_get)
websocket_api.async_register_command(hass, websocket_automation_trace_list)
websocket_api.async_register_command(hass, websocket_automation_breakpoint_clear)
websocket_api.async_register_command(hass, websocket_automation_breakpoint_list)
websocket_api.async_register_command(hass, websocket_automation_breakpoint_set)
websocket_api.async_register_command(hass, websocket_automation_debug_continue)
websocket_api.async_register_command(hass, websocket_automation_debug_step)
websocket_api.async_register_command(hass, websocket_automation_debug_stop)
websocket_api.async_register_command(hass, websocket_subscribe_breakpoint_events)
async def hook(action, config_key):
"""post_write_hook for Config View that reloads automations."""
await hass.services.async_call(DOMAIN, SERVICE_RELOAD)
@ -115,192 +80,3 @@ class EditAutomationConfigView(EditIdBasedConfigView):
updated_value.update(cur_value)
updated_value.update(new_value)
data[index] = updated_value
@callback
@websocket_api.require_admin
@websocket_api.websocket_command(
{vol.Required("type"): "automation/trace/get", vol.Optional("automation_id"): str}
)
def websocket_automation_trace_get(hass, connection, msg):
"""Get automation traces."""
automation_id = msg.get("automation_id")
if not automation_id:
automation_traces = get_debug_traces(hass)
else:
automation_traces = {
automation_id: get_debug_traces_for_automation(hass, automation_id)
}
connection.send_result(msg["id"], automation_traces)
@callback
@websocket_api.require_admin
@websocket_api.websocket_command({vol.Required("type"): "automation/trace/list"})
def websocket_automation_trace_list(hass, connection, msg):
"""Summarize automation traces."""
automation_traces = get_debug_traces(hass, summary=True)
connection.send_result(msg["id"], automation_traces)
@callback
@websocket_api.require_admin
@websocket_api.websocket_command(
{
vol.Required("type"): "automation/debug/breakpoint/set",
vol.Required("automation_id"): str,
vol.Required("node"): str,
vol.Optional("run_id"): str,
}
)
def websocket_automation_breakpoint_set(hass, connection, msg):
"""Set breakpoint."""
automation_id = msg["automation_id"]
node = msg["node"]
run_id = msg.get("run_id")
if (
SCRIPT_BREAKPOINT_HIT not in hass.data.get(DATA_DISPATCHER, {})
or not hass.data[DATA_DISPATCHER][SCRIPT_BREAKPOINT_HIT]
):
raise HomeAssistantError("No breakpoint subscription")
result = breakpoint_set(hass, automation_id, run_id, node)
connection.send_result(msg["id"], result)
@callback
@websocket_api.require_admin
@websocket_api.websocket_command(
{
vol.Required("type"): "automation/debug/breakpoint/clear",
vol.Required("automation_id"): str,
vol.Required("node"): str,
vol.Optional("run_id"): str,
}
)
def websocket_automation_breakpoint_clear(hass, connection, msg):
"""Clear breakpoint."""
automation_id = msg["automation_id"]
node = msg["node"]
run_id = msg.get("run_id")
result = breakpoint_clear(hass, automation_id, run_id, node)
connection.send_result(msg["id"], result)
@callback
@websocket_api.require_admin
@websocket_api.websocket_command(
{vol.Required("type"): "automation/debug/breakpoint/list"}
)
def websocket_automation_breakpoint_list(hass, connection, msg):
"""List breakpoints."""
breakpoints = breakpoint_list(hass)
for _breakpoint in breakpoints:
_breakpoint["automation_id"] = _breakpoint.pop("unique_id")
connection.send_result(msg["id"], breakpoints)
@callback
@websocket_api.require_admin
@websocket_api.websocket_command(
{vol.Required("type"): "automation/debug/breakpoint/subscribe"}
)
def websocket_subscribe_breakpoint_events(hass, connection, msg):
"""Subscribe to breakpoint events."""
@callback
def breakpoint_hit(automation_id, run_id, node):
"""Forward events to websocket."""
connection.send_message(
websocket_api.event_message(
msg["id"],
{
"automation_id": automation_id,
"run_id": run_id,
"node": node,
},
)
)
remove_signal = async_dispatcher_connect(
hass, SCRIPT_BREAKPOINT_HIT, breakpoint_hit
)
@callback
def unsub():
"""Unsubscribe from breakpoint events."""
remove_signal()
if (
SCRIPT_BREAKPOINT_HIT not in hass.data.get(DATA_DISPATCHER, {})
or not hass.data[DATA_DISPATCHER][SCRIPT_BREAKPOINT_HIT]
):
breakpoint_clear_all(hass)
async_dispatcher_send(hass, SCRIPT_DEBUG_CONTINUE_ALL)
connection.subscriptions[msg["id"]] = unsub
connection.send_message(websocket_api.result_message(msg["id"]))
@callback
@websocket_api.require_admin
@websocket_api.websocket_command(
{
vol.Required("type"): "automation/debug/continue",
vol.Required("automation_id"): str,
vol.Required("run_id"): str,
}
)
def websocket_automation_debug_continue(hass, connection, msg):
"""Resume execution of halted automation."""
automation_id = msg["automation_id"]
run_id = msg["run_id"]
result = debug_continue(hass, automation_id, run_id)
connection.send_result(msg["id"], result)
@callback
@websocket_api.require_admin
@websocket_api.websocket_command(
{
vol.Required("type"): "automation/debug/step",
vol.Required("automation_id"): str,
vol.Required("run_id"): str,
}
)
def websocket_automation_debug_step(hass, connection, msg):
"""Single step a halted automation."""
automation_id = msg["automation_id"]
run_id = msg["run_id"]
result = debug_step(hass, automation_id, run_id)
connection.send_result(msg["id"], result)
@callback
@websocket_api.require_admin
@websocket_api.websocket_command(
{
vol.Required("type"): "automation/debug/stop",
vol.Required("automation_id"): str,
vol.Required("run_id"): str,
}
)
def websocket_automation_debug_stop(hass, connection, msg):
"""Stop a halted automation."""
automation_id = msg["automation_id"]
run_id = msg["run_id"]
result = debug_stop(hass, automation_id, run_id)
connection.send_result(msg["id"], result)

View File

@ -360,7 +360,7 @@ class _ScriptRun:
handler = f"_async_{cv.determine_script_action(self._action)}_step"
await getattr(self, handler)()
except Exception as ex:
if not isinstance(ex, (_StopScript, asyncio.CancelledError)) and (
if not isinstance(ex, _StopScript) and (
self._log_exceptions or log_exceptions
):
self._log_exception(ex)

View File

@ -0,0 +1,803 @@
"""Test Automation config panel."""
from unittest.mock import patch
from homeassistant.bootstrap import async_setup_component
from homeassistant.components import automation, config
from tests.common import assert_lists_same
from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
async def test_get_automation_trace(hass, hass_ws_client):
"""Test tracing an automation."""
id = 1
def next_id():
nonlocal id
id += 1
return id
sun_config = {
"id": "sun",
"trigger": {"platform": "event", "event_type": "test_event"},
"action": {"service": "test.automation"},
}
moon_config = {
"id": "moon",
"trigger": [
{"platform": "event", "event_type": "test_event2"},
{"platform": "event", "event_type": "test_event3"},
],
"condition": {
"condition": "template",
"value_template": "{{ trigger.event.event_type=='test_event2' }}",
},
"action": {"event": "another_event"},
}
assert await async_setup_component(
hass,
"automation",
{
"automation": [
sun_config,
moon_config,
]
},
)
with patch.object(config, "SECTIONS", ["automation"]):
await async_setup_component(hass, "config", {})
client = await hass_ws_client()
# Trigger "sun" automation
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
# List traces
await client.send_json({"id": next_id(), "type": "automation/trace/list"})
response = await client.receive_json()
assert response["success"]
run_id = response["result"]["sun"][-1]["run_id"]
# Get trace
await client.send_json(
{
"id": next_id(),
"type": "automation/trace/get",
"automation_id": "sun",
"run_id": run_id,
}
)
response = await client.receive_json()
assert response["success"]
trace = response["result"]
assert len(trace["action_trace"]) == 1
assert len(trace["action_trace"]["action/0"]) == 1
assert trace["action_trace"]["action/0"][0]["error"]
assert "result" not in trace["action_trace"]["action/0"][0]
assert trace["condition_trace"] == {}
assert trace["config"] == sun_config
assert trace["context"]
assert trace["error"] == "Unable to find service test.automation"
assert trace["state"] == "stopped"
assert trace["trigger"] == "event 'test_event'"
assert trace["unique_id"] == "sun"
assert trace["variables"]
# Trigger "moon" automation, with passing condition
hass.bus.async_fire("test_event2")
await hass.async_block_till_done()
# List traces
await client.send_json({"id": next_id(), "type": "automation/trace/list"})
response = await client.receive_json()
assert response["success"]
run_id = response["result"]["moon"][-1]["run_id"]
# Get trace
await client.send_json(
{
"id": next_id(),
"type": "automation/trace/get",
"automation_id": "moon",
"run_id": run_id,
}
)
response = await client.receive_json()
assert response["success"]
trace = response["result"]
assert len(trace["action_trace"]) == 1
assert len(trace["action_trace"]["action/0"]) == 1
assert "error" not in trace["action_trace"]["action/0"][0]
assert "result" not in trace["action_trace"]["action/0"][0]
assert len(trace["condition_trace"]) == 1
assert len(trace["condition_trace"]["condition/0"]) == 1
assert trace["condition_trace"]["condition/0"][0]["result"] == {"result": True}
assert trace["config"] == moon_config
assert trace["context"]
assert "error" not in trace
assert trace["state"] == "stopped"
assert trace["trigger"] == "event 'test_event2'"
assert trace["unique_id"] == "moon"
assert trace["variables"]
# Trigger "moon" automation, with failing condition
hass.bus.async_fire("test_event3")
await hass.async_block_till_done()
# List traces
await client.send_json({"id": next_id(), "type": "automation/trace/list"})
response = await client.receive_json()
assert response["success"]
run_id = response["result"]["moon"][-1]["run_id"]
# Get trace
await client.send_json(
{
"id": next_id(),
"type": "automation/trace/get",
"automation_id": "moon",
"run_id": run_id,
}
)
response = await client.receive_json()
assert response["success"]
trace = response["result"]
assert len(trace["action_trace"]) == 0
assert len(trace["condition_trace"]) == 1
assert len(trace["condition_trace"]["condition/0"]) == 1
assert trace["condition_trace"]["condition/0"][0]["result"] == {"result": False}
assert trace["config"] == moon_config
assert trace["context"]
assert "error" not in trace
assert trace["state"] == "stopped"
assert trace["trigger"] == "event 'test_event3'"
assert trace["unique_id"] == "moon"
assert trace["variables"]
# Trigger "moon" automation, with passing condition
hass.bus.async_fire("test_event2")
await hass.async_block_till_done()
# List traces
await client.send_json({"id": next_id(), "type": "automation/trace/list"})
response = await client.receive_json()
assert response["success"]
run_id = response["result"]["moon"][-1]["run_id"]
# Get trace
await client.send_json(
{
"id": next_id(),
"type": "automation/trace/get",
"automation_id": "moon",
"run_id": run_id,
}
)
response = await client.receive_json()
assert response["success"]
trace = response["result"]
assert len(trace["action_trace"]) == 1
assert len(trace["action_trace"]["action/0"]) == 1
assert "error" not in trace["action_trace"]["action/0"][0]
assert "result" not in trace["action_trace"]["action/0"][0]
assert len(trace["condition_trace"]) == 1
assert len(trace["condition_trace"]["condition/0"]) == 1
assert trace["condition_trace"]["condition/0"][0]["result"] == {"result": True}
assert trace["config"] == moon_config
assert trace["context"]
assert "error" not in trace
assert trace["state"] == "stopped"
assert trace["trigger"] == "event 'test_event2'"
assert trace["unique_id"] == "moon"
assert trace["variables"]
async def test_automation_trace_overflow(hass, hass_ws_client):
"""Test the number of stored traces per automation is limited."""
id = 1
def next_id():
nonlocal id
id += 1
return id
sun_config = {
"id": "sun",
"trigger": {"platform": "event", "event_type": "test_event"},
"action": {"event": "some_event"},
}
moon_config = {
"id": "moon",
"trigger": {"platform": "event", "event_type": "test_event2"},
"action": {"event": "another_event"},
}
assert await async_setup_component(
hass,
"automation",
{
"automation": [
sun_config,
moon_config,
]
},
)
with patch.object(config, "SECTIONS", ["automation"]):
await async_setup_component(hass, "config", {})
client = await hass_ws_client()
await client.send_json({"id": next_id(), "type": "automation/trace/list"})
response = await client.receive_json()
assert response["success"]
assert response["result"] == {}
# Trigger "sun" and "moon" automation once
hass.bus.async_fire("test_event")
hass.bus.async_fire("test_event2")
await hass.async_block_till_done()
# List traces
await client.send_json({"id": next_id(), "type": "automation/trace/list"})
response = await client.receive_json()
assert response["success"]
assert len(response["result"]["moon"]) == 1
moon_run_id = response["result"]["moon"][0]["run_id"]
assert len(response["result"]["sun"]) == 1
# Trigger "moon" automation enough times to overflow the number of stored traces
for _ in range(automation.trace.STORED_TRACES):
hass.bus.async_fire("test_event2")
await hass.async_block_till_done()
await client.send_json({"id": next_id(), "type": "automation/trace/list"})
response = await client.receive_json()
assert response["success"]
assert len(response["result"]["moon"]) == automation.trace.STORED_TRACES
assert len(response["result"]["sun"]) == 1
assert int(response["result"]["moon"][0]["run_id"]) == int(moon_run_id) + 1
assert (
int(response["result"]["moon"][-1]["run_id"])
== int(moon_run_id) + automation.trace.STORED_TRACES
)
async def test_list_automation_traces(hass, hass_ws_client):
"""Test listing automation traces."""
id = 1
def next_id():
nonlocal id
id += 1
return id
sun_config = {
"id": "sun",
"trigger": {"platform": "event", "event_type": "test_event"},
"action": {"service": "test.automation"},
}
moon_config = {
"id": "moon",
"trigger": [
{"platform": "event", "event_type": "test_event2"},
{"platform": "event", "event_type": "test_event3"},
],
"condition": {
"condition": "template",
"value_template": "{{ trigger.event.event_type=='test_event2' }}",
},
"action": {"event": "another_event"},
}
assert await async_setup_component(
hass,
"automation",
{
"automation": [
sun_config,
moon_config,
]
},
)
with patch.object(config, "SECTIONS", ["automation"]):
await async_setup_component(hass, "config", {})
client = await hass_ws_client()
await client.send_json({"id": next_id(), "type": "automation/trace/list"})
response = await client.receive_json()
assert response["success"]
assert response["result"] == {}
# Trigger "sun" automation
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
# Get trace
await client.send_json({"id": next_id(), "type": "automation/trace/list"})
response = await client.receive_json()
assert response["success"]
assert "moon" not in response["result"]
assert len(response["result"]["sun"]) == 1
# Trigger "moon" automation, with passing condition
hass.bus.async_fire("test_event2")
await hass.async_block_till_done()
# Trigger "moon" automation, with failing condition
hass.bus.async_fire("test_event3")
await hass.async_block_till_done()
# Trigger "moon" automation, with passing condition
hass.bus.async_fire("test_event2")
await hass.async_block_till_done()
# Get trace
await client.send_json({"id": next_id(), "type": "automation/trace/list"})
response = await client.receive_json()
assert response["success"]
assert len(response["result"]["moon"]) == 3
assert len(response["result"]["sun"]) == 1
trace = response["result"]["sun"][0]
assert trace["last_action"] == "action/0"
assert trace["last_condition"] is None
assert trace["error"] == "Unable to find service test.automation"
assert trace["state"] == "stopped"
assert trace["timestamp"]
assert trace["trigger"] == "event 'test_event'"
assert trace["unique_id"] == "sun"
trace = response["result"]["moon"][0]
assert trace["last_action"] == "action/0"
assert trace["last_condition"] == "condition/0"
assert "error" not in trace
assert trace["state"] == "stopped"
assert trace["timestamp"]
assert trace["trigger"] == "event 'test_event2'"
assert trace["unique_id"] == "moon"
trace = response["result"]["moon"][1]
assert trace["last_action"] is None
assert trace["last_condition"] == "condition/0"
assert "error" not in trace
assert trace["state"] == "stopped"
assert trace["timestamp"]
assert trace["trigger"] == "event 'test_event3'"
assert trace["unique_id"] == "moon"
trace = response["result"]["moon"][2]
assert trace["last_action"] == "action/0"
assert trace["last_condition"] == "condition/0"
assert "error" not in trace
assert trace["state"] == "stopped"
assert trace["timestamp"]
assert trace["trigger"] == "event 'test_event2'"
assert trace["unique_id"] == "moon"
async def test_automation_breakpoints(hass, hass_ws_client):
"""Test automation breakpoints."""
id = 1
def next_id():
nonlocal id
id += 1
return id
async def assert_last_action(automation_id, expected_action, expected_state):
await client.send_json({"id": next_id(), "type": "automation/trace/list"})
response = await client.receive_json()
assert response["success"]
trace = response["result"][automation_id][-1]
assert trace["last_action"] == expected_action
assert trace["state"] == expected_state
return trace["run_id"]
sun_config = {
"id": "sun",
"trigger": {"platform": "event", "event_type": "test_event"},
"action": [
{"event": "event0"},
{"event": "event1"},
{"event": "event2"},
{"event": "event3"},
{"event": "event4"},
{"event": "event5"},
{"event": "event6"},
{"event": "event7"},
{"event": "event8"},
],
}
assert await async_setup_component(
hass,
"automation",
{
"automation": [
sun_config,
]
},
)
with patch.object(config, "SECTIONS", ["automation"]):
await async_setup_component(hass, "config", {})
client = await hass_ws_client()
await client.send_json(
{
"id": next_id(),
"type": "automation/debug/breakpoint/set",
"automation_id": "sun",
"node": "1",
}
)
response = await client.receive_json()
assert not response["success"]
await client.send_json(
{"id": next_id(), "type": "automation/debug/breakpoint/list"}
)
response = await client.receive_json()
assert response["success"]
assert response["result"] == []
subscription_id = next_id()
await client.send_json(
{"id": subscription_id, "type": "automation/debug/breakpoint/subscribe"}
)
response = await client.receive_json()
assert response["success"]
await client.send_json(
{
"id": next_id(),
"type": "automation/debug/breakpoint/set",
"automation_id": "sun",
"node": "action/1",
}
)
response = await client.receive_json()
assert response["success"]
await client.send_json(
{
"id": next_id(),
"type": "automation/debug/breakpoint/set",
"automation_id": "sun",
"node": "action/5",
}
)
response = await client.receive_json()
assert response["success"]
await client.send_json(
{"id": next_id(), "type": "automation/debug/breakpoint/list"}
)
response = await client.receive_json()
assert response["success"]
assert_lists_same(
response["result"],
[
{"node": "action/1", "run_id": "*", "automation_id": "sun"},
{"node": "action/5", "run_id": "*", "automation_id": "sun"},
],
)
# Trigger "sun" automation
hass.bus.async_fire("test_event")
response = await client.receive_json()
run_id = await assert_last_action("sun", "action/1", "running")
assert response["event"] == {
"automation_id": "sun",
"node": "action/1",
"run_id": run_id,
}
await client.send_json(
{
"id": next_id(),
"type": "automation/debug/step",
"automation_id": "sun",
"run_id": run_id,
}
)
response = await client.receive_json()
assert response["success"]
response = await client.receive_json()
run_id = await assert_last_action("sun", "action/2", "running")
assert response["event"] == {
"automation_id": "sun",
"node": "action/2",
"run_id": run_id,
}
await client.send_json(
{
"id": next_id(),
"type": "automation/debug/continue",
"automation_id": "sun",
"run_id": run_id,
}
)
response = await client.receive_json()
assert response["success"]
response = await client.receive_json()
run_id = await assert_last_action("sun", "action/5", "running")
assert response["event"] == {
"automation_id": "sun",
"node": "action/5",
"run_id": run_id,
}
await client.send_json(
{
"id": next_id(),
"type": "automation/debug/stop",
"automation_id": "sun",
"run_id": run_id,
}
)
response = await client.receive_json()
assert response["success"]
await hass.async_block_till_done()
await assert_last_action("sun", "action/5", "stopped")
async def test_automation_breakpoints_2(hass, hass_ws_client):
"""Test execution resumes and breakpoints are removed after subscription removed."""
id = 1
def next_id():
nonlocal id
id += 1
return id
async def assert_last_action(automation_id, expected_action, expected_state):
await client.send_json({"id": next_id(), "type": "automation/trace/list"})
response = await client.receive_json()
assert response["success"]
trace = response["result"][automation_id][-1]
assert trace["last_action"] == expected_action
assert trace["state"] == expected_state
return trace["run_id"]
sun_config = {
"id": "sun",
"trigger": {"platform": "event", "event_type": "test_event"},
"action": [
{"event": "event0"},
{"event": "event1"},
{"event": "event2"},
{"event": "event3"},
{"event": "event4"},
{"event": "event5"},
{"event": "event6"},
{"event": "event7"},
{"event": "event8"},
],
}
assert await async_setup_component(
hass,
"automation",
{
"automation": [
sun_config,
]
},
)
with patch.object(config, "SECTIONS", ["automation"]):
await async_setup_component(hass, "config", {})
client = await hass_ws_client()
subscription_id = next_id()
await client.send_json(
{"id": subscription_id, "type": "automation/debug/breakpoint/subscribe"}
)
response = await client.receive_json()
assert response["success"]
await client.send_json(
{
"id": next_id(),
"type": "automation/debug/breakpoint/set",
"automation_id": "sun",
"node": "action/1",
}
)
response = await client.receive_json()
assert response["success"]
# Trigger "sun" automation
hass.bus.async_fire("test_event")
response = await client.receive_json()
run_id = await assert_last_action("sun", "action/1", "running")
assert response["event"] == {
"automation_id": "sun",
"node": "action/1",
"run_id": run_id,
}
# Unsubscribe - execution should resume
await client.send_json(
{"id": next_id(), "type": "unsubscribe_events", "subscription": subscription_id}
)
response = await client.receive_json()
assert response["success"]
await hass.async_block_till_done()
await assert_last_action("sun", "action/8", "stopped")
# Should not be possible to set breakpoints
await client.send_json(
{
"id": next_id(),
"type": "automation/debug/breakpoint/set",
"automation_id": "sun",
"node": "1",
}
)
response = await client.receive_json()
assert not response["success"]
# Trigger "sun" automation, should finish without stopping on breakpoints
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
new_run_id = await assert_last_action("sun", "action/8", "stopped")
assert new_run_id != run_id
async def test_automation_breakpoints_3(hass, hass_ws_client):
"""Test breakpoints can be cleared."""
id = 1
def next_id():
nonlocal id
id += 1
return id
async def assert_last_action(automation_id, expected_action, expected_state):
await client.send_json({"id": next_id(), "type": "automation/trace/list"})
response = await client.receive_json()
assert response["success"]
trace = response["result"][automation_id][-1]
assert trace["last_action"] == expected_action
assert trace["state"] == expected_state
return trace["run_id"]
sun_config = {
"id": "sun",
"trigger": {"platform": "event", "event_type": "test_event"},
"action": [
{"event": "event0"},
{"event": "event1"},
{"event": "event2"},
{"event": "event3"},
{"event": "event4"},
{"event": "event5"},
{"event": "event6"},
{"event": "event7"},
{"event": "event8"},
],
}
assert await async_setup_component(
hass,
"automation",
{
"automation": [
sun_config,
]
},
)
with patch.object(config, "SECTIONS", ["automation"]):
await async_setup_component(hass, "config", {})
client = await hass_ws_client()
subscription_id = next_id()
await client.send_json(
{"id": subscription_id, "type": "automation/debug/breakpoint/subscribe"}
)
response = await client.receive_json()
assert response["success"]
await client.send_json(
{
"id": next_id(),
"type": "automation/debug/breakpoint/set",
"automation_id": "sun",
"node": "action/1",
}
)
response = await client.receive_json()
assert response["success"]
await client.send_json(
{
"id": next_id(),
"type": "automation/debug/breakpoint/set",
"automation_id": "sun",
"node": "action/5",
}
)
response = await client.receive_json()
assert response["success"]
# Trigger "sun" automation
hass.bus.async_fire("test_event")
response = await client.receive_json()
run_id = await assert_last_action("sun", "action/1", "running")
assert response["event"] == {
"automation_id": "sun",
"node": "action/1",
"run_id": run_id,
}
await client.send_json(
{
"id": next_id(),
"type": "automation/debug/continue",
"automation_id": "sun",
"run_id": run_id,
}
)
response = await client.receive_json()
assert response["success"]
response = await client.receive_json()
run_id = await assert_last_action("sun", "action/5", "running")
assert response["event"] == {
"automation_id": "sun",
"node": "action/5",
"run_id": run_id,
}
await client.send_json(
{
"id": next_id(),
"type": "automation/debug/stop",
"automation_id": "sun",
"run_id": run_id,
}
)
response = await client.receive_json()
assert response["success"]
await hass.async_block_till_done()
await assert_last_action("sun", "action/5", "stopped")
# Clear 1st breakpoint
await client.send_json(
{
"id": next_id(),
"type": "automation/debug/breakpoint/clear",
"automation_id": "sun",
"node": "action/1",
}
)
response = await client.receive_json()
assert response["success"]
# Trigger "sun" automation
hass.bus.async_fire("test_event")
response = await client.receive_json()
run_id = await assert_last_action("sun", "action/5", "running")
assert response["event"] == {
"automation_id": "sun",
"node": "action/5",
"run_id": run_id,
}

View File

@ -3,10 +3,9 @@ import json
from unittest.mock import patch
from homeassistant.bootstrap import async_setup_component
from homeassistant.components import automation, config
from homeassistant.components import config
from homeassistant.helpers import entity_registry as er
from tests.common import assert_lists_same
from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@ -166,772 +165,3 @@ async def test_delete_automation(hass, hass_client):
assert written[0][0]["id"] == "moon"
assert len(ent_reg.entities) == 1
async def test_get_automation_trace(hass, hass_ws_client):
"""Test tracing an automation."""
id = 1
def next_id():
nonlocal id
id += 1
return id
sun_config = {
"id": "sun",
"trigger": {"platform": "event", "event_type": "test_event"},
"action": {"service": "test.automation"},
}
moon_config = {
"id": "moon",
"trigger": [
{"platform": "event", "event_type": "test_event2"},
{"platform": "event", "event_type": "test_event3"},
],
"condition": {
"condition": "template",
"value_template": "{{ trigger.event.event_type=='test_event2' }}",
},
"action": {"event": "another_event"},
}
assert await async_setup_component(
hass,
"automation",
{
"automation": [
sun_config,
moon_config,
]
},
)
with patch.object(config, "SECTIONS", ["automation"]):
await async_setup_component(hass, "config", {})
client = await hass_ws_client()
await client.send_json({"id": next_id(), "type": "automation/trace/get"})
response = await client.receive_json()
assert response["success"]
assert response["result"] == {}
await client.send_json(
{"id": next_id(), "type": "automation/trace/get", "automation_id": "sun"}
)
response = await client.receive_json()
assert response["success"]
assert response["result"] == {"sun": []}
# Trigger "sun" automation
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
# Get trace
await client.send_json({"id": next_id(), "type": "automation/trace/get"})
response = await client.receive_json()
assert response["success"]
assert "moon" not in response["result"]
assert len(response["result"]["sun"]) == 1
trace = response["result"]["sun"][0]
assert len(trace["action_trace"]) == 1
assert len(trace["action_trace"]["action/0"]) == 1
assert trace["action_trace"]["action/0"][0]["error"]
assert "result" not in trace["action_trace"]["action/0"][0]
assert trace["condition_trace"] == {}
assert trace["config"] == sun_config
assert trace["context"]
assert trace["error"] == "Unable to find service test.automation"
assert trace["state"] == "stopped"
assert trace["trigger"]["description"] == "event 'test_event'"
assert trace["unique_id"] == "sun"
assert trace["variables"]
# Trigger "moon" automation, with passing condition
hass.bus.async_fire("test_event2")
await hass.async_block_till_done()
# Get trace
await client.send_json(
{"id": next_id(), "type": "automation/trace/get", "automation_id": "moon"}
)
response = await client.receive_json()
assert response["success"]
assert "sun" not in response["result"]
assert len(response["result"]["moon"]) == 1
trace = response["result"]["moon"][0]
assert len(trace["action_trace"]) == 1
assert len(trace["action_trace"]["action/0"]) == 1
assert "error" not in trace["action_trace"]["action/0"][0]
assert "result" not in trace["action_trace"]["action/0"][0]
assert len(trace["condition_trace"]) == 1
assert len(trace["condition_trace"]["condition/0"]) == 1
assert trace["condition_trace"]["condition/0"][0]["result"] == {"result": True}
assert trace["config"] == moon_config
assert trace["context"]
assert "error" not in trace
assert trace["state"] == "stopped"
assert trace["trigger"]["description"] == "event 'test_event2'"
assert trace["unique_id"] == "moon"
assert trace["variables"]
# Trigger "moon" automation, with failing condition
hass.bus.async_fire("test_event3")
await hass.async_block_till_done()
# Get trace
await client.send_json(
{"id": next_id(), "type": "automation/trace/get", "automation_id": "moon"}
)
response = await client.receive_json()
assert response["success"]
assert "sun" not in response["result"]
assert len(response["result"]["moon"]) == 2
trace = response["result"]["moon"][1]
assert len(trace["action_trace"]) == 0
assert len(trace["condition_trace"]) == 1
assert len(trace["condition_trace"]["condition/0"]) == 1
assert trace["condition_trace"]["condition/0"][0]["result"] == {"result": False}
assert trace["config"] == moon_config
assert trace["context"]
assert "error" not in trace
assert trace["state"] == "stopped"
assert trace["trigger"]["description"] == "event 'test_event3'"
assert trace["unique_id"] == "moon"
assert trace["variables"]
# Trigger "moon" automation, with passing condition
hass.bus.async_fire("test_event2")
await hass.async_block_till_done()
# Get trace
await client.send_json(
{"id": next_id(), "type": "automation/trace/get", "automation_id": "moon"}
)
response = await client.receive_json()
assert response["success"]
assert "sun" not in response["result"]
assert len(response["result"]["moon"]) == 3
trace = response["result"]["moon"][2]
assert len(trace["action_trace"]) == 1
assert len(trace["action_trace"]["action/0"]) == 1
assert "error" not in trace["action_trace"]["action/0"][0]
assert "result" not in trace["action_trace"]["action/0"][0]
assert len(trace["condition_trace"]) == 1
assert len(trace["condition_trace"]["condition/0"]) == 1
assert trace["condition_trace"]["condition/0"][0]["result"] == {"result": True}
assert trace["config"] == moon_config
assert trace["context"]
assert "error" not in trace
assert trace["state"] == "stopped"
assert trace["trigger"]["description"] == "event 'test_event2'"
assert trace["unique_id"] == "moon"
assert trace["variables"]
async def test_automation_trace_overflow(hass, hass_ws_client):
"""Test the number of stored traces per automation is limited."""
id = 1
def next_id():
nonlocal id
id += 1
return id
sun_config = {
"id": "sun",
"trigger": {"platform": "event", "event_type": "test_event"},
"action": {"event": "some_event"},
}
moon_config = {
"id": "moon",
"trigger": {"platform": "event", "event_type": "test_event2"},
"action": {"event": "another_event"},
}
assert await async_setup_component(
hass,
"automation",
{
"automation": [
sun_config,
moon_config,
]
},
)
with patch.object(config, "SECTIONS", ["automation"]):
await async_setup_component(hass, "config", {})
client = await hass_ws_client()
await client.send_json({"id": next_id(), "type": "automation/trace/list"})
response = await client.receive_json()
assert response["success"]
assert response["result"] == {}
# Trigger "sun" and "moon" automation once
hass.bus.async_fire("test_event")
hass.bus.async_fire("test_event2")
await hass.async_block_till_done()
# Get traces
await client.send_json({"id": next_id(), "type": "automation/trace/list"})
response = await client.receive_json()
assert response["success"]
assert len(response["result"]["moon"]) == 1
moon_run_id = response["result"]["moon"][0]["run_id"]
assert len(response["result"]["sun"]) == 1
# Trigger "moon" automation enough times to overflow the number of stored traces
for _ in range(automation.STORED_TRACES):
hass.bus.async_fire("test_event2")
await hass.async_block_till_done()
await client.send_json({"id": next_id(), "type": "automation/trace/list"})
response = await client.receive_json()
assert response["success"]
assert len(response["result"]["moon"]) == automation.STORED_TRACES
assert len(response["result"]["sun"]) == 1
assert int(response["result"]["moon"][0]["run_id"]) == int(moon_run_id) + 1
assert (
int(response["result"]["moon"][-1]["run_id"])
== int(moon_run_id) + automation.STORED_TRACES
)
async def test_list_automation_traces(hass, hass_ws_client):
"""Test listing automation traces."""
id = 1
def next_id():
nonlocal id
id += 1
return id
sun_config = {
"id": "sun",
"trigger": {"platform": "event", "event_type": "test_event"},
"action": {"service": "test.automation"},
}
moon_config = {
"id": "moon",
"trigger": [
{"platform": "event", "event_type": "test_event2"},
{"platform": "event", "event_type": "test_event3"},
],
"condition": {
"condition": "template",
"value_template": "{{ trigger.event.event_type=='test_event2' }}",
},
"action": {"event": "another_event"},
}
assert await async_setup_component(
hass,
"automation",
{
"automation": [
sun_config,
moon_config,
]
},
)
with patch.object(config, "SECTIONS", ["automation"]):
await async_setup_component(hass, "config", {})
client = await hass_ws_client()
await client.send_json({"id": next_id(), "type": "automation/trace/list"})
response = await client.receive_json()
assert response["success"]
assert response["result"] == {}
# Trigger "sun" automation
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
# Get trace
await client.send_json({"id": next_id(), "type": "automation/trace/list"})
response = await client.receive_json()
assert response["success"]
assert "moon" not in response["result"]
assert len(response["result"]["sun"]) == 1
# Trigger "moon" automation, with passing condition
hass.bus.async_fire("test_event2")
await hass.async_block_till_done()
# Trigger "moon" automation, with failing condition
hass.bus.async_fire("test_event3")
await hass.async_block_till_done()
# Trigger "moon" automation, with passing condition
hass.bus.async_fire("test_event2")
await hass.async_block_till_done()
# Get trace
await client.send_json({"id": next_id(), "type": "automation/trace/list"})
response = await client.receive_json()
assert response["success"]
assert len(response["result"]["moon"]) == 3
assert len(response["result"]["sun"]) == 1
trace = response["result"]["sun"][0]
assert trace["last_action"] == "action/0"
assert trace["last_condition"] is None
assert trace["error"] == "Unable to find service test.automation"
assert trace["state"] == "stopped"
assert trace["timestamp"]
assert trace["trigger"] == "event 'test_event'"
assert trace["unique_id"] == "sun"
trace = response["result"]["moon"][0]
assert trace["last_action"] == "action/0"
assert trace["last_condition"] == "condition/0"
assert "error" not in trace
assert trace["state"] == "stopped"
assert trace["timestamp"]
assert trace["trigger"] == "event 'test_event2'"
assert trace["unique_id"] == "moon"
trace = response["result"]["moon"][1]
assert trace["last_action"] is None
assert trace["last_condition"] == "condition/0"
assert "error" not in trace
assert trace["state"] == "stopped"
assert trace["timestamp"]
assert trace["trigger"] == "event 'test_event3'"
assert trace["unique_id"] == "moon"
trace = response["result"]["moon"][2]
assert trace["last_action"] == "action/0"
assert trace["last_condition"] == "condition/0"
assert "error" not in trace
assert trace["state"] == "stopped"
assert trace["timestamp"]
assert trace["trigger"] == "event 'test_event2'"
assert trace["unique_id"] == "moon"
async def test_automation_breakpoints(hass, hass_ws_client):
"""Test automation breakpoints."""
id = 1
def next_id():
nonlocal id
id += 1
return id
async def assert_last_action(automation_id, expected_action, expected_state):
await client.send_json({"id": next_id(), "type": "automation/trace/list"})
response = await client.receive_json()
assert response["success"]
trace = response["result"][automation_id][-1]
assert trace["last_action"] == expected_action
assert trace["state"] == expected_state
return trace["run_id"]
sun_config = {
"id": "sun",
"trigger": {"platform": "event", "event_type": "test_event"},
"action": [
{"event": "event0"},
{"event": "event1"},
{"event": "event2"},
{"event": "event3"},
{"event": "event4"},
{"event": "event5"},
{"event": "event6"},
{"event": "event7"},
{"event": "event8"},
],
}
assert await async_setup_component(
hass,
"automation",
{
"automation": [
sun_config,
]
},
)
with patch.object(config, "SECTIONS", ["automation"]):
await async_setup_component(hass, "config", {})
client = await hass_ws_client()
await client.send_json(
{
"id": next_id(),
"type": "automation/debug/breakpoint/set",
"automation_id": "sun",
"node": "1",
}
)
response = await client.receive_json()
assert not response["success"]
await client.send_json(
{"id": next_id(), "type": "automation/debug/breakpoint/list"}
)
response = await client.receive_json()
assert response["success"]
assert response["result"] == []
subscription_id = next_id()
await client.send_json(
{"id": subscription_id, "type": "automation/debug/breakpoint/subscribe"}
)
response = await client.receive_json()
assert response["success"]
await client.send_json(
{
"id": next_id(),
"type": "automation/debug/breakpoint/set",
"automation_id": "sun",
"node": "action/1",
}
)
response = await client.receive_json()
assert response["success"]
await client.send_json(
{
"id": next_id(),
"type": "automation/debug/breakpoint/set",
"automation_id": "sun",
"node": "action/5",
}
)
response = await client.receive_json()
assert response["success"]
await client.send_json(
{"id": next_id(), "type": "automation/debug/breakpoint/list"}
)
response = await client.receive_json()
assert response["success"]
assert_lists_same(
response["result"],
[
{"node": "action/1", "run_id": "*", "automation_id": "sun"},
{"node": "action/5", "run_id": "*", "automation_id": "sun"},
],
)
# Trigger "sun" automation
hass.bus.async_fire("test_event")
response = await client.receive_json()
run_id = await assert_last_action("sun", "action/1", "running")
assert response["event"] == {
"automation_id": "sun",
"node": "action/1",
"run_id": run_id,
}
await client.send_json(
{
"id": next_id(),
"type": "automation/debug/step",
"automation_id": "sun",
"run_id": run_id,
}
)
response = await client.receive_json()
assert response["success"]
response = await client.receive_json()
run_id = await assert_last_action("sun", "action/2", "running")
assert response["event"] == {
"automation_id": "sun",
"node": "action/2",
"run_id": run_id,
}
await client.send_json(
{
"id": next_id(),
"type": "automation/debug/continue",
"automation_id": "sun",
"run_id": run_id,
}
)
response = await client.receive_json()
assert response["success"]
response = await client.receive_json()
run_id = await assert_last_action("sun", "action/5", "running")
assert response["event"] == {
"automation_id": "sun",
"node": "action/5",
"run_id": run_id,
}
await client.send_json(
{
"id": next_id(),
"type": "automation/debug/stop",
"automation_id": "sun",
"run_id": run_id,
}
)
response = await client.receive_json()
assert response["success"]
await hass.async_block_till_done()
await assert_last_action("sun", "action/5", "stopped")
async def test_automation_breakpoints_2(hass, hass_ws_client):
"""Test execution resumes and breakpoints are removed after subscription removed."""
id = 1
def next_id():
nonlocal id
id += 1
return id
async def assert_last_action(automation_id, expected_action, expected_state):
await client.send_json({"id": next_id(), "type": "automation/trace/list"})
response = await client.receive_json()
assert response["success"]
trace = response["result"][automation_id][-1]
assert trace["last_action"] == expected_action
assert trace["state"] == expected_state
return trace["run_id"]
sun_config = {
"id": "sun",
"trigger": {"platform": "event", "event_type": "test_event"},
"action": [
{"event": "event0"},
{"event": "event1"},
{"event": "event2"},
{"event": "event3"},
{"event": "event4"},
{"event": "event5"},
{"event": "event6"},
{"event": "event7"},
{"event": "event8"},
],
}
assert await async_setup_component(
hass,
"automation",
{
"automation": [
sun_config,
]
},
)
with patch.object(config, "SECTIONS", ["automation"]):
await async_setup_component(hass, "config", {})
client = await hass_ws_client()
subscription_id = next_id()
await client.send_json(
{"id": subscription_id, "type": "automation/debug/breakpoint/subscribe"}
)
response = await client.receive_json()
assert response["success"]
await client.send_json(
{
"id": next_id(),
"type": "automation/debug/breakpoint/set",
"automation_id": "sun",
"node": "action/1",
}
)
response = await client.receive_json()
assert response["success"]
# Trigger "sun" automation
hass.bus.async_fire("test_event")
response = await client.receive_json()
run_id = await assert_last_action("sun", "action/1", "running")
assert response["event"] == {
"automation_id": "sun",
"node": "action/1",
"run_id": run_id,
}
# Unsubscribe - execution should resume
await client.send_json(
{"id": next_id(), "type": "unsubscribe_events", "subscription": subscription_id}
)
response = await client.receive_json()
assert response["success"]
await hass.async_block_till_done()
await assert_last_action("sun", "action/8", "stopped")
# Should not be possible to set breakpoints
await client.send_json(
{
"id": next_id(),
"type": "automation/debug/breakpoint/set",
"automation_id": "sun",
"node": "1",
}
)
response = await client.receive_json()
assert not response["success"]
# Trigger "sun" automation, should finish without stopping on breakpoints
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
new_run_id = await assert_last_action("sun", "action/8", "stopped")
assert new_run_id != run_id
async def test_automation_breakpoints_3(hass, hass_ws_client):
"""Test breakpoints can be cleared."""
id = 1
def next_id():
nonlocal id
id += 1
return id
async def assert_last_action(automation_id, expected_action, expected_state):
await client.send_json({"id": next_id(), "type": "automation/trace/list"})
response = await client.receive_json()
assert response["success"]
trace = response["result"][automation_id][-1]
assert trace["last_action"] == expected_action
assert trace["state"] == expected_state
return trace["run_id"]
sun_config = {
"id": "sun",
"trigger": {"platform": "event", "event_type": "test_event"},
"action": [
{"event": "event0"},
{"event": "event1"},
{"event": "event2"},
{"event": "event3"},
{"event": "event4"},
{"event": "event5"},
{"event": "event6"},
{"event": "event7"},
{"event": "event8"},
],
}
assert await async_setup_component(
hass,
"automation",
{
"automation": [
sun_config,
]
},
)
with patch.object(config, "SECTIONS", ["automation"]):
await async_setup_component(hass, "config", {})
client = await hass_ws_client()
subscription_id = next_id()
await client.send_json(
{"id": subscription_id, "type": "automation/debug/breakpoint/subscribe"}
)
response = await client.receive_json()
assert response["success"]
await client.send_json(
{
"id": next_id(),
"type": "automation/debug/breakpoint/set",
"automation_id": "sun",
"node": "action/1",
}
)
response = await client.receive_json()
assert response["success"]
await client.send_json(
{
"id": next_id(),
"type": "automation/debug/breakpoint/set",
"automation_id": "sun",
"node": "action/5",
}
)
response = await client.receive_json()
assert response["success"]
# Trigger "sun" automation
hass.bus.async_fire("test_event")
response = await client.receive_json()
run_id = await assert_last_action("sun", "action/1", "running")
assert response["event"] == {
"automation_id": "sun",
"node": "action/1",
"run_id": run_id,
}
await client.send_json(
{
"id": next_id(),
"type": "automation/debug/continue",
"automation_id": "sun",
"run_id": run_id,
}
)
response = await client.receive_json()
assert response["success"]
response = await client.receive_json()
run_id = await assert_last_action("sun", "action/5", "running")
assert response["event"] == {
"automation_id": "sun",
"node": "action/5",
"run_id": run_id,
}
await client.send_json(
{
"id": next_id(),
"type": "automation/debug/stop",
"automation_id": "sun",
"run_id": run_id,
}
)
response = await client.receive_json()
assert response["success"]
await hass.async_block_till_done()
await assert_last_action("sun", "action/5", "stopped")
# Clear 1st breakpoint
await client.send_json(
{
"id": next_id(),
"type": "automation/debug/breakpoint/clear",
"automation_id": "sun",
"node": "action/1",
}
)
response = await client.receive_json()
assert response["success"]
# Trigger "sun" automation
hass.bus.async_fire("test_event")
response = await client.receive_json()
run_id = await assert_last_action("sun", "action/5", "running")
assert response["event"] == {
"automation_id": "sun",
"node": "action/5",
"run_id": run_id,
}