Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 1 addition & 5 deletions homeassistant/components/automation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@
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
Expand All @@ -76,7 +75,7 @@
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
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/automation/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
87 changes: 5 additions & 82 deletions homeassistant/components/automation/trace.py
Original file line number Diff line number Diff line change
@@ -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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that we should allow other integrations to be aware of hass.data[DATA_TRACE]. We should abstract that.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #48276

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

Expand Down Expand Up @@ -134,35 +125,14 @@ def as_short_dict(self) -> dict[str, Any]:
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."""
automation_trace = AutomationTrace(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
Comment on lines +135 to 138
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should abstract away how traces are stored from automation and script integrations.

Suggested change
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
async_store_trace(hass, DOMAIN, automation_trace)

async_store_trace would be a helper from trace integration. It can then pull the unique_id from the trace itself and store it in the right place.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #48276

Expand All @@ -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)}
12 changes: 12 additions & 0 deletions homeassistant/components/trace/__init__.py
Original file line number Diff line number Diff line change
@@ -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, {})
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't use setdefault. We only call async_setup guaranteed once.

Suggested change
hass.data.setdefault(DATA_TRACE, {})
hass.data[DATA_TRACE] = {}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #48276

websocket_api.async_setup(hass)
return True
4 changes: 4 additions & 0 deletions homeassistant/components/trace/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""Shared constants for automation and script tracing and debugging."""

DATA_TRACE = "trace"
STORED_TRACES = 5 # Stored traces per automation
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
STORED_TRACES = 5 # Stored traces per automation
STORED_TRACES = 5 # Stored traces per item

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #48276

9 changes: 9 additions & 0 deletions homeassistant/components/trace/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"domain": "trace",
"name": "Trace",
"documentation": "https://www.home-assistant.io/integrations/automation",
"codeowners": [
"@home-assistant/core"
],
"quality_scale": "internal"
}
35 changes: 35 additions & 0 deletions homeassistant/components/trace/trace.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""Support for automation and script tracing and debugging."""
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file needs a better name. Any reason we wouldn't just put this in the __init__? We don't want other integrations to be aware of implementation details of the trace integration.

Copy link
Copy Markdown
Contributor Author

@emontnemery emontnemery Mar 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #48288

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):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this _for_automation ? I would expect a generic fetch method that also tells integration to pass in their DOMAIN.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #48288

"""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 changes: 43 additions & 0 deletions homeassistant/components/trace/utils.py
Original file line number Diff line number Diff line change
@@ -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)}
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@
)

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

Expand Down Expand Up @@ -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}
Expand Down
1 change: 1 addition & 0 deletions tests/components/trace/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""The tests for Trace."""
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Test Automation config panel."""
"""Test Trace websocket API."""
from unittest.mock import patch

from homeassistant.bootstrap import async_setup_component
Expand Down