diff --git a/CODEOWNERS b/CODEOWNERS index 2afbc288b0f..31ce9706baa 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -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 diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 79c6dcc2312..1dd81afaa70 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -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) diff --git a/homeassistant/components/automation/manifest.json b/homeassistant/components/automation/manifest.json index 2db56eb597f..2483f57de8e 100644 --- a/homeassistant/components/automation/manifest.json +++ b/homeassistant/components/automation/manifest.json @@ -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" diff --git a/homeassistant/components/automation/trace.py b/homeassistant/components/automation/trace.py index 79fa4c844bc..de199ad9310 100644 --- a/homeassistant/components/automation/trace.py +++ b/homeassistant/components/automation/trace.py @@ -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)} diff --git a/homeassistant/components/trace/__init__.py b/homeassistant/components/trace/__init__.py new file mode 100644 index 00000000000..0dc8cda6664 --- /dev/null +++ b/homeassistant/components/trace/__init__.py @@ -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 diff --git a/homeassistant/components/trace/const.py b/homeassistant/components/trace/const.py new file mode 100644 index 00000000000..547bdb35c77 --- /dev/null +++ b/homeassistant/components/trace/const.py @@ -0,0 +1,4 @@ +"""Shared constants for automation and script tracing and debugging.""" + +DATA_TRACE = "trace" +STORED_TRACES = 5 # Stored traces per automation diff --git a/homeassistant/components/trace/manifest.json b/homeassistant/components/trace/manifest.json new file mode 100644 index 00000000000..cdd857d00d3 --- /dev/null +++ b/homeassistant/components/trace/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "trace", + "name": "Trace", + "documentation": "https://www.home-assistant.io/integrations/automation", + "codeowners": [ + "@home-assistant/core" + ], + "quality_scale": "internal" +} diff --git a/homeassistant/components/trace/trace.py b/homeassistant/components/trace/trace.py new file mode 100644 index 00000000000..cc51e3269ab --- /dev/null +++ b/homeassistant/components/trace/trace.py @@ -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 diff --git a/homeassistant/components/trace/utils.py b/homeassistant/components/trace/utils.py new file mode 100644 index 00000000000..59bf8c98498 --- /dev/null +++ b/homeassistant/components/trace/utils.py @@ -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)} diff --git a/homeassistant/components/automation/websocket_api.py b/homeassistant/components/trace/websocket_api.py similarity index 97% rename from homeassistant/components/automation/websocket_api.py rename to homeassistant/components/trace/websocket_api.py index 7bd1b57d064..9ac1828de14 100644 --- a/homeassistant/components/automation/websocket_api.py +++ b/homeassistant/components/trace/websocket_api.py @@ -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} diff --git a/tests/components/trace/__init__.py b/tests/components/trace/__init__.py new file mode 100644 index 00000000000..13937105ae3 --- /dev/null +++ b/tests/components/trace/__init__.py @@ -0,0 +1 @@ +"""The tests for Trace.""" diff --git a/tests/components/automation/test_trace.py b/tests/components/trace/test_utils.py similarity index 88% rename from tests/components/automation/test_trace.py rename to tests/components/trace/test_utils.py index 612a0ccfcab..ce0f09bfdd8 100644 --- a/tests/components/automation/test_trace.py +++ b/tests/components/trace/test_utils.py @@ -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 diff --git a/tests/components/automation/test_websocket_api.py b/tests/components/trace/test_websocket_api.py similarity index 99% rename from tests/components/automation/test_websocket_api.py rename to tests/components/trace/test_websocket_api.py index 6f630c27470..882d857c014 100644 --- a/tests/components/automation/test_websocket_api.py +++ b/tests/components/trace/test_websocket_api.py @@ -1,4 +1,4 @@ -"""Test Automation config panel.""" +"""Test Trace websocket API.""" from unittest.mock import patch from homeassistant.bootstrap import async_setup_component