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