2021-03-04 13:16:24 +00:00
|
|
|
"""Helpers for script and condition tracing."""
|
2021-03-17 17:34:19 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2021-03-04 13:16:24 +00:00
|
|
|
from collections import deque
|
2021-04-20 15:40:41 +00:00
|
|
|
from collections.abc import Generator
|
2021-03-06 11:57:21 +00:00
|
|
|
from contextlib import contextmanager
|
2021-03-04 13:16:24 +00:00
|
|
|
from contextvars import ContextVar
|
2021-03-18 20:14:06 +00:00
|
|
|
from functools import wraps
|
2021-04-20 15:40:41 +00:00
|
|
|
from typing import Any, Callable, cast
|
2021-03-04 13:16:24 +00:00
|
|
|
|
|
|
|
from homeassistant.helpers.typing import TemplateVarsType
|
|
|
|
import homeassistant.util.dt as dt_util
|
|
|
|
|
|
|
|
|
|
|
|
class TraceElement:
|
|
|
|
"""Container for trace data."""
|
|
|
|
|
2021-05-20 15:53:29 +00:00
|
|
|
def __init__(self, variables: TemplateVarsType, path: str) -> None:
|
2021-03-04 13:16:24 +00:00
|
|
|
"""Container for trace data."""
|
2021-03-26 17:14:01 +00:00
|
|
|
self._child_key: tuple[str, str] | None = None
|
|
|
|
self._child_run_id: str | None = None
|
2021-03-17 17:34:19 +00:00
|
|
|
self._error: Exception | None = None
|
2021-03-15 23:51:04 +00:00
|
|
|
self.path: str = path
|
2021-03-17 17:34:19 +00:00
|
|
|
self._result: dict | None = None
|
2021-05-17 23:54:17 +00:00
|
|
|
self.reuse_by_child = False
|
2021-03-04 13:16:24 +00:00
|
|
|
self._timestamp = dt_util.utcnow()
|
2021-03-08 21:48:36 +00:00
|
|
|
|
|
|
|
if variables is None:
|
|
|
|
variables = {}
|
|
|
|
last_variables = variables_cv.get() or {}
|
|
|
|
variables_cv.set(dict(variables))
|
|
|
|
changed_variables = {
|
|
|
|
key: value
|
|
|
|
for key, value in variables.items()
|
|
|
|
if key not in last_variables or last_variables[key] != value
|
|
|
|
}
|
|
|
|
self._variables = changed_variables
|
2021-03-04 13:16:24 +00:00
|
|
|
|
|
|
|
def __repr__(self) -> str:
|
|
|
|
"""Container for trace data."""
|
|
|
|
return str(self.as_dict())
|
|
|
|
|
2021-03-26 17:14:01 +00:00
|
|
|
def set_child_id(self, child_key: tuple[str, 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
|
|
|
|
|
2021-03-04 13:16:24 +00:00
|
|
|
def set_error(self, ex: Exception) -> None:
|
|
|
|
"""Set error."""
|
|
|
|
self._error = ex
|
|
|
|
|
|
|
|
def set_result(self, **kwargs: Any) -> None:
|
|
|
|
"""Set result."""
|
|
|
|
self._result = {**kwargs}
|
|
|
|
|
2021-04-12 18:22:28 +00:00
|
|
|
def update_result(self, **kwargs: Any) -> None:
|
|
|
|
"""Set result."""
|
|
|
|
old_result = self._result or {}
|
|
|
|
self._result = {**old_result, **kwargs}
|
|
|
|
|
2021-03-17 17:34:19 +00:00
|
|
|
def as_dict(self) -> dict[str, Any]:
|
2021-03-04 13:16:24 +00:00
|
|
|
"""Return dictionary version of this TraceElement."""
|
2021-03-17 17:34:19 +00:00
|
|
|
result: dict[str, Any] = {"path": self.path, "timestamp": self._timestamp}
|
2021-03-26 17:14:01 +00:00
|
|
|
if self._child_key is not None:
|
|
|
|
result["child_id"] = {
|
|
|
|
"domain": self._child_key[0],
|
|
|
|
"item_id": self._child_key[1],
|
|
|
|
"run_id": str(self._child_run_id),
|
|
|
|
}
|
2021-03-08 21:48:36 +00:00
|
|
|
if self._variables:
|
|
|
|
result["changed_variables"] = self._variables
|
2021-03-04 13:16:24 +00:00
|
|
|
if self._error is not None:
|
|
|
|
result["error"] = str(self._error)
|
|
|
|
if self._result is not None:
|
|
|
|
result["result"] = self._result
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
2021-03-06 11:57:21 +00:00
|
|
|
# Context variables for tracing
|
|
|
|
# Current trace
|
2021-04-20 15:40:41 +00:00
|
|
|
trace_cv: ContextVar[dict[str, deque[TraceElement]] | None] = ContextVar(
|
2021-03-06 11:57:21 +00:00
|
|
|
"trace_cv", default=None
|
|
|
|
)
|
|
|
|
# Stack of TraceElements
|
2021-03-17 17:34:19 +00:00
|
|
|
trace_stack_cv: ContextVar[list[TraceElement] | None] = ContextVar(
|
2021-03-06 11:57:21 +00:00
|
|
|
"trace_stack_cv", default=None
|
|
|
|
)
|
|
|
|
# Current location in config tree
|
2021-03-17 17:34:19 +00:00
|
|
|
trace_path_stack_cv: ContextVar[list[str] | None] = ContextVar(
|
2021-03-06 11:57:21 +00:00
|
|
|
"trace_path_stack_cv", default=None
|
|
|
|
)
|
2021-03-08 21:48:36 +00:00
|
|
|
# Copy of last variables
|
2021-03-17 17:34:19 +00:00
|
|
|
variables_cv: ContextVar[Any | None] = ContextVar("variables_cv", default=None)
|
2021-03-24 16:56:22 +00:00
|
|
|
# (domain, item_id) + Run ID
|
2021-07-03 15:52:31 +00:00
|
|
|
trace_id_cv: ContextVar[tuple[tuple[str, str], str] | None] = ContextVar(
|
2021-03-10 05:23:11 +00:00
|
|
|
"trace_id_cv", default=None
|
|
|
|
)
|
2021-04-01 16:42:23 +00:00
|
|
|
# Reason for stopped script execution
|
|
|
|
script_execution_cv: ContextVar[StopReason | None] = ContextVar(
|
|
|
|
"script_execution_cv", default=None
|
|
|
|
)
|
2021-03-10 05:23:11 +00:00
|
|
|
|
|
|
|
|
2021-07-03 15:52:31 +00:00
|
|
|
def trace_id_set(trace_id: tuple[tuple[str, str], str]) -> None:
|
2021-03-10 05:23:11 +00:00
|
|
|
"""Set id of the current trace."""
|
|
|
|
trace_id_cv.set(trace_id)
|
|
|
|
|
|
|
|
|
2021-07-03 15:52:31 +00:00
|
|
|
def trace_id_get() -> tuple[tuple[str, str], str] | None:
|
2021-03-10 05:23:11 +00:00
|
|
|
"""Get id if the current trace."""
|
|
|
|
return trace_id_cv.get()
|
2021-03-06 11:57:21 +00:00
|
|
|
|
|
|
|
|
|
|
|
def trace_stack_push(trace_stack_var: ContextVar, node: Any) -> None:
|
|
|
|
"""Push an element to the top of a trace stack."""
|
|
|
|
trace_stack = trace_stack_var.get()
|
|
|
|
if trace_stack is None:
|
|
|
|
trace_stack = []
|
|
|
|
trace_stack_var.set(trace_stack)
|
|
|
|
trace_stack.append(node)
|
|
|
|
|
|
|
|
|
|
|
|
def trace_stack_pop(trace_stack_var: ContextVar) -> None:
|
|
|
|
"""Remove the top element from a trace stack."""
|
|
|
|
trace_stack = trace_stack_var.get()
|
|
|
|
trace_stack.pop()
|
|
|
|
|
|
|
|
|
2021-03-17 17:34:19 +00:00
|
|
|
def trace_stack_top(trace_stack_var: ContextVar) -> Any | None:
|
2021-03-06 11:57:21 +00:00
|
|
|
"""Return the element at the top of a trace stack."""
|
|
|
|
trace_stack = trace_stack_var.get()
|
|
|
|
return trace_stack[-1] if trace_stack else None
|
|
|
|
|
|
|
|
|
2021-03-17 17:34:19 +00:00
|
|
|
def trace_path_push(suffix: str | list[str]) -> int:
|
2021-03-06 11:57:21 +00:00
|
|
|
"""Go deeper in the config tree."""
|
|
|
|
if isinstance(suffix, str):
|
|
|
|
suffix = [suffix]
|
|
|
|
for node in suffix:
|
|
|
|
trace_stack_push(trace_path_stack_cv, node)
|
|
|
|
return len(suffix)
|
|
|
|
|
|
|
|
|
|
|
|
def trace_path_pop(count: int) -> None:
|
|
|
|
"""Go n levels up in the config tree."""
|
|
|
|
for _ in range(count):
|
|
|
|
trace_stack_pop(trace_path_stack_cv)
|
|
|
|
|
|
|
|
|
|
|
|
def trace_path_get() -> str:
|
|
|
|
"""Return a string representing the current location in the config tree."""
|
|
|
|
path = trace_path_stack_cv.get()
|
|
|
|
if not path:
|
|
|
|
return ""
|
|
|
|
return "/".join(path)
|
|
|
|
|
|
|
|
|
2021-03-04 13:16:24 +00:00
|
|
|
def trace_append_element(
|
|
|
|
trace_element: TraceElement,
|
2021-03-17 17:34:19 +00:00
|
|
|
maxlen: int | None = None,
|
2021-03-04 13:16:24 +00:00
|
|
|
) -> None:
|
|
|
|
"""Append a TraceElement to trace[path]."""
|
2021-03-15 23:51:04 +00:00
|
|
|
path = trace_element.path
|
2021-03-06 11:57:21 +00:00
|
|
|
trace = trace_cv.get()
|
2021-03-04 13:16:24 +00:00
|
|
|
if trace is None:
|
2021-03-06 11:57:21 +00:00
|
|
|
trace = {}
|
|
|
|
trace_cv.set(trace)
|
2021-03-04 13:16:24 +00:00
|
|
|
if path not in trace:
|
|
|
|
trace[path] = deque(maxlen=maxlen)
|
|
|
|
trace[path].append(trace_element)
|
2021-03-06 11:57:21 +00:00
|
|
|
|
|
|
|
|
2021-04-20 15:40:41 +00:00
|
|
|
def trace_get(clear: bool = True) -> dict[str, deque[TraceElement]] | None:
|
2021-03-06 11:57:21 +00:00
|
|
|
"""Return the current trace."""
|
|
|
|
if clear:
|
|
|
|
trace_clear()
|
|
|
|
return trace_cv.get()
|
|
|
|
|
|
|
|
|
|
|
|
def trace_clear() -> None:
|
|
|
|
"""Clear the trace."""
|
|
|
|
trace_cv.set({})
|
|
|
|
trace_stack_cv.set(None)
|
|
|
|
trace_path_stack_cv.set(None)
|
2021-03-08 21:48:36 +00:00
|
|
|
variables_cv.set(None)
|
2021-04-01 16:42:23 +00:00
|
|
|
script_execution_cv.set(StopReason())
|
2021-03-06 11:57:21 +00:00
|
|
|
|
|
|
|
|
2021-03-26 17:14:01 +00:00
|
|
|
def trace_set_child_id(child_key: tuple[str, 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:
|
|
|
|
node.set_child_id(child_key, child_run_id)
|
|
|
|
|
|
|
|
|
2021-03-06 11:57:21 +00:00
|
|
|
def trace_set_result(**kwargs: Any) -> None:
|
|
|
|
"""Set the result of TraceElement at the top of the stack."""
|
|
|
|
node = cast(TraceElement, trace_stack_top(trace_stack_cv))
|
|
|
|
node.set_result(**kwargs)
|
|
|
|
|
|
|
|
|
2021-05-17 23:54:17 +00:00
|
|
|
def trace_update_result(**kwargs: Any) -> None:
|
|
|
|
"""Update the result of TraceElement at the top of the stack."""
|
|
|
|
node = cast(TraceElement, trace_stack_top(trace_stack_cv))
|
|
|
|
node.update_result(**kwargs)
|
|
|
|
|
|
|
|
|
2021-04-01 16:42:23 +00:00
|
|
|
class StopReason:
|
|
|
|
"""Mutable container class for script_execution."""
|
|
|
|
|
|
|
|
script_execution: str | None = None
|
|
|
|
|
|
|
|
|
|
|
|
def script_execution_set(reason: str) -> None:
|
|
|
|
"""Set stop reason."""
|
|
|
|
data = script_execution_cv.get()
|
|
|
|
if data is None:
|
|
|
|
return
|
|
|
|
data.script_execution = reason
|
|
|
|
|
|
|
|
|
|
|
|
def script_execution_get() -> str | None:
|
|
|
|
"""Return the current trace."""
|
|
|
|
data = script_execution_cv.get()
|
|
|
|
if data is None:
|
|
|
|
return None
|
|
|
|
return data.script_execution
|
|
|
|
|
|
|
|
|
2021-03-06 11:57:21 +00:00
|
|
|
@contextmanager
|
2021-03-17 17:34:19 +00:00
|
|
|
def trace_path(suffix: str | list[str]) -> Generator:
|
2021-03-18 20:14:06 +00:00
|
|
|
"""Go deeper in the config tree.
|
|
|
|
|
|
|
|
Can not be used as a decorator on couroutine functions.
|
|
|
|
"""
|
2021-03-06 11:57:21 +00:00
|
|
|
count = trace_path_push(suffix)
|
|
|
|
try:
|
|
|
|
yield
|
|
|
|
finally:
|
|
|
|
trace_path_pop(count)
|
2021-03-18 20:14:06 +00:00
|
|
|
|
|
|
|
|
|
|
|
def async_trace_path(suffix: str | list[str]) -> Callable:
|
|
|
|
"""Go deeper in the config tree.
|
|
|
|
|
|
|
|
To be used as a decorator on coroutine functions.
|
|
|
|
"""
|
|
|
|
|
|
|
|
def _trace_path_decorator(func: Callable) -> Callable:
|
|
|
|
"""Decorate a coroutine function."""
|
|
|
|
|
|
|
|
@wraps(func)
|
|
|
|
async def async_wrapper(*args: Any) -> None:
|
|
|
|
"""Catch and log exception."""
|
|
|
|
with trace_path(suffix):
|
|
|
|
await func(*args)
|
|
|
|
|
|
|
|
return async_wrapper
|
|
|
|
|
|
|
|
return _trace_path_decorator
|