Refactor tracing: Move trace support to its own integration (#48224)
parent
781084880b
commit
a49989241a
|
@ -490,6 +490,7 @@ homeassistant/components/toon/* @frenck
|
|||
homeassistant/components/totalconnect/* @austinmroczek
|
||||
homeassistant/components/tplink/* @rytilahti @thegardenmonkey
|
||||
homeassistant/components/traccar/* @ludeeus
|
||||
homeassistant/components/trace/* @home-assistant/core
|
||||
homeassistant/components/trafikverket_train/* @endor-force
|
||||
homeassistant/components/trafikverket_weatherstation/* @endor-force
|
||||
homeassistant/components/transmission/* @engrbm87 @JPHutchins
|
||||
|
|
|
@ -61,7 +61,6 @@ from homeassistant.helpers.typing import TemplateVarsType
|
|||
from homeassistant.loader import bind_hass
|
||||
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
|
||||
|
@ -76,7 +75,7 @@ from .const import (
|
|||
LOGGER,
|
||||
)
|
||||
from .helpers import async_get_blueprints
|
||||
from .trace import DATA_AUTOMATION_TRACE, trace_automation
|
||||
from .trace import trace_automation
|
||||
|
||||
# mypy: allow-untyped-calls, allow-untyped-defs
|
||||
# mypy: no-check-untyped-defs, no-warn-return-any
|
||||
|
@ -176,9 +175,6 @@ 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)
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"domain": "automation",
|
||||
"name": "Automation",
|
||||
"documentation": "https://www.home-assistant.io/integrations/automation",
|
||||
"dependencies": ["blueprint"],
|
||||
"dependencies": ["blueprint", "trace"],
|
||||
"after_dependencies": [
|
||||
"device_automation",
|
||||
"webhook"
|
||||
|
|
|
@ -1,26 +1,17 @@
|
|||
"""Trace support for automation."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import OrderedDict
|
||||
from contextlib import contextmanager
|
||||
import datetime as dt
|
||||
from datetime import timedelta
|
||||
from itertools import count
|
||||
import logging
|
||||
from typing import Any, Awaitable, Callable, Deque
|
||||
from typing import Any, Deque
|
||||
|
||||
from homeassistant.core import Context, HomeAssistant, callback
|
||||
from homeassistant.helpers.json import JSONEncoder as HAJSONEncoder
|
||||
from homeassistant.components.trace.const import DATA_TRACE, STORED_TRACES
|
||||
from homeassistant.components.trace.utils import LimitedSizeDict
|
||||
from homeassistant.core import Context
|
||||
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
|
||||
|
||||
|
@ -134,27 +125,6 @@ class AutomationTrace:
|
|||
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."""
|
||||
|
@ -162,7 +132,7 @@ def trace_automation(hass, unique_id, config, context):
|
|||
trace_id_set((unique_id, automation_trace.run_id))
|
||||
|
||||
if unique_id:
|
||||
automation_traces = hass.data[DATA_AUTOMATION_TRACE]
|
||||
automation_traces = hass.data[DATA_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
|
||||
|
@ -176,50 +146,3 @@ def trace_automation(hass, unique_id, config, context):
|
|||
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.extend(get_debug_traces_for_automation(hass, automation_id, summary))
|
||||
|
||||
return traces
|
||||
|
||||
|
||||
class TraceJSONEncoder(HAJSONEncoder):
|
||||
"""JSONEncoder that supports Home Assistant objects and falls back to repr(o)."""
|
||||
|
||||
def default(self, o: Any) -> Any:
|
||||
"""Convert certain objects.
|
||||
|
||||
Fall back to repr(o).
|
||||
"""
|
||||
if isinstance(o, timedelta):
|
||||
return {"__type": str(type(o)), "total_seconds": o.total_seconds()}
|
||||
try:
|
||||
return super().default(o)
|
||||
except TypeError:
|
||||
return {"__type": str(type(o)), "repr": repr(o)}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
"""Support for automation and script tracing and debugging."""
|
||||
from . import websocket_api
|
||||
from .const import DATA_TRACE
|
||||
|
||||
DOMAIN = "trace"
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
"""Initialize the trace integration."""
|
||||
hass.data.setdefault(DATA_TRACE, {})
|
||||
websocket_api.async_setup(hass)
|
||||
return True
|
|
@ -0,0 +1,4 @@
|
|||
"""Shared constants for automation and script tracing and debugging."""
|
||||
|
||||
DATA_TRACE = "trace"
|
||||
STORED_TRACES = 5 # Stored traces per automation
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"domain": "trace",
|
||||
"name": "Trace",
|
||||
"documentation": "https://www.home-assistant.io/integrations/automation",
|
||||
"codeowners": [
|
||||
"@home-assistant/core"
|
||||
],
|
||||
"quality_scale": "internal"
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
"""Support for automation and script tracing and debugging."""
|
||||
from homeassistant.core import callback
|
||||
|
||||
from .const import DATA_TRACE
|
||||
|
||||
|
||||
@callback
|
||||
def get_debug_trace(hass, automation_id, run_id):
|
||||
"""Return a serializable debug trace."""
|
||||
return hass.data[DATA_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_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_TRACE]:
|
||||
traces.extend(get_debug_traces_for_automation(hass, automation_id, summary))
|
||||
|
||||
return traces
|
|
@ -0,0 +1,43 @@
|
|||
"""Helpers for automation and script tracing and debugging."""
|
||||
from collections import OrderedDict
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.helpers.json import JSONEncoder as HAJSONEncoder
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
class TraceJSONEncoder(HAJSONEncoder):
|
||||
"""JSONEncoder that supports Home Assistant objects and falls back to repr(o)."""
|
||||
|
||||
def default(self, o: Any) -> Any:
|
||||
"""Convert certain objects.
|
||||
|
||||
Fall back to repr(o).
|
||||
"""
|
||||
if isinstance(o, timedelta):
|
||||
return {"__type": str(type(o)), "total_seconds": o.total_seconds()}
|
||||
try:
|
||||
return super().default(o)
|
||||
except TypeError:
|
||||
return {"__type": str(type(o)), "repr": repr(o)}
|
|
@ -24,12 +24,12 @@ from homeassistant.helpers.script import (
|
|||
)
|
||||
|
||||
from .trace import (
|
||||
DATA_AUTOMATION_TRACE,
|
||||
TraceJSONEncoder,
|
||||
DATA_TRACE,
|
||||
get_debug_trace,
|
||||
get_debug_traces,
|
||||
get_debug_traces_for_automation,
|
||||
)
|
||||
from .utils import TraceJSONEncoder
|
||||
|
||||
# mypy: allow-untyped-calls, allow-untyped-defs
|
||||
|
||||
|
@ -101,11 +101,9 @@ def websocket_automation_trace_contexts(hass, connection, msg):
|
|||
automation_id = msg.get("automation_id")
|
||||
|
||||
if automation_id is not None:
|
||||
values = {
|
||||
automation_id: hass.data[DATA_AUTOMATION_TRACE].get(automation_id, {})
|
||||
}
|
||||
values = {automation_id: hass.data[DATA_TRACE].get(automation_id, {})}
|
||||
else:
|
||||
values = hass.data[DATA_AUTOMATION_TRACE]
|
||||
values = hass.data[DATA_TRACE]
|
||||
|
||||
contexts = {
|
||||
trace.context.id: {"run_id": trace.run_id, "automation_id": automation_id}
|
|
@ -0,0 +1 @@
|
|||
"""The tests for Trace."""
|
|
@ -1,14 +1,14 @@
|
|||
"""Test Automation trace helpers."""
|
||||
"""Test trace helpers."""
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant import core
|
||||
from homeassistant.components import automation
|
||||
from homeassistant.components import trace
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
|
||||
def test_json_encoder(hass):
|
||||
"""Test the Trace JSON Encoder."""
|
||||
ha_json_enc = automation.trace.TraceJSONEncoder()
|
||||
ha_json_enc = trace.utils.TraceJSONEncoder()
|
||||
state = core.State("test.test", "hello")
|
||||
|
||||
# Test serializing a datetime
|
|
@ -1,4 +1,4 @@
|
|||
"""Test Automation config panel."""
|
||||
"""Test Trace websocket API."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.bootstrap import async_setup_component
|
Loading…
Reference in New Issue