mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
Store automation and script traces (#56894)
* Store automation and script traces * Pylint * Deduplicate code * Fix issues when no stored traces are available * Store serialized data for restored traces * Update WS API * Update test * Restore context * Improve tests * Add new test files * Rename restore_traces to async_restore_traces * Refactor trace.websocket_api * Defer loading stored traces * Lint * Revert refactoring which is no longer needed * Correct order when restoring traces * Apply suggestion from code review * Improve test coverage * Apply suggestions from code review
This commit is contained in:
parent
29c062fcc4
commit
961ee717ef
@ -228,7 +228,6 @@ def areas_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)
|
||||
|
||||
# To register the automation blueprints
|
||||
|
@ -8,6 +8,8 @@ from homeassistant.components.trace import ActionTrace, async_store_trace
|
||||
from homeassistant.components.trace.const import CONF_STORED_TRACES
|
||||
from homeassistant.core import Context
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
# mypy: allow-untyped-calls, allow-untyped-defs
|
||||
# mypy: no-check-untyped-defs, no-warn-return-any
|
||||
|
||||
@ -15,6 +17,8 @@ from homeassistant.core import Context
|
||||
class AutomationTrace(ActionTrace):
|
||||
"""Container for automation trace."""
|
||||
|
||||
_domain = DOMAIN
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
item_id: str,
|
||||
@ -23,8 +27,7 @@ class AutomationTrace(ActionTrace):
|
||||
context: Context,
|
||||
) -> None:
|
||||
"""Container for automation trace."""
|
||||
key = ("automation", item_id)
|
||||
super().__init__(key, config, blueprint_inputs, context)
|
||||
super().__init__(item_id, config, blueprint_inputs, context)
|
||||
self._trigger_description: str | None = None
|
||||
|
||||
def set_trigger_description(self, trigger: str) -> None:
|
||||
@ -33,6 +36,9 @@ class AutomationTrace(ActionTrace):
|
||||
|
||||
def as_short_dict(self) -> dict[str, Any]:
|
||||
"""Return a brief dictionary version of this AutomationTrace."""
|
||||
if self._short_dict:
|
||||
return self._short_dict
|
||||
|
||||
result = super().as_short_dict()
|
||||
result["trigger"] = self._trigger_description
|
||||
return result
|
||||
|
@ -9,20 +9,13 @@ from homeassistant.components.trace import ActionTrace, async_store_trace
|
||||
from homeassistant.components.trace.const import CONF_STORED_TRACES
|
||||
from homeassistant.core import Context, HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
class ScriptTrace(ActionTrace):
|
||||
"""Container for automation trace."""
|
||||
"""Container for script trace."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
item_id: str,
|
||||
config: dict[str, Any],
|
||||
blueprint_inputs: dict[str, Any],
|
||||
context: Context,
|
||||
) -> None:
|
||||
"""Container for automation trace."""
|
||||
key = ("script", item_id)
|
||||
super().__init__(key, config, blueprint_inputs, context)
|
||||
_domain = DOMAIN
|
||||
|
||||
|
||||
@contextmanager
|
||||
|
@ -1,15 +1,20 @@
|
||||
"""Support for script and automation tracing and debugging."""
|
||||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
from collections import deque
|
||||
import datetime as dt
|
||||
from itertools import count
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import Context
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.json import ExtendedJSONEncoder
|
||||
from homeassistant.helpers.storage import Store
|
||||
from homeassistant.helpers.trace import (
|
||||
TraceElement,
|
||||
script_execution_get,
|
||||
@ -18,13 +23,25 @@ from homeassistant.helpers.trace import (
|
||||
trace_set_child_id,
|
||||
)
|
||||
import homeassistant.util.dt as dt_util
|
||||
import homeassistant.util.uuid as uuid_util
|
||||
|
||||
from . import websocket_api
|
||||
from .const import CONF_STORED_TRACES, DATA_TRACE, DEFAULT_STORED_TRACES
|
||||
from .const import (
|
||||
CONF_STORED_TRACES,
|
||||
DATA_TRACE,
|
||||
DATA_TRACE_STORE,
|
||||
DATA_TRACES_RESTORED,
|
||||
DEFAULT_STORED_TRACES,
|
||||
)
|
||||
from .utils import LimitedSizeDict
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "trace"
|
||||
|
||||
STORAGE_KEY = "trace.saved_traces"
|
||||
STORAGE_VERSION = 1
|
||||
|
||||
TRACE_CONFIG_SCHEMA = {
|
||||
vol.Optional(CONF_STORED_TRACES, default=DEFAULT_STORED_TRACES): cv.positive_int
|
||||
}
|
||||
@ -34,13 +51,89 @@ async def async_setup(hass, config):
|
||||
"""Initialize the trace integration."""
|
||||
hass.data[DATA_TRACE] = {}
|
||||
websocket_api.async_setup(hass)
|
||||
store = Store(hass, STORAGE_VERSION, STORAGE_KEY, encoder=ExtendedJSONEncoder)
|
||||
hass.data[DATA_TRACE_STORE] = store
|
||||
|
||||
async def _async_store_traces_at_stop(*_) -> None:
|
||||
"""Save traces to storage."""
|
||||
_LOGGER.debug("Storing traces")
|
||||
try:
|
||||
await store.async_save(
|
||||
{
|
||||
key: list(traces.values())
|
||||
for key, traces in hass.data[DATA_TRACE].items()
|
||||
}
|
||||
)
|
||||
except HomeAssistantError as exc:
|
||||
_LOGGER.error("Error storing traces", exc_info=exc)
|
||||
|
||||
# Store traces when stopping hass
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_store_traces_at_stop)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_get_trace(hass, key, run_id):
|
||||
"""Return the requested trace."""
|
||||
# Restore saved traces if not done
|
||||
await async_restore_traces(hass)
|
||||
|
||||
return hass.data[DATA_TRACE][key][run_id].as_extended_dict()
|
||||
|
||||
|
||||
async def async_list_contexts(hass, key):
|
||||
"""List contexts for which we have traces."""
|
||||
# Restore saved traces if not done
|
||||
await async_restore_traces(hass)
|
||||
|
||||
if key is not None:
|
||||
values = {key: hass.data[DATA_TRACE].get(key, {})}
|
||||
else:
|
||||
values = hass.data[DATA_TRACE]
|
||||
|
||||
def _trace_id(run_id, key) -> dict:
|
||||
"""Make trace_id for the response."""
|
||||
domain, item_id = key.split(".", 1)
|
||||
return {"run_id": run_id, "domain": domain, "item_id": item_id}
|
||||
|
||||
return {
|
||||
trace.context.id: _trace_id(trace.run_id, key)
|
||||
for key, traces in values.items()
|
||||
for trace in traces.values()
|
||||
}
|
||||
|
||||
|
||||
def _get_debug_traces(hass, key):
|
||||
"""Return a serializable list of debug traces for a script or automation."""
|
||||
traces = []
|
||||
|
||||
for trace in hass.data[DATA_TRACE].get(key, {}).values():
|
||||
traces.append(trace.as_short_dict())
|
||||
|
||||
return traces
|
||||
|
||||
|
||||
async def async_list_traces(hass, wanted_domain, wanted_key):
|
||||
"""List traces for a domain."""
|
||||
# Restore saved traces if not done already
|
||||
await async_restore_traces(hass)
|
||||
|
||||
if not wanted_key:
|
||||
traces = []
|
||||
for key in hass.data[DATA_TRACE]:
|
||||
domain = key.split(".", 1)[0]
|
||||
if domain == wanted_domain:
|
||||
traces.extend(_get_debug_traces(hass, key))
|
||||
else:
|
||||
traces = _get_debug_traces(hass, wanted_key)
|
||||
|
||||
return traces
|
||||
|
||||
|
||||
def async_store_trace(hass, trace, stored_traces):
|
||||
"""Store a trace if its item_id is valid."""
|
||||
"""Store a trace if its key is valid."""
|
||||
key = trace.key
|
||||
if key[1]:
|
||||
if key:
|
||||
traces = hass.data[DATA_TRACE]
|
||||
if key not in traces:
|
||||
traces[key] = LimitedSizeDict(size_limit=stored_traces)
|
||||
@ -49,14 +142,79 @@ def async_store_trace(hass, trace, stored_traces):
|
||||
traces[key][trace.run_id] = trace
|
||||
|
||||
|
||||
class ActionTrace:
|
||||
def _async_store_restored_trace(hass, trace):
|
||||
"""Store a restored trace and move it to the end of the LimitedSizeDict."""
|
||||
key = trace.key
|
||||
traces = hass.data[DATA_TRACE]
|
||||
if key not in traces:
|
||||
traces[key] = LimitedSizeDict()
|
||||
traces[key][trace.run_id] = trace
|
||||
traces[key].move_to_end(trace.run_id, last=False)
|
||||
|
||||
|
||||
async def async_restore_traces(hass):
|
||||
"""Restore saved traces."""
|
||||
if DATA_TRACES_RESTORED in hass.data:
|
||||
return
|
||||
|
||||
hass.data[DATA_TRACES_RESTORED] = True
|
||||
|
||||
store = hass.data[DATA_TRACE_STORE]
|
||||
try:
|
||||
restored_traces = await store.async_load() or {}
|
||||
except HomeAssistantError:
|
||||
_LOGGER.exception("Error loading traces")
|
||||
restored_traces = {}
|
||||
|
||||
for key, traces in restored_traces.items():
|
||||
# Add stored traces in reversed order to priorize the newest traces
|
||||
for json_trace in reversed(traces):
|
||||
if (
|
||||
(stored_traces := hass.data[DATA_TRACE].get(key))
|
||||
and stored_traces.size_limit is not None
|
||||
and len(stored_traces) >= stored_traces.size_limit
|
||||
):
|
||||
break
|
||||
|
||||
try:
|
||||
trace = RestoredTrace(json_trace)
|
||||
# Catch any exception to not blow up if the stored trace is invalid
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Failed to restore trace")
|
||||
continue
|
||||
_async_store_restored_trace(hass, trace)
|
||||
|
||||
|
||||
class BaseTrace(abc.ABC):
|
||||
"""Base container for a script or automation trace."""
|
||||
|
||||
_run_ids = count(0)
|
||||
context: Context
|
||||
key: str
|
||||
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
"""Return an dictionary version of this ActionTrace for saving."""
|
||||
return {
|
||||
"extended_dict": self.as_extended_dict(),
|
||||
"short_dict": self.as_short_dict(),
|
||||
}
|
||||
|
||||
@abc.abstractmethod
|
||||
def as_extended_dict(self) -> dict[str, Any]:
|
||||
"""Return an extended dictionary version of this ActionTrace."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def as_short_dict(self) -> dict[str, Any]:
|
||||
"""Return a brief dictionary version of this ActionTrace."""
|
||||
|
||||
|
||||
class ActionTrace(BaseTrace):
|
||||
"""Base container for a script or automation trace."""
|
||||
|
||||
_domain: str | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
key: tuple[str, str],
|
||||
item_id: str,
|
||||
config: dict[str, Any],
|
||||
blueprint_inputs: dict[str, Any],
|
||||
context: Context,
|
||||
@ -69,16 +227,18 @@ class ActionTrace:
|
||||
self._error: Exception | None = None
|
||||
self._state: str = "running"
|
||||
self._script_execution: str | None = None
|
||||
self.run_id: str = str(next(self._run_ids))
|
||||
self.run_id: str = uuid_util.random_uuid_hex()
|
||||
self._timestamp_finish: dt.datetime | None = None
|
||||
self._timestamp_start: dt.datetime = dt_util.utcnow()
|
||||
self.key: tuple[str, str] = key
|
||||
self.key = f"{self._domain}.{item_id}"
|
||||
self._dict: dict[str, Any] | None = None
|
||||
self._short_dict: dict[str, Any] | None = None
|
||||
if trace_id_get():
|
||||
trace_set_child_id(self.key, self.run_id)
|
||||
trace_id_set((key, self.run_id))
|
||||
trace_id_set((self.key, self.run_id))
|
||||
|
||||
def set_trace(self, trace: dict[str, deque[TraceElement]]) -> None:
|
||||
"""Set trace."""
|
||||
"""Set action trace."""
|
||||
self._trace = trace
|
||||
|
||||
def set_error(self, ex: Exception) -> None:
|
||||
@ -91,10 +251,12 @@ class ActionTrace:
|
||||
self._state = "stopped"
|
||||
self._script_execution = script_execution_get()
|
||||
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
"""Return dictionary version of this ActionTrace."""
|
||||
def as_extended_dict(self) -> dict[str, Any]:
|
||||
"""Return an extended dictionary version of this ActionTrace."""
|
||||
if self._dict:
|
||||
return self._dict
|
||||
|
||||
result = self.as_short_dict()
|
||||
result = dict(self.as_short_dict())
|
||||
|
||||
traces = {}
|
||||
if self._trace:
|
||||
@ -110,15 +272,21 @@ class ActionTrace:
|
||||
}
|
||||
)
|
||||
|
||||
if self._state == "stopped":
|
||||
# Execution has stopped, save the result
|
||||
self._dict = result
|
||||
return result
|
||||
|
||||
def as_short_dict(self) -> dict[str, Any]:
|
||||
"""Return a brief dictionary version of this ActionTrace."""
|
||||
if self._short_dict:
|
||||
return self._short_dict
|
||||
|
||||
last_step = None
|
||||
|
||||
if self._trace:
|
||||
last_step = list(self._trace)[-1]
|
||||
domain, item_id = self.key.split(".", 1)
|
||||
|
||||
result = {
|
||||
"last_step": last_step,
|
||||
@ -129,10 +297,40 @@ class ActionTrace:
|
||||
"start": self._timestamp_start,
|
||||
"finish": self._timestamp_finish,
|
||||
},
|
||||
"domain": self.key[0],
|
||||
"item_id": self.key[1],
|
||||
"domain": domain,
|
||||
"item_id": item_id,
|
||||
}
|
||||
if self._error is not None:
|
||||
result["error"] = str(self._error)
|
||||
|
||||
if self._state == "stopped":
|
||||
# Execution has stopped, save the result
|
||||
self._short_dict = result
|
||||
return result
|
||||
|
||||
|
||||
class RestoredTrace(BaseTrace):
|
||||
"""Container for a restored script or automation trace."""
|
||||
|
||||
def __init__(self, data: dict[str, Any]) -> None:
|
||||
"""Restore from dict."""
|
||||
extended_dict = data["extended_dict"]
|
||||
short_dict = data["short_dict"]
|
||||
context = Context(
|
||||
user_id=extended_dict["context"]["user_id"],
|
||||
parent_id=extended_dict["context"]["parent_id"],
|
||||
id=extended_dict["context"]["id"],
|
||||
)
|
||||
self.context = context
|
||||
self.key = f"{extended_dict['domain']}.{extended_dict['item_id']}"
|
||||
self.run_id = extended_dict["run_id"]
|
||||
self._dict = extended_dict
|
||||
self._short_dict = short_dict
|
||||
|
||||
def as_extended_dict(self) -> dict[str, Any]:
|
||||
"""Return an extended dictionary version of this RestoredTrace."""
|
||||
return self._dict
|
||||
|
||||
def as_short_dict(self) -> dict[str, Any]:
|
||||
"""Return a brief dictionary version of this RestoredTrace."""
|
||||
return self._short_dict
|
||||
|
@ -2,4 +2,6 @@
|
||||
|
||||
CONF_STORED_TRACES = "stored_traces"
|
||||
DATA_TRACE = "trace"
|
||||
DATA_TRACE_STORE = "trace_store"
|
||||
DATA_TRACES_RESTORED = "trace_traces_restored"
|
||||
DEFAULT_STORED_TRACES = 5 # Stored traces per script or automation
|
||||
|
@ -3,7 +3,7 @@ import json
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.components import trace, websocket_api
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
@ -24,8 +24,6 @@ from homeassistant.helpers.script import (
|
||||
debug_stop,
|
||||
)
|
||||
|
||||
from .const import DATA_TRACE
|
||||
|
||||
# mypy: allow-untyped-calls, allow-untyped-defs
|
||||
|
||||
TRACE_DOMAINS = ("automation", "script")
|
||||
@ -46,7 +44,6 @@ def async_setup(hass: HomeAssistant) -> None:
|
||||
websocket_api.async_register_command(hass, websocket_subscribe_breakpoint_events)
|
||||
|
||||
|
||||
@callback
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
@ -56,37 +53,27 @@ def async_setup(hass: HomeAssistant) -> None:
|
||||
vol.Required("run_id"): str,
|
||||
}
|
||||
)
|
||||
def websocket_trace_get(hass, connection, msg):
|
||||
@websocket_api.async_response
|
||||
async def websocket_trace_get(hass, connection, msg):
|
||||
"""Get a script or automation trace."""
|
||||
key = (msg["domain"], msg["item_id"])
|
||||
key = f"{msg['domain']}.{msg['item_id']}"
|
||||
run_id = msg["run_id"]
|
||||
|
||||
try:
|
||||
trace = hass.data[DATA_TRACE][key][run_id]
|
||||
requested_trace = await trace.async_get_trace(hass, key, run_id)
|
||||
except KeyError:
|
||||
connection.send_error(
|
||||
msg["id"], websocket_api.ERR_NOT_FOUND, "The trace could not be found"
|
||||
)
|
||||
return
|
||||
|
||||
message = websocket_api.messages.result_message(msg["id"], trace)
|
||||
message = websocket_api.messages.result_message(msg["id"], requested_trace)
|
||||
|
||||
connection.send_message(
|
||||
json.dumps(message, cls=ExtendedJSONEncoder, allow_nan=False)
|
||||
)
|
||||
|
||||
|
||||
def get_debug_traces(hass, key):
|
||||
"""Return a serializable list of debug traces for a script or automation."""
|
||||
traces = []
|
||||
|
||||
for trace in hass.data[DATA_TRACE].get(key, {}).values():
|
||||
traces.append(trace.as_short_dict())
|
||||
|
||||
return traces
|
||||
|
||||
|
||||
@callback
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
@ -95,23 +82,17 @@ def get_debug_traces(hass, key):
|
||||
vol.Optional("item_id", "id"): str,
|
||||
}
|
||||
)
|
||||
def websocket_trace_list(hass, connection, msg):
|
||||
@websocket_api.async_response
|
||||
async def websocket_trace_list(hass, connection, msg):
|
||||
"""Summarize script and automation traces."""
|
||||
domain = msg["domain"]
|
||||
key = (domain, msg["item_id"]) if "item_id" in msg else None
|
||||
wanted_domain = msg["domain"]
|
||||
key = f"{msg['domain']}.{msg['item_id']}" if "item_id" in msg else None
|
||||
|
||||
if not key:
|
||||
traces = []
|
||||
for key in hass.data[DATA_TRACE]:
|
||||
if key[0] == domain:
|
||||
traces.extend(get_debug_traces(hass, key))
|
||||
else:
|
||||
traces = get_debug_traces(hass, key)
|
||||
traces = await trace.async_list_traces(hass, wanted_domain, key)
|
||||
|
||||
connection.send_result(msg["id"], traces)
|
||||
|
||||
|
||||
@callback
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
@ -120,20 +101,12 @@ def websocket_trace_list(hass, connection, msg):
|
||||
vol.Inclusive("item_id", "id"): str,
|
||||
}
|
||||
)
|
||||
def websocket_trace_contexts(hass, connection, msg):
|
||||
@websocket_api.async_response
|
||||
async def websocket_trace_contexts(hass, connection, msg):
|
||||
"""Retrieve contexts we have traces for."""
|
||||
key = (msg["domain"], msg["item_id"]) if "item_id" in msg else None
|
||||
key = f"{msg['domain']}.{msg['item_id']}" if "item_id" in msg else None
|
||||
|
||||
if key is not None:
|
||||
values = {key: hass.data[DATA_TRACE].get(key, {})}
|
||||
else:
|
||||
values = hass.data[DATA_TRACE]
|
||||
|
||||
contexts = {
|
||||
trace.context.id: {"run_id": trace.run_id, "domain": key[0], "item_id": key[1]}
|
||||
for key, traces in values.items()
|
||||
for trace in traces.values()
|
||||
}
|
||||
contexts = await trace.async_list_contexts(hass, key)
|
||||
|
||||
connection.send_result(msg["id"], contexts)
|
||||
|
||||
@ -151,7 +124,7 @@ def websocket_trace_contexts(hass, connection, msg):
|
||||
)
|
||||
def websocket_breakpoint_set(hass, connection, msg):
|
||||
"""Set breakpoint."""
|
||||
key = (msg["domain"], msg["item_id"])
|
||||
key = f"{msg['domain']}.{msg['item_id']}"
|
||||
node = msg["node"]
|
||||
run_id = msg.get("run_id")
|
||||
|
||||
@ -178,7 +151,7 @@ def websocket_breakpoint_set(hass, connection, msg):
|
||||
)
|
||||
def websocket_breakpoint_clear(hass, connection, msg):
|
||||
"""Clear breakpoint."""
|
||||
key = (msg["domain"], msg["item_id"])
|
||||
key = f"{msg['domain']}.{msg['item_id']}"
|
||||
node = msg["node"]
|
||||
run_id = msg.get("run_id")
|
||||
|
||||
@ -194,7 +167,8 @@ def websocket_breakpoint_list(hass, connection, msg):
|
||||
"""List breakpoints."""
|
||||
breakpoints = breakpoint_list(hass)
|
||||
for _breakpoint in breakpoints:
|
||||
_breakpoint["domain"], _breakpoint["item_id"] = _breakpoint.pop("key")
|
||||
key = _breakpoint.pop("key")
|
||||
_breakpoint["domain"], _breakpoint["item_id"] = key.split(".", 1)
|
||||
|
||||
connection.send_result(msg["id"], breakpoints)
|
||||
|
||||
@ -210,12 +184,13 @@ def websocket_subscribe_breakpoint_events(hass, connection, msg):
|
||||
@callback
|
||||
def breakpoint_hit(key, run_id, node):
|
||||
"""Forward events to websocket."""
|
||||
domain, item_id = key.split(".", 1)
|
||||
connection.send_message(
|
||||
websocket_api.event_message(
|
||||
msg["id"],
|
||||
{
|
||||
"domain": key[0],
|
||||
"item_id": key[1],
|
||||
"domain": domain,
|
||||
"item_id": item_id,
|
||||
"run_id": run_id,
|
||||
"node": node,
|
||||
},
|
||||
@ -254,7 +229,7 @@ def websocket_subscribe_breakpoint_events(hass, connection, msg):
|
||||
)
|
||||
def websocket_debug_continue(hass, connection, msg):
|
||||
"""Resume execution of halted script or automation."""
|
||||
key = (msg["domain"], msg["item_id"])
|
||||
key = f"{msg['domain']}.{msg['item_id']}"
|
||||
run_id = msg["run_id"]
|
||||
|
||||
result = debug_continue(hass, key, run_id)
|
||||
@ -274,7 +249,7 @@ def websocket_debug_continue(hass, connection, msg):
|
||||
)
|
||||
def websocket_debug_step(hass, connection, msg):
|
||||
"""Single step a halted script or automation."""
|
||||
key = (msg["domain"], msg["item_id"])
|
||||
key = f"{msg['domain']}.{msg['item_id']}"
|
||||
run_id = msg["run_id"]
|
||||
|
||||
result = debug_step(hass, key, run_id)
|
||||
@ -294,7 +269,7 @@ def websocket_debug_step(hass, connection, msg):
|
||||
)
|
||||
def websocket_debug_stop(hass, connection, msg):
|
||||
"""Stop a halted script or automation."""
|
||||
key = (msg["domain"], msg["item_id"])
|
||||
key = f"{msg['domain']}.{msg['item_id']}"
|
||||
run_id = msg["run_id"]
|
||||
|
||||
result = debug_stop(hass, key, run_id)
|
||||
|
@ -17,7 +17,7 @@ class TraceElement:
|
||||
|
||||
def __init__(self, variables: TemplateVarsType, path: str) -> None:
|
||||
"""Container for trace data."""
|
||||
self._child_key: tuple[str, str] | None = None
|
||||
self._child_key: str | None = None
|
||||
self._child_run_id: str | None = None
|
||||
self._error: Exception | None = None
|
||||
self.path: str = path
|
||||
@ -40,7 +40,7 @@ class TraceElement:
|
||||
"""Container for trace data."""
|
||||
return str(self.as_dict())
|
||||
|
||||
def set_child_id(self, child_key: tuple[str, str], child_run_id: str) -> None:
|
||||
def set_child_id(self, child_key: str, child_run_id: str) -> None:
|
||||
"""Set trace id of a nested script run."""
|
||||
self._child_key = child_key
|
||||
self._child_run_id = child_run_id
|
||||
@ -62,9 +62,10 @@ class TraceElement:
|
||||
"""Return dictionary version of this TraceElement."""
|
||||
result: dict[str, Any] = {"path": self.path, "timestamp": self._timestamp}
|
||||
if self._child_key is not None:
|
||||
domain, item_id = self._child_key.split(".", 1)
|
||||
result["child_id"] = {
|
||||
"domain": self._child_key[0],
|
||||
"item_id": self._child_key[1],
|
||||
"domain": domain,
|
||||
"item_id": item_id,
|
||||
"run_id": str(self._child_run_id),
|
||||
}
|
||||
if self._variables:
|
||||
@ -91,8 +92,8 @@ trace_path_stack_cv: ContextVar[list[str] | None] = ContextVar(
|
||||
)
|
||||
# Copy of last variables
|
||||
variables_cv: ContextVar[Any | None] = ContextVar("variables_cv", default=None)
|
||||
# (domain, item_id) + Run ID
|
||||
trace_id_cv: ContextVar[tuple[tuple[str, str], str] | None] = ContextVar(
|
||||
# (domain.item_id, Run ID)
|
||||
trace_id_cv: ContextVar[tuple[str, str] | None] = ContextVar(
|
||||
"trace_id_cv", default=None
|
||||
)
|
||||
# Reason for stopped script execution
|
||||
@ -101,12 +102,12 @@ script_execution_cv: ContextVar[StopReason | None] = ContextVar(
|
||||
)
|
||||
|
||||
|
||||
def trace_id_set(trace_id: tuple[tuple[str, str], str]) -> None:
|
||||
def trace_id_set(trace_id: tuple[str, str]) -> None:
|
||||
"""Set id of the current trace."""
|
||||
trace_id_cv.set(trace_id)
|
||||
|
||||
|
||||
def trace_id_get() -> tuple[tuple[str, str], str] | None:
|
||||
def trace_id_get() -> tuple[str, str] | None:
|
||||
"""Get id if the current trace."""
|
||||
return trace_id_cv.get()
|
||||
|
||||
@ -182,7 +183,7 @@ def trace_clear() -> None:
|
||||
script_execution_cv.set(StopReason())
|
||||
|
||||
|
||||
def trace_set_child_id(child_key: tuple[str, str], child_run_id: str) -> None:
|
||||
def trace_set_child_id(child_key: str, child_run_id: str) -> None:
|
||||
"""Set child trace_id of TraceElement at the top of the stack."""
|
||||
node = cast(TraceElement, trace_stack_top(trace_stack_cv))
|
||||
if node:
|
||||
|
@ -1,7 +1,6 @@
|
||||
"""The tests for the Script component."""
|
||||
# pylint: disable=protected-access
|
||||
import asyncio
|
||||
import unittest
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
@ -29,113 +28,62 @@ from homeassistant.exceptions import ServiceNotFound
|
||||
from homeassistant.helpers import template
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
from homeassistant.helpers.service import async_get_all_descriptions
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.setup import async_setup_component, setup_component
|
||||
from homeassistant.setup import async_setup_component
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from tests.common import async_mock_service, get_test_home_assistant, mock_restore_cache
|
||||
from tests.common import async_mock_service, mock_restore_cache
|
||||
from tests.components.logbook.test_init import MockLazyEventPartialState
|
||||
|
||||
ENTITY_ID = "script.test"
|
||||
|
||||
|
||||
@bind_hass
|
||||
def turn_on(hass, entity_id, variables=None, context=None):
|
||||
"""Turn script on.
|
||||
async def test_passing_variables(hass):
|
||||
"""Test different ways of passing in variables."""
|
||||
mock_restore_cache(hass, ())
|
||||
calls = []
|
||||
context = Context()
|
||||
|
||||
This is a legacy helper method. Do not use it for new tests.
|
||||
"""
|
||||
_, object_id = split_entity_id(entity_id)
|
||||
@callback
|
||||
def record_call(service):
|
||||
"""Add recorded event to set."""
|
||||
calls.append(service)
|
||||
|
||||
hass.services.call(DOMAIN, object_id, variables, context=context)
|
||||
hass.services.async_register("test", "script", record_call)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def turn_off(hass, entity_id):
|
||||
"""Turn script on.
|
||||
|
||||
This is a legacy helper method. Do not use it for new tests.
|
||||
"""
|
||||
hass.services.call(DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id})
|
||||
|
||||
|
||||
@bind_hass
|
||||
def toggle(hass, entity_id):
|
||||
"""Toggle the script.
|
||||
|
||||
This is a legacy helper method. Do not use it for new tests.
|
||||
"""
|
||||
hass.services.call(DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: entity_id})
|
||||
|
||||
|
||||
@bind_hass
|
||||
def reload(hass):
|
||||
"""Reload script component.
|
||||
|
||||
This is a legacy helper method. Do not use it for new tests.
|
||||
"""
|
||||
hass.services.call(DOMAIN, SERVICE_RELOAD)
|
||||
|
||||
|
||||
class TestScriptComponent(unittest.TestCase):
|
||||
"""Test the Script component."""
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
def setUp(self):
|
||||
"""Set up things to be run when tests are started."""
|
||||
self.hass = get_test_home_assistant()
|
||||
|
||||
self.addCleanup(self.tear_down_cleanup)
|
||||
|
||||
def tear_down_cleanup(self):
|
||||
"""Stop down everything that was started."""
|
||||
self.hass.stop()
|
||||
|
||||
def test_passing_variables(self):
|
||||
"""Test different ways of passing in variables."""
|
||||
mock_restore_cache(self.hass, ())
|
||||
calls = []
|
||||
context = Context()
|
||||
|
||||
@callback
|
||||
def record_call(service):
|
||||
"""Add recorded event to set."""
|
||||
calls.append(service)
|
||||
|
||||
self.hass.services.register("test", "script", record_call)
|
||||
|
||||
assert setup_component(
|
||||
self.hass,
|
||||
"script",
|
||||
{
|
||||
"script": {
|
||||
"test": {
|
||||
"sequence": {
|
||||
"service": "test.script",
|
||||
"data_template": {"hello": "{{ greeting }}"},
|
||||
}
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
"script",
|
||||
{
|
||||
"script": {
|
||||
"test": {
|
||||
"sequence": {
|
||||
"service": "test.script",
|
||||
"data_template": {"hello": "{{ greeting }}"},
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
turn_on(self.hass, ENTITY_ID, {"greeting": "world"}, context=context)
|
||||
await hass.services.async_call(
|
||||
DOMAIN, "test", {"greeting": "world"}, context=context
|
||||
)
|
||||
|
||||
self.hass.block_till_done()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(calls) == 1
|
||||
assert calls[0].context is context
|
||||
assert calls[0].data["hello"] == "world"
|
||||
assert len(calls) == 1
|
||||
assert calls[0].context is context
|
||||
assert calls[0].data["hello"] == "world"
|
||||
|
||||
self.hass.services.call(
|
||||
"script", "test", {"greeting": "universe"}, context=context
|
||||
)
|
||||
await hass.services.async_call(
|
||||
"script", "test", {"greeting": "universe"}, context=context
|
||||
)
|
||||
|
||||
self.hass.block_till_done()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(calls) == 2
|
||||
assert calls[1].context is context
|
||||
assert calls[1].data["hello"] == "universe"
|
||||
assert len(calls) == 2
|
||||
assert calls[1].context is context
|
||||
assert calls[1].data["hello"] == "universe"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("toggle", [False, True])
|
||||
|
@ -1,14 +1,19 @@
|
||||
"""Test Trace websocket API."""
|
||||
import asyncio
|
||||
import json
|
||||
from typing import DefaultDict
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.bootstrap import async_setup_component
|
||||
from homeassistant.components.trace.const import DEFAULT_STORED_TRACES
|
||||
from homeassistant.core import Context, callback
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import Context, CoreState, callback
|
||||
from homeassistant.helpers.typing import UNDEFINED
|
||||
from homeassistant.util.uuid import random_uuid_hex
|
||||
|
||||
from tests.common import assert_lists_same
|
||||
from tests.common import assert_lists_same, load_fixture
|
||||
|
||||
|
||||
def _find_run_id(traces, trace_type, item_id):
|
||||
@ -70,8 +75,12 @@ def _assert_raw_config(domain, config, trace):
|
||||
assert trace["config"] == config
|
||||
|
||||
|
||||
async def _assert_contexts(client, next_id, contexts):
|
||||
await client.send_json({"id": next_id(), "type": "trace/contexts"})
|
||||
async def _assert_contexts(client, next_id, contexts, domain=None, item_id=None):
|
||||
request = {"id": next_id(), "type": "trace/contexts"}
|
||||
if domain is not None:
|
||||
request["domain"] = domain
|
||||
request["item_id"] = item_id
|
||||
await client.send_json(request)
|
||||
response = await client.receive_json()
|
||||
assert response["success"]
|
||||
assert response["result"] == contexts
|
||||
@ -101,6 +110,7 @@ async def _assert_contexts(client, next_id, contexts):
|
||||
)
|
||||
async def test_get_trace(
|
||||
hass,
|
||||
hass_storage,
|
||||
hass_ws_client,
|
||||
domain,
|
||||
prefix,
|
||||
@ -152,6 +162,8 @@ async def test_get_trace(
|
||||
|
||||
client = await hass_ws_client()
|
||||
contexts = {}
|
||||
contexts_sun = {}
|
||||
contexts_moon = {}
|
||||
|
||||
# Trigger "sun" automation / run "sun" script
|
||||
context = Context()
|
||||
@ -195,6 +207,11 @@ async def test_get_trace(
|
||||
"domain": domain,
|
||||
"item_id": trace["item_id"],
|
||||
}
|
||||
contexts_sun[trace["context"]["id"]] = {
|
||||
"run_id": trace["run_id"],
|
||||
"domain": domain,
|
||||
"item_id": trace["item_id"],
|
||||
}
|
||||
|
||||
# Trigger "moon" automation, with passing condition / run "moon" script
|
||||
await _run_automation_or_script(hass, domain, moon_config, "test_event2", context)
|
||||
@ -244,10 +261,17 @@ async def test_get_trace(
|
||||
"domain": domain,
|
||||
"item_id": trace["item_id"],
|
||||
}
|
||||
contexts_moon[trace["context"]["id"]] = {
|
||||
"run_id": trace["run_id"],
|
||||
"domain": domain,
|
||||
"item_id": trace["item_id"],
|
||||
}
|
||||
|
||||
if len(extra_trace_keys) <= 2:
|
||||
# Check contexts
|
||||
await _assert_contexts(client, next_id, contexts)
|
||||
await _assert_contexts(client, next_id, contexts_moon, domain, "moon")
|
||||
await _assert_contexts(client, next_id, contexts_sun, domain, "sun")
|
||||
return
|
||||
|
||||
# Trigger "moon" automation with failing condition
|
||||
@ -291,6 +315,11 @@ async def test_get_trace(
|
||||
"domain": domain,
|
||||
"item_id": trace["item_id"],
|
||||
}
|
||||
contexts_moon[trace["context"]["id"]] = {
|
||||
"run_id": trace["run_id"],
|
||||
"domain": domain,
|
||||
"item_id": trace["item_id"],
|
||||
}
|
||||
|
||||
# Trigger "moon" automation with passing condition
|
||||
hass.bus.async_fire("test_event2")
|
||||
@ -336,9 +365,119 @@ async def test_get_trace(
|
||||
"domain": domain,
|
||||
"item_id": trace["item_id"],
|
||||
}
|
||||
contexts_moon[trace["context"]["id"]] = {
|
||||
"run_id": trace["run_id"],
|
||||
"domain": domain,
|
||||
"item_id": trace["item_id"],
|
||||
}
|
||||
|
||||
# Check contexts
|
||||
await _assert_contexts(client, next_id, contexts)
|
||||
await _assert_contexts(client, next_id, contexts_moon, domain, "moon")
|
||||
await _assert_contexts(client, next_id, contexts_sun, domain, "sun")
|
||||
|
||||
# List traces
|
||||
await client.send_json({"id": next_id(), "type": "trace/list", "domain": domain})
|
||||
response = await client.receive_json()
|
||||
assert response["success"]
|
||||
trace_list = response["result"]
|
||||
|
||||
# Get all traces and generate expected stored traces
|
||||
traces = DefaultDict(list)
|
||||
for trace in trace_list:
|
||||
item_id = trace["item_id"]
|
||||
run_id = trace["run_id"]
|
||||
await client.send_json(
|
||||
{
|
||||
"id": next_id(),
|
||||
"type": "trace/get",
|
||||
"domain": domain,
|
||||
"item_id": item_id,
|
||||
"run_id": run_id,
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
assert response["success"]
|
||||
traces[f"{domain}.{item_id}"].append(
|
||||
{"short_dict": trace, "extended_dict": response["result"]}
|
||||
)
|
||||
|
||||
# Fake stop
|
||||
assert "trace.saved_traces" not in hass_storage
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Check that saved data is same as the serialized traces
|
||||
assert "trace.saved_traces" in hass_storage
|
||||
assert hass_storage["trace.saved_traces"]["data"] == traces
|
||||
|
||||
|
||||
@pytest.mark.parametrize("domain", ["automation", "script"])
|
||||
async def test_restore_traces(hass, hass_storage, hass_ws_client, domain):
|
||||
"""Test restored traces."""
|
||||
hass.state = CoreState.not_running
|
||||
id = 1
|
||||
|
||||
def next_id():
|
||||
nonlocal id
|
||||
id += 1
|
||||
return id
|
||||
|
||||
saved_traces = json.loads(load_fixture(f"trace/{domain}_saved_traces.json"))
|
||||
hass_storage["trace.saved_traces"] = saved_traces
|
||||
await _setup_automation_or_script(hass, domain, [])
|
||||
await hass.async_start()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
client = await hass_ws_client()
|
||||
|
||||
# List traces
|
||||
await client.send_json({"id": next_id(), "type": "trace/list", "domain": domain})
|
||||
response = await client.receive_json()
|
||||
assert response["success"]
|
||||
trace_list = response["result"]
|
||||
|
||||
# Get all traces and generate expected stored traces
|
||||
traces = DefaultDict(list)
|
||||
contexts = {}
|
||||
for trace in trace_list:
|
||||
item_id = trace["item_id"]
|
||||
run_id = trace["run_id"]
|
||||
await client.send_json(
|
||||
{
|
||||
"id": next_id(),
|
||||
"type": "trace/get",
|
||||
"domain": domain,
|
||||
"item_id": item_id,
|
||||
"run_id": run_id,
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
assert response["success"]
|
||||
traces[f"{domain}.{item_id}"].append(
|
||||
{"short_dict": trace, "extended_dict": response["result"]}
|
||||
)
|
||||
contexts[response["result"]["context"]["id"]] = {
|
||||
"run_id": trace["run_id"],
|
||||
"domain": domain,
|
||||
"item_id": trace["item_id"],
|
||||
}
|
||||
|
||||
# Check that loaded data is same as the serialized traces
|
||||
assert hass_storage["trace.saved_traces"]["data"] == traces
|
||||
|
||||
# Check restored contexts
|
||||
await _assert_contexts(client, next_id, contexts)
|
||||
|
||||
# Fake stop
|
||||
hass_storage.pop("trace.saved_traces")
|
||||
assert "trace.saved_traces" not in hass_storage
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Check that saved data is same as the serialized traces
|
||||
assert "trace.saved_traces" in hass_storage
|
||||
assert hass_storage["trace.saved_traces"] == saved_traces
|
||||
|
||||
|
||||
@pytest.mark.parametrize("domain", ["automation", "script"])
|
||||
@ -368,6 +507,13 @@ async def test_trace_overflow(hass, hass_ws_client, domain, stored_traces):
|
||||
"""Test the number of stored traces per script or automation is limited."""
|
||||
id = 1
|
||||
|
||||
trace_uuids = []
|
||||
|
||||
def mock_random_uuid_hex():
|
||||
nonlocal trace_uuids
|
||||
trace_uuids.append(random_uuid_hex())
|
||||
return trace_uuids[-1]
|
||||
|
||||
def next_id():
|
||||
nonlocal id
|
||||
id += 1
|
||||
@ -404,13 +550,16 @@ async def test_trace_overflow(hass, hass_ws_client, domain, stored_traces):
|
||||
response = await client.receive_json()
|
||||
assert response["success"]
|
||||
assert len(_find_traces(response["result"], domain, "moon")) == 1
|
||||
moon_run_id = _find_run_id(response["result"], domain, "moon")
|
||||
assert len(_find_traces(response["result"], domain, "sun")) == 1
|
||||
|
||||
# Trigger "moon" enough times to overflow the max number of stored traces
|
||||
for _ in range(stored_traces or DEFAULT_STORED_TRACES):
|
||||
await _run_automation_or_script(hass, domain, moon_config, "test_event2")
|
||||
await hass.async_block_till_done()
|
||||
with patch(
|
||||
"homeassistant.components.trace.uuid_util.random_uuid_hex",
|
||||
wraps=mock_random_uuid_hex,
|
||||
):
|
||||
for _ in range(stored_traces or DEFAULT_STORED_TRACES):
|
||||
await _run_automation_or_script(hass, domain, moon_config, "test_event2")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await client.send_json({"id": next_id(), "type": "trace/list", "domain": domain})
|
||||
response = await client.receive_json()
|
||||
@ -418,10 +567,153 @@ async def test_trace_overflow(hass, hass_ws_client, domain, stored_traces):
|
||||
moon_traces = _find_traces(response["result"], domain, "moon")
|
||||
assert len(moon_traces) == stored_traces or DEFAULT_STORED_TRACES
|
||||
assert moon_traces[0]
|
||||
assert int(moon_traces[0]["run_id"]) == int(moon_run_id) + 1
|
||||
assert int(moon_traces[-1]["run_id"]) == int(moon_run_id) + (
|
||||
stored_traces or DEFAULT_STORED_TRACES
|
||||
)
|
||||
assert moon_traces[0]["run_id"] == trace_uuids[0]
|
||||
assert moon_traces[-1]["run_id"] == trace_uuids[-1]
|
||||
assert len(_find_traces(response["result"], domain, "sun")) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"domain,num_restored_moon_traces", [("automation", 3), ("script", 1)]
|
||||
)
|
||||
async def test_restore_traces_overflow(
|
||||
hass, hass_storage, hass_ws_client, domain, num_restored_moon_traces
|
||||
):
|
||||
"""Test restored traces are evicted first."""
|
||||
hass.state = CoreState.not_running
|
||||
id = 1
|
||||
|
||||
trace_uuids = []
|
||||
|
||||
def mock_random_uuid_hex():
|
||||
nonlocal trace_uuids
|
||||
trace_uuids.append(random_uuid_hex())
|
||||
return trace_uuids[-1]
|
||||
|
||||
def next_id():
|
||||
nonlocal id
|
||||
id += 1
|
||||
return id
|
||||
|
||||
saved_traces = json.loads(load_fixture(f"trace/{domain}_saved_traces.json"))
|
||||
hass_storage["trace.saved_traces"] = saved_traces
|
||||
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"},
|
||||
}
|
||||
await _setup_automation_or_script(hass, domain, [sun_config, moon_config])
|
||||
await hass.async_start()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
client = await hass_ws_client()
|
||||
|
||||
# Traces should not yet be restored
|
||||
assert "trace_traces_restored" not in hass.data
|
||||
|
||||
# List traces
|
||||
await client.send_json({"id": next_id(), "type": "trace/list", "domain": domain})
|
||||
response = await client.receive_json()
|
||||
assert response["success"]
|
||||
restored_moon_traces = _find_traces(response["result"], domain, "moon")
|
||||
assert len(restored_moon_traces) == num_restored_moon_traces
|
||||
assert len(_find_traces(response["result"], domain, "sun")) == 1
|
||||
|
||||
# Traces should be restored
|
||||
assert "trace_traces_restored" in hass.data
|
||||
|
||||
# Trigger "moon" enough times to overflow the max number of stored traces
|
||||
with patch(
|
||||
"homeassistant.components.trace.uuid_util.random_uuid_hex",
|
||||
wraps=mock_random_uuid_hex,
|
||||
):
|
||||
for _ in range(DEFAULT_STORED_TRACES - num_restored_moon_traces + 1):
|
||||
await _run_automation_or_script(hass, domain, moon_config, "test_event2")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await client.send_json({"id": next_id(), "type": "trace/list", "domain": domain})
|
||||
response = await client.receive_json()
|
||||
assert response["success"]
|
||||
moon_traces = _find_traces(response["result"], domain, "moon")
|
||||
assert len(moon_traces) == DEFAULT_STORED_TRACES
|
||||
if num_restored_moon_traces > 1:
|
||||
assert moon_traces[0]["run_id"] == restored_moon_traces[1]["run_id"]
|
||||
assert moon_traces[num_restored_moon_traces - 1]["run_id"] == trace_uuids[0]
|
||||
assert moon_traces[-1]["run_id"] == trace_uuids[-1]
|
||||
assert len(_find_traces(response["result"], domain, "sun")) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"domain,num_restored_moon_traces,restored_run_id",
|
||||
[("automation", 3, "e2c97432afe9b8a42d7983588ed5e6ef"), ("script", 1, "")],
|
||||
)
|
||||
async def test_restore_traces_late_overflow(
|
||||
hass,
|
||||
hass_storage,
|
||||
hass_ws_client,
|
||||
domain,
|
||||
num_restored_moon_traces,
|
||||
restored_run_id,
|
||||
):
|
||||
"""Test restored traces are evicted first."""
|
||||
hass.state = CoreState.not_running
|
||||
id = 1
|
||||
|
||||
trace_uuids = []
|
||||
|
||||
def mock_random_uuid_hex():
|
||||
nonlocal trace_uuids
|
||||
trace_uuids.append(random_uuid_hex())
|
||||
return trace_uuids[-1]
|
||||
|
||||
def next_id():
|
||||
nonlocal id
|
||||
id += 1
|
||||
return id
|
||||
|
||||
saved_traces = json.loads(load_fixture(f"trace/{domain}_saved_traces.json"))
|
||||
hass_storage["trace.saved_traces"] = saved_traces
|
||||
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"},
|
||||
}
|
||||
await _setup_automation_or_script(hass, domain, [sun_config, moon_config])
|
||||
await hass.async_start()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
client = await hass_ws_client()
|
||||
|
||||
# Traces should not yet be restored
|
||||
assert "trace_traces_restored" not in hass.data
|
||||
|
||||
# Trigger "moon" enough times to overflow the max number of stored traces
|
||||
with patch(
|
||||
"homeassistant.components.trace.uuid_util.random_uuid_hex",
|
||||
wraps=mock_random_uuid_hex,
|
||||
):
|
||||
for _ in range(DEFAULT_STORED_TRACES - num_restored_moon_traces + 1):
|
||||
await _run_automation_or_script(hass, domain, moon_config, "test_event2")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await client.send_json({"id": next_id(), "type": "trace/list", "domain": domain})
|
||||
response = await client.receive_json()
|
||||
assert response["success"]
|
||||
moon_traces = _find_traces(response["result"], domain, "moon")
|
||||
assert len(moon_traces) == DEFAULT_STORED_TRACES
|
||||
if num_restored_moon_traces > 1:
|
||||
assert moon_traces[0]["run_id"] == restored_run_id
|
||||
assert moon_traces[num_restored_moon_traces - 1]["run_id"] == trace_uuids[0]
|
||||
assert moon_traces[-1]["run_id"] == trace_uuids[-1]
|
||||
assert len(_find_traces(response["result"], domain, "sun")) == 1
|
||||
|
||||
|
||||
|
486
tests/fixtures/trace/automation_saved_traces.json
vendored
Normal file
486
tests/fixtures/trace/automation_saved_traces.json
vendored
Normal file
@ -0,0 +1,486 @@
|
||||
{
|
||||
"version": 1,
|
||||
"key": "trace.saved_traces",
|
||||
"data": {
|
||||
"automation.sun": [
|
||||
{
|
||||
"extended_dict": {
|
||||
"last_step": "action/0",
|
||||
"run_id": "d09f46a4007732c53fa69f434acc1c02",
|
||||
"state": "stopped",
|
||||
"script_execution": "error",
|
||||
"timestamp": {
|
||||
"start": "2021-10-14T06:43:39.540977+00:00",
|
||||
"finish": "2021-10-14T06:43:39.542744+00:00"
|
||||
},
|
||||
"domain": "automation",
|
||||
"item_id": "sun",
|
||||
"error": "Unable to find service test.automation",
|
||||
"trigger": "event 'test_event'",
|
||||
"trace": {
|
||||
"trigger/0": [
|
||||
{
|
||||
"path": "trigger/0",
|
||||
"timestamp": "2021-10-14T06:43:39.541024+00:00",
|
||||
"changed_variables": {
|
||||
"this": {
|
||||
"entity_id": "automation.automation_0",
|
||||
"state": "on",
|
||||
"attributes": {
|
||||
"last_triggered": null,
|
||||
"mode": "single",
|
||||
"current": 0,
|
||||
"id": "sun",
|
||||
"friendly_name": "automation 0"
|
||||
},
|
||||
"last_changed": "2021-10-14T06:43:39.368423+00:00",
|
||||
"last_updated": "2021-10-14T06:43:39.368423+00:00",
|
||||
"context": {
|
||||
"id": "c62f6b3f975b4f9bd479b10a4d7425db",
|
||||
"parent_id": null,
|
||||
"user_id": null
|
||||
}
|
||||
},
|
||||
"trigger": {
|
||||
"id": "0",
|
||||
"idx": "0",
|
||||
"platform": "event",
|
||||
"event": {
|
||||
"event_type": "test_event",
|
||||
"data": {},
|
||||
"origin": "LOCAL",
|
||||
"time_fired": "2021-10-14T06:43:39.540382+00:00",
|
||||
"context": {
|
||||
"id": "66934a357e691e845d7f00ee953c0f0f",
|
||||
"parent_id": null,
|
||||
"user_id": null
|
||||
}
|
||||
},
|
||||
"description": "event 'test_event'"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"action/0": [
|
||||
{
|
||||
"path": "action/0",
|
||||
"timestamp": "2021-10-14T06:43:39.541738+00:00",
|
||||
"changed_variables": {
|
||||
"context": {
|
||||
"id": "4438e85e335bd05e6474d2846d7001cc",
|
||||
"parent_id": "66934a357e691e845d7f00ee953c0f0f",
|
||||
"user_id": null
|
||||
}
|
||||
},
|
||||
"error": "Unable to find service test.automation",
|
||||
"result": {
|
||||
"params": {
|
||||
"domain": "test",
|
||||
"service": "automation",
|
||||
"service_data": {},
|
||||
"target": {}
|
||||
},
|
||||
"running_script": false,
|
||||
"limit": 10
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
"id": "sun",
|
||||
"trigger": {
|
||||
"platform": "event",
|
||||
"event_type": "test_event"
|
||||
},
|
||||
"action": {
|
||||
"service": "test.automation"
|
||||
}
|
||||
},
|
||||
"blueprint_inputs": null,
|
||||
"context": {
|
||||
"id": "4438e85e335bd05e6474d2846d7001cc",
|
||||
"parent_id": "66934a357e691e845d7f00ee953c0f0f",
|
||||
"user_id": null
|
||||
}
|
||||
},
|
||||
"short_dict": {
|
||||
"last_step": "action/0",
|
||||
"run_id": "d09f46a4007732c53fa69f434acc1c02",
|
||||
"state": "stopped",
|
||||
"script_execution": "error",
|
||||
"timestamp": {
|
||||
"start": "2021-10-14T06:43:39.540977+00:00",
|
||||
"finish": "2021-10-14T06:43:39.542744+00:00"
|
||||
},
|
||||
"domain": "automation",
|
||||
"item_id": "sun",
|
||||
"error": "Unable to find service test.automation",
|
||||
"trigger": "event 'test_event'"
|
||||
}
|
||||
}
|
||||
],
|
||||
"automation.moon": [
|
||||
{
|
||||
"extended_dict": {
|
||||
"last_step": "action/0",
|
||||
"run_id": "511d210ac62aa04668ab418063b57e2c",
|
||||
"state": "stopped",
|
||||
"script_execution": "finished",
|
||||
"timestamp": {
|
||||
"start": "2021-10-14T06:43:39.545290+00:00",
|
||||
"finish": "2021-10-14T06:43:39.546962+00:00"
|
||||
},
|
||||
"domain": "automation",
|
||||
"item_id": "moon",
|
||||
"trigger": "event 'test_event2'",
|
||||
"trace": {
|
||||
"trigger/0": [
|
||||
{
|
||||
"path": "trigger/0",
|
||||
"timestamp": "2021-10-14T06:43:39.545313+00:00",
|
||||
"changed_variables": {
|
||||
"this": {
|
||||
"entity_id": "automation.automation_1",
|
||||
"state": "on",
|
||||
"attributes": {
|
||||
"last_triggered": null,
|
||||
"mode": "single",
|
||||
"current": 0,
|
||||
"id": "moon",
|
||||
"friendly_name": "automation 1"
|
||||
},
|
||||
"last_changed": "2021-10-14T06:43:39.369282+00:00",
|
||||
"last_updated": "2021-10-14T06:43:39.369282+00:00",
|
||||
"context": {
|
||||
"id": "c914e818f5b234c0fc0dfddf75e98b0e",
|
||||
"parent_id": null,
|
||||
"user_id": null
|
||||
}
|
||||
},
|
||||
"trigger": {
|
||||
"id": "0",
|
||||
"idx": "0",
|
||||
"platform": "event",
|
||||
"event": {
|
||||
"event_type": "test_event2",
|
||||
"data": {},
|
||||
"origin": "LOCAL",
|
||||
"time_fired": "2021-10-14T06:43:39.545003+00:00",
|
||||
"context": {
|
||||
"id": "66934a357e691e845d7f00ee953c0f0f",
|
||||
"parent_id": null,
|
||||
"user_id": null
|
||||
}
|
||||
},
|
||||
"description": "event 'test_event2'"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"condition/0": [
|
||||
{
|
||||
"path": "condition/0",
|
||||
"timestamp": "2021-10-14T06:43:39.545336+00:00",
|
||||
"result": {
|
||||
"result": true,
|
||||
"entities": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"action/0": [
|
||||
{
|
||||
"path": "action/0",
|
||||
"timestamp": "2021-10-14T06:43:39.546378+00:00",
|
||||
"changed_variables": {
|
||||
"context": {
|
||||
"id": "8948898e0074ecaa98be2e041256c81b",
|
||||
"parent_id": "66934a357e691e845d7f00ee953c0f0f",
|
||||
"user_id": null
|
||||
}
|
||||
},
|
||||
"result": {
|
||||
"event": "another_event",
|
||||
"event_data": {}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"blueprint_inputs": null,
|
||||
"context": {
|
||||
"id": "8948898e0074ecaa98be2e041256c81b",
|
||||
"parent_id": "66934a357e691e845d7f00ee953c0f0f",
|
||||
"user_id": null
|
||||
}
|
||||
},
|
||||
"short_dict": {
|
||||
"last_step": "action/0",
|
||||
"run_id": "511d210ac62aa04668ab418063b57e2c",
|
||||
"state": "stopped",
|
||||
"script_execution": "finished",
|
||||
"timestamp": {
|
||||
"start": "2021-10-14T06:43:39.545290+00:00",
|
||||
"finish": "2021-10-14T06:43:39.546962+00:00"
|
||||
},
|
||||
"domain": "automation",
|
||||
"item_id": "moon",
|
||||
"trigger": "event 'test_event2'"
|
||||
}
|
||||
},
|
||||
{
|
||||
"extended_dict": {
|
||||
"last_step": "condition/0",
|
||||
"run_id": "e2c97432afe9b8a42d7983588ed5e6ef",
|
||||
"state": "stopped",
|
||||
"script_execution": "failed_conditions",
|
||||
"timestamp": {
|
||||
"start": "2021-10-14T06:43:39.549081+00:00",
|
||||
"finish": "2021-10-14T06:43:39.549468+00:00"
|
||||
},
|
||||
"domain": "automation",
|
||||
"item_id": "moon",
|
||||
"trigger": "event 'test_event3'",
|
||||
"trace": {
|
||||
"trigger/1": [
|
||||
{
|
||||
"path": "trigger/1",
|
||||
"timestamp": "2021-10-14T06:43:39.549115+00:00",
|
||||
"changed_variables": {
|
||||
"this": {
|
||||
"entity_id": "automation.automation_1",
|
||||
"state": "on",
|
||||
"attributes": {
|
||||
"last_triggered": "2021-10-14T06:43:39.545943+00:00",
|
||||
"mode": "single",
|
||||
"current": 0,
|
||||
"id": "moon",
|
||||
"friendly_name": "automation 1"
|
||||
},
|
||||
"last_changed": "2021-10-14T06:43:39.369282+00:00",
|
||||
"last_updated": "2021-10-14T06:43:39.546662+00:00",
|
||||
"context": {
|
||||
"id": "8948898e0074ecaa98be2e041256c81b",
|
||||
"parent_id": "66934a357e691e845d7f00ee953c0f0f",
|
||||
"user_id": null
|
||||
}
|
||||
},
|
||||
"trigger": {
|
||||
"id": "1",
|
||||
"idx": "1",
|
||||
"platform": "event",
|
||||
"event": {
|
||||
"event_type": "test_event3",
|
||||
"data": {},
|
||||
"origin": "LOCAL",
|
||||
"time_fired": "2021-10-14T06:43:39.548788+00:00",
|
||||
"context": {
|
||||
"id": "5f5113a378b3c06fe146ead2908f6f44",
|
||||
"parent_id": null,
|
||||
"user_id": null
|
||||
}
|
||||
},
|
||||
"description": "event 'test_event3'"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"condition/0": [
|
||||
{
|
||||
"path": "condition/0",
|
||||
"timestamp": "2021-10-14T06:43:39.549136+00:00",
|
||||
"result": {
|
||||
"result": false,
|
||||
"entities": []
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"blueprint_inputs": null,
|
||||
"context": {
|
||||
"id": "77d041c4e0ecc91ab5e707239c983faf",
|
||||
"parent_id": "5f5113a378b3c06fe146ead2908f6f44",
|
||||
"user_id": null
|
||||
}
|
||||
},
|
||||
"short_dict": {
|
||||
"last_step": "condition/0",
|
||||
"run_id": "e2c97432afe9b8a42d7983588ed5e6ef",
|
||||
"state": "stopped",
|
||||
"script_execution": "failed_conditions",
|
||||
"timestamp": {
|
||||
"start": "2021-10-14T06:43:39.549081+00:00",
|
||||
"finish": "2021-10-14T06:43:39.549468+00:00"
|
||||
},
|
||||
"domain": "automation",
|
||||
"item_id": "moon",
|
||||
"trigger": "event 'test_event3'"
|
||||
}
|
||||
},
|
||||
{
|
||||
"extended_dict": {
|
||||
"last_step": "action/0",
|
||||
"run_id": "f71d7fa261d361ed999c1dda0a846c99",
|
||||
"state": "stopped",
|
||||
"script_execution": "finished",
|
||||
"timestamp": {
|
||||
"start": "2021-10-14T06:43:39.551485+00:00",
|
||||
"finish": "2021-10-14T06:43:39.552822+00:00"
|
||||
},
|
||||
"domain": "automation",
|
||||
"item_id": "moon",
|
||||
"trigger": "event 'test_event2'",
|
||||
"trace": {
|
||||
"trigger/0": [
|
||||
{
|
||||
"path": "trigger/0",
|
||||
"timestamp": "2021-10-14T06:43:39.551503+00:00",
|
||||
"changed_variables": {
|
||||
"this": {
|
||||
"entity_id": "automation.automation_1",
|
||||
"state": "on",
|
||||
"attributes": {
|
||||
"last_triggered": "2021-10-14T06:43:39.545943+00:00",
|
||||
"mode": "single",
|
||||
"current": 0,
|
||||
"id": "moon",
|
||||
"friendly_name": "automation 1"
|
||||
},
|
||||
"last_changed": "2021-10-14T06:43:39.369282+00:00",
|
||||
"last_updated": "2021-10-14T06:43:39.546662+00:00",
|
||||
"context": {
|
||||
"id": "8948898e0074ecaa98be2e041256c81b",
|
||||
"parent_id": "66934a357e691e845d7f00ee953c0f0f",
|
||||
"user_id": null
|
||||
}
|
||||
},
|
||||
"trigger": {
|
||||
"id": "0",
|
||||
"idx": "0",
|
||||
"platform": "event",
|
||||
"event": {
|
||||
"event_type": "test_event2",
|
||||
"data": {},
|
||||
"origin": "LOCAL",
|
||||
"time_fired": "2021-10-14T06:43:39.551202+00:00",
|
||||
"context": {
|
||||
"id": "66a59f97502785c544724fdb46bcb94d",
|
||||
"parent_id": null,
|
||||
"user_id": null
|
||||
}
|
||||
},
|
||||
"description": "event 'test_event2'"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"condition/0": [
|
||||
{
|
||||
"path": "condition/0",
|
||||
"timestamp": "2021-10-14T06:43:39.551524+00:00",
|
||||
"result": {
|
||||
"result": true,
|
||||
"entities": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"action/0": [
|
||||
{
|
||||
"path": "action/0",
|
||||
"timestamp": "2021-10-14T06:43:39.552236+00:00",
|
||||
"changed_variables": {
|
||||
"context": {
|
||||
"id": "3128b5fa3494cb17cfb485176ef2cee3",
|
||||
"parent_id": "66a59f97502785c544724fdb46bcb94d",
|
||||
"user_id": null
|
||||
}
|
||||
},
|
||||
"result": {
|
||||
"event": "another_event",
|
||||
"event_data": {}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"blueprint_inputs": null,
|
||||
"context": {
|
||||
"id": "3128b5fa3494cb17cfb485176ef2cee3",
|
||||
"parent_id": "66a59f97502785c544724fdb46bcb94d",
|
||||
"user_id": null
|
||||
}
|
||||
},
|
||||
"short_dict": {
|
||||
"last_step": "action/0",
|
||||
"run_id": "f71d7fa261d361ed999c1dda0a846c99",
|
||||
"state": "stopped",
|
||||
"script_execution": "finished",
|
||||
"timestamp": {
|
||||
"start": "2021-10-14T06:43:39.551485+00:00",
|
||||
"finish": "2021-10-14T06:43:39.552822+00:00"
|
||||
},
|
||||
"domain": "automation",
|
||||
"item_id": "moon",
|
||||
"trigger": "event 'test_event2'"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
165
tests/fixtures/trace/script_saved_traces.json
vendored
Normal file
165
tests/fixtures/trace/script_saved_traces.json
vendored
Normal file
@ -0,0 +1,165 @@
|
||||
{
|
||||
"version": 1,
|
||||
"key": "trace.saved_traces",
|
||||
"data": {
|
||||
"script.sun": [
|
||||
{
|
||||
"extended_dict": {
|
||||
"last_step": "sequence/0",
|
||||
"run_id": "6bd24c3b715333fd2192c9501b77664a",
|
||||
"state": "stopped",
|
||||
"script_execution": "error",
|
||||
"timestamp": {
|
||||
"start": "2021-10-14T06:48:18.037973+00:00",
|
||||
"finish": "2021-10-14T06:48:18.039367+00:00"
|
||||
},
|
||||
"domain": "script",
|
||||
"item_id": "sun",
|
||||
"error": "Unable to find service test.automation",
|
||||
"trace": {
|
||||
"sequence/0": [
|
||||
{
|
||||
"path": "sequence/0",
|
||||
"timestamp": "2021-10-14T06:48:18.038692+00:00",
|
||||
"changed_variables": {
|
||||
"this": {
|
||||
"entity_id": "script.sun",
|
||||
"state": "off",
|
||||
"attributes": {
|
||||
"last_triggered": null,
|
||||
"mode": "single",
|
||||
"current": 0,
|
||||
"friendly_name": "sun"
|
||||
},
|
||||
"last_changed": "2021-10-14T06:48:18.023069+00:00",
|
||||
"last_updated": "2021-10-14T06:48:18.023069+00:00",
|
||||
"context": {
|
||||
"id": "0c28537a7a55a0c43360fda5c86fb63a",
|
||||
"parent_id": null,
|
||||
"user_id": null
|
||||
}
|
||||
},
|
||||
"context": {
|
||||
"id": "436e5cbeb27415fae813d302e2acb168",
|
||||
"parent_id": null,
|
||||
"user_id": null
|
||||
}
|
||||
},
|
||||
"error": "Unable to find service test.automation",
|
||||
"result": {
|
||||
"params": {
|
||||
"domain": "test",
|
||||
"service": "automation",
|
||||
"service_data": {},
|
||||
"target": {}
|
||||
},
|
||||
"running_script": false,
|
||||
"limit": 10
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
"sequence": {
|
||||
"service": "test.automation"
|
||||
}
|
||||
},
|
||||
"blueprint_inputs": null,
|
||||
"context": {
|
||||
"id": "436e5cbeb27415fae813d302e2acb168",
|
||||
"parent_id": null,
|
||||
"user_id": null
|
||||
}
|
||||
},
|
||||
"short_dict": {
|
||||
"last_step": "sequence/0",
|
||||
"run_id": "6bd24c3b715333fd2192c9501b77664a",
|
||||
"state": "stopped",
|
||||
"script_execution": "error",
|
||||
"timestamp": {
|
||||
"start": "2021-10-14T06:48:18.037973+00:00",
|
||||
"finish": "2021-10-14T06:48:18.039367+00:00"
|
||||
},
|
||||
"domain": "script",
|
||||
"item_id": "sun",
|
||||
"error": "Unable to find service test.automation"
|
||||
}
|
||||
}
|
||||
],
|
||||
"script.moon": [
|
||||
{
|
||||
"extended_dict": {
|
||||
"last_step": "sequence/0",
|
||||
"run_id": "76912f5a7f5e7be2300f92523fd3edf7",
|
||||
"state": "stopped",
|
||||
"script_execution": "finished",
|
||||
"timestamp": {
|
||||
"start": "2021-10-14T06:48:18.045937+00:00",
|
||||
"finish": "2021-10-14T06:48:18.047293+00:00"
|
||||
},
|
||||
"domain": "script",
|
||||
"item_id": "moon",
|
||||
"trace": {
|
||||
"sequence/0": [
|
||||
{
|
||||
"path": "sequence/0",
|
||||
"timestamp": "2021-10-14T06:48:18.046659+00:00",
|
||||
"changed_variables": {
|
||||
"this": {
|
||||
"entity_id": "script.moon",
|
||||
"state": "off",
|
||||
"attributes": {
|
||||
"last_triggered": null,
|
||||
"mode": "single",
|
||||
"current": 0,
|
||||
"friendly_name": "moon"
|
||||
},
|
||||
"last_changed": "2021-10-14T06:48:18.023671+00:00",
|
||||
"last_updated": "2021-10-14T06:48:18.023671+00:00",
|
||||
"context": {
|
||||
"id": "3dcdb3daa596e44bfd10b407f3078ec0",
|
||||
"parent_id": null,
|
||||
"user_id": null
|
||||
}
|
||||
},
|
||||
"context": {
|
||||
"id": "436e5cbeb27415fae813d302e2acb168",
|
||||
"parent_id": null,
|
||||
"user_id": null
|
||||
}
|
||||
},
|
||||
"result": {
|
||||
"event": "another_event",
|
||||
"event_data": {}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
"sequence": {
|
||||
"event": "another_event"
|
||||
}
|
||||
},
|
||||
"blueprint_inputs": null,
|
||||
"context": {
|
||||
"id": "436e5cbeb27415fae813d302e2acb168",
|
||||
"parent_id": null,
|
||||
"user_id": null
|
||||
}
|
||||
},
|
||||
"short_dict": {
|
||||
"last_step": "sequence/0",
|
||||
"run_id": "76912f5a7f5e7be2300f92523fd3edf7",
|
||||
"state": "stopped",
|
||||
"script_execution": "finished",
|
||||
"timestamp": {
|
||||
"start": "2021-10-14T06:48:18.045937+00:00",
|
||||
"finish": "2021-10-14T06:48:18.047293+00:00"
|
||||
},
|
||||
"domain": "script",
|
||||
"item_id": "moon"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user