mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
Refactor tracing: Move trace support to its own integration (#48224)
This commit is contained in:
parent
781084880b
commit
a49989241a
@ -490,6 +490,7 @@ homeassistant/components/toon/* @frenck
|
|||||||
homeassistant/components/totalconnect/* @austinmroczek
|
homeassistant/components/totalconnect/* @austinmroczek
|
||||||
homeassistant/components/tplink/* @rytilahti @thegardenmonkey
|
homeassistant/components/tplink/* @rytilahti @thegardenmonkey
|
||||||
homeassistant/components/traccar/* @ludeeus
|
homeassistant/components/traccar/* @ludeeus
|
||||||
|
homeassistant/components/trace/* @home-assistant/core
|
||||||
homeassistant/components/trafikverket_train/* @endor-force
|
homeassistant/components/trafikverket_train/* @endor-force
|
||||||
homeassistant/components/trafikverket_weatherstation/* @endor-force
|
homeassistant/components/trafikverket_weatherstation/* @endor-force
|
||||||
homeassistant/components/transmission/* @engrbm87 @JPHutchins
|
homeassistant/components/transmission/* @engrbm87 @JPHutchins
|
||||||
|
@ -61,7 +61,6 @@ from homeassistant.helpers.typing import TemplateVarsType
|
|||||||
from homeassistant.loader import bind_hass
|
from homeassistant.loader import bind_hass
|
||||||
from homeassistant.util.dt import parse_datetime
|
from homeassistant.util.dt import parse_datetime
|
||||||
|
|
||||||
from . import websocket_api
|
|
||||||
from .config import AutomationConfig, async_validate_config_item
|
from .config import AutomationConfig, async_validate_config_item
|
||||||
|
|
||||||
# Not used except by packages to check config structure
|
# Not used except by packages to check config structure
|
||||||
@ -76,7 +75,7 @@ from .const import (
|
|||||||
LOGGER,
|
LOGGER,
|
||||||
)
|
)
|
||||||
from .helpers import async_get_blueprints
|
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: allow-untyped-calls, allow-untyped-defs
|
||||||
# mypy: no-check-untyped-defs, no-warn-return-any
|
# mypy: no-check-untyped-defs, no-warn-return-any
|
||||||
@ -176,9 +175,6 @@ async def async_setup(hass, config):
|
|||||||
"""Set up all automations."""
|
"""Set up all automations."""
|
||||||
# Local import to avoid circular import
|
# Local import to avoid circular import
|
||||||
hass.data[DOMAIN] = component = EntityComponent(LOGGER, DOMAIN, hass)
|
hass.data[DOMAIN] = component = EntityComponent(LOGGER, DOMAIN, hass)
|
||||||
hass.data.setdefault(DATA_AUTOMATION_TRACE, {})
|
|
||||||
|
|
||||||
websocket_api.async_setup(hass)
|
|
||||||
|
|
||||||
# To register the automation blueprints
|
# To register the automation blueprints
|
||||||
async_get_blueprints(hass)
|
async_get_blueprints(hass)
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"domain": "automation",
|
"domain": "automation",
|
||||||
"name": "Automation",
|
"name": "Automation",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/automation",
|
"documentation": "https://www.home-assistant.io/integrations/automation",
|
||||||
"dependencies": ["blueprint"],
|
"dependencies": ["blueprint", "trace"],
|
||||||
"after_dependencies": [
|
"after_dependencies": [
|
||||||
"device_automation",
|
"device_automation",
|
||||||
"webhook"
|
"webhook"
|
||||||
|
@ -1,26 +1,17 @@
|
|||||||
"""Trace support for automation."""
|
"""Trace support for automation."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections import OrderedDict
|
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
from datetime import timedelta
|
|
||||||
from itertools import count
|
from itertools import count
|
||||||
import logging
|
from typing import Any, Deque
|
||||||
from typing import Any, Awaitable, Callable, Deque
|
|
||||||
|
|
||||||
from homeassistant.core import Context, HomeAssistant, callback
|
from homeassistant.components.trace.const import DATA_TRACE, STORED_TRACES
|
||||||
from homeassistant.helpers.json import JSONEncoder as HAJSONEncoder
|
from homeassistant.components.trace.utils import LimitedSizeDict
|
||||||
|
from homeassistant.core import Context
|
||||||
from homeassistant.helpers.trace import TraceElement, trace_id_set
|
from homeassistant.helpers.trace import TraceElement, trace_id_set
|
||||||
from homeassistant.helpers.typing import TemplateVarsType
|
|
||||||
from homeassistant.util import dt as dt_util
|
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: allow-untyped-calls, allow-untyped-defs
|
||||||
# mypy: no-check-untyped-defs, no-warn-return-any
|
# mypy: no-check-untyped-defs, no-warn-return-any
|
||||||
|
|
||||||
@ -134,27 +125,6 @@ class AutomationTrace:
|
|||||||
return result
|
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
|
@contextmanager
|
||||||
def trace_automation(hass, unique_id, config, context):
|
def trace_automation(hass, unique_id, config, context):
|
||||||
"""Trace action execution of automation with automation_id."""
|
"""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))
|
trace_id_set((unique_id, automation_trace.run_id))
|
||||||
|
|
||||||
if unique_id:
|
if unique_id:
|
||||||
automation_traces = hass.data[DATA_AUTOMATION_TRACE]
|
automation_traces = hass.data[DATA_TRACE]
|
||||||
if unique_id not in automation_traces:
|
if unique_id not in automation_traces:
|
||||||
automation_traces[unique_id] = LimitedSizeDict(size_limit=STORED_TRACES)
|
automation_traces[unique_id] = LimitedSizeDict(size_limit=STORED_TRACES)
|
||||||
automation_traces[unique_id][automation_trace.run_id] = automation_trace
|
automation_traces[unique_id][automation_trace.run_id] = automation_trace
|
||||||
@ -176,50 +146,3 @@ def trace_automation(hass, unique_id, config, context):
|
|||||||
finally:
|
finally:
|
||||||
if unique_id:
|
if unique_id:
|
||||||
automation_trace.finished()
|
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)}
|
|
||||||
|
12
homeassistant/components/trace/__init__.py
Normal file
12
homeassistant/components/trace/__init__.py
Normal file
@ -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
|
4
homeassistant/components/trace/const.py
Normal file
4
homeassistant/components/trace/const.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
"""Shared constants for automation and script tracing and debugging."""
|
||||||
|
|
||||||
|
DATA_TRACE = "trace"
|
||||||
|
STORED_TRACES = 5 # Stored traces per automation
|
9
homeassistant/components/trace/manifest.json
Normal file
9
homeassistant/components/trace/manifest.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"domain": "trace",
|
||||||
|
"name": "Trace",
|
||||||
|
"documentation": "https://www.home-assistant.io/integrations/automation",
|
||||||
|
"codeowners": [
|
||||||
|
"@home-assistant/core"
|
||||||
|
],
|
||||||
|
"quality_scale": "internal"
|
||||||
|
}
|
35
homeassistant/components/trace/trace.py
Normal file
35
homeassistant/components/trace/trace.py
Normal file
@ -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
|
43
homeassistant/components/trace/utils.py
Normal file
43
homeassistant/components/trace/utils.py
Normal file
@ -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 (
|
from .trace import (
|
||||||
DATA_AUTOMATION_TRACE,
|
DATA_TRACE,
|
||||||
TraceJSONEncoder,
|
|
||||||
get_debug_trace,
|
get_debug_trace,
|
||||||
get_debug_traces,
|
get_debug_traces,
|
||||||
get_debug_traces_for_automation,
|
get_debug_traces_for_automation,
|
||||||
)
|
)
|
||||||
|
from .utils import TraceJSONEncoder
|
||||||
|
|
||||||
# mypy: allow-untyped-calls, allow-untyped-defs
|
# 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")
|
automation_id = msg.get("automation_id")
|
||||||
|
|
||||||
if automation_id is not None:
|
if automation_id is not None:
|
||||||
values = {
|
values = {automation_id: hass.data[DATA_TRACE].get(automation_id, {})}
|
||||||
automation_id: hass.data[DATA_AUTOMATION_TRACE].get(automation_id, {})
|
|
||||||
}
|
|
||||||
else:
|
else:
|
||||||
values = hass.data[DATA_AUTOMATION_TRACE]
|
values = hass.data[DATA_TRACE]
|
||||||
|
|
||||||
contexts = {
|
contexts = {
|
||||||
trace.context.id: {"run_id": trace.run_id, "automation_id": automation_id}
|
trace.context.id: {"run_id": trace.run_id, "automation_id": automation_id}
|
1
tests/components/trace/__init__.py
Normal file
1
tests/components/trace/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""The tests for Trace."""
|
@ -1,14 +1,14 @@
|
|||||||
"""Test Automation trace helpers."""
|
"""Test trace helpers."""
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from homeassistant import core
|
from homeassistant import core
|
||||||
from homeassistant.components import automation
|
from homeassistant.components import trace
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
|
|
||||||
def test_json_encoder(hass):
|
def test_json_encoder(hass):
|
||||||
"""Test the Trace JSON Encoder."""
|
"""Test the Trace JSON Encoder."""
|
||||||
ha_json_enc = automation.trace.TraceJSONEncoder()
|
ha_json_enc = trace.utils.TraceJSONEncoder()
|
||||||
state = core.State("test.test", "hello")
|
state = core.State("test.test", "hello")
|
||||||
|
|
||||||
# Test serializing a datetime
|
# Test serializing a datetime
|
@ -1,4 +1,4 @@
|
|||||||
"""Test Automation config panel."""
|
"""Test Trace websocket API."""
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from homeassistant.bootstrap import async_setup_component
|
from homeassistant.bootstrap import async_setup_component
|
Loading…
x
Reference in New Issue
Block a user