From f82e59c32a10e093d00401b4daeeaa368548cd9d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 16 Mar 2021 00:51:04 +0100 Subject: [PATCH] Make it possible to list debug traces for a specific automation (#47744) --- homeassistant/components/automation/trace.py | 7 +- .../components/automation/websocket_api.py | 17 +++- homeassistant/helpers/condition.py | 4 +- homeassistant/helpers/script.py | 4 +- homeassistant/helpers/trace.py | 7 +- .../automation/test_websocket_api.py | 86 +++++++++++++------ 6 files changed, 86 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/automation/trace.py b/homeassistant/components/automation/trace.py index ebd981f2541..351ca1ed979 100644 --- a/homeassistant/components/automation/trace.py +++ b/homeassistant/components/automation/trace.py @@ -109,6 +109,7 @@ class AutomationTrace: trigger = self._variables.get("trigger", {}).get("description") result = { + "automation_id": self._unique_id, "last_action": last_action, "last_condition": last_condition, "run_id": self.run_id, @@ -196,11 +197,9 @@ def get_debug_traces_for_automation(hass, automation_id, summary=False): @callback def get_debug_traces(hass, summary=False): """Return a serializable list of debug traces.""" - traces = {} + traces = [] for automation_id in hass.data[DATA_AUTOMATION_TRACE]: - traces[automation_id] = get_debug_traces_for_automation( - hass, automation_id, summary - ) + traces.extend(get_debug_traces_for_automation(hass, automation_id, summary)) return traces diff --git a/homeassistant/components/automation/websocket_api.py b/homeassistant/components/automation/websocket_api.py index aaebbef7f83..bb47dd58ff9 100644 --- a/homeassistant/components/automation/websocket_api.py +++ b/homeassistant/components/automation/websocket_api.py @@ -21,7 +21,7 @@ from homeassistant.helpers.script import ( debug_stop, ) -from .trace import get_debug_trace, get_debug_traces +from .trace import get_debug_trace, get_debug_traces, get_debug_traces_for_automation # mypy: allow-untyped-calls, allow-untyped-defs @@ -50,7 +50,7 @@ def async_setup(hass: HomeAssistant) -> None: } ) def websocket_automation_trace_get(hass, connection, msg): - """Get automation traces.""" + """Get an automation trace.""" automation_id = msg["automation_id"] run_id = msg["run_id"] @@ -61,10 +61,19 @@ def websocket_automation_trace_get(hass, connection, msg): @callback @websocket_api.require_admin -@websocket_api.websocket_command({vol.Required("type"): "automation/trace/list"}) +@websocket_api.websocket_command( + {vol.Required("type"): "automation/trace/list", vol.Optional("automation_id"): str} +) def websocket_automation_trace_list(hass, connection, msg): """Summarize automation traces.""" - automation_traces = get_debug_traces(hass, summary=True) + automation_id = msg.get("automation_id") + + if not automation_id: + automation_traces = get_debug_traces(hass, summary=True) + else: + automation_traces = get_debug_traces_for_automation( + hass, automation_id, summary=True + ) connection.send_result(msg["id"], automation_traces) diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 71bbaa5f0f4..10b59645ed0 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -77,8 +77,8 @@ ConditionCheckerType = Callable[[HomeAssistant, TemplateVarsType], bool] def condition_trace_append(variables: TemplateVarsType, path: str) -> TraceElement: """Append a TraceElement to trace[path].""" - trace_element = TraceElement(variables) - trace_append_element(trace_element, path) + trace_element = TraceElement(variables, path) + trace_append_element(trace_element) return trace_element diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index a2df055331d..f1732aef316 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -137,8 +137,8 @@ SCRIPT_DEBUG_CONTINUE_ALL = "script_debug_continue_all" def action_trace_append(variables, path): """Append a TraceElement to trace[path].""" - trace_element = TraceElement(variables) - trace_append_element(trace_element, path, ACTION_TRACE_NODE_MAX_LEN) + trace_element = TraceElement(variables, path) + trace_append_element(trace_element, ACTION_TRACE_NODE_MAX_LEN) return trace_element diff --git a/homeassistant/helpers/trace.py b/homeassistant/helpers/trace.py index e0c67a1de54..a7cd63de479 100644 --- a/homeassistant/helpers/trace.py +++ b/homeassistant/helpers/trace.py @@ -11,9 +11,10 @@ import homeassistant.util.dt as dt_util class TraceElement: """Container for trace data.""" - def __init__(self, variables: TemplateVarsType): + def __init__(self, variables: TemplateVarsType, path: str): """Container for trace data.""" self._error: Optional[Exception] = None + self.path: str = path self._result: Optional[dict] = None self._timestamp = dt_util.utcnow() @@ -42,7 +43,7 @@ class TraceElement: def as_dict(self) -> Dict[str, Any]: """Return dictionary version of this TraceElement.""" - result: Dict[str, Any] = {"timestamp": self._timestamp} + result: Dict[str, Any] = {"path": self.path, "timestamp": self._timestamp} if self._variables: result["changed_variables"] = self._variables if self._error is not None: @@ -129,10 +130,10 @@ def trace_path_get() -> str: def trace_append_element( trace_element: TraceElement, - path: str, maxlen: Optional[int] = None, ) -> None: """Append a TraceElement to trace[path].""" + path = trace_element.path trace = trace_cv.get() if trace is None: trace = {} diff --git a/tests/components/automation/test_websocket_api.py b/tests/components/automation/test_websocket_api.py index 99b9540b06e..84c85e224e5 100644 --- a/tests/components/automation/test_websocket_api.py +++ b/tests/components/automation/test_websocket_api.py @@ -9,6 +9,20 @@ from tests.common import assert_lists_same from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401 +def _find_run_id(traces, automation_id): + """Find newest run_id for an automation.""" + for trace in reversed(traces): + if trace["automation_id"] == automation_id: + return trace["run_id"] + + return None + + +def _find_traces_for_automation(traces, automation_id): + """Find traces for an automation.""" + return [trace for trace in traces if trace["automation_id"] == automation_id] + + async def test_get_automation_trace(hass, hass_ws_client): """Test tracing an automation.""" id = 1 @@ -61,7 +75,7 @@ async def test_get_automation_trace(hass, hass_ws_client): await client.send_json({"id": next_id(), "type": "automation/trace/list"}) response = await client.receive_json() assert response["success"] - run_id = response["result"]["sun"][-1]["run_id"] + run_id = _find_run_id(response["result"], "sun") # Get trace await client.send_json( @@ -97,7 +111,7 @@ async def test_get_automation_trace(hass, hass_ws_client): await client.send_json({"id": next_id(), "type": "automation/trace/list"}) response = await client.receive_json() assert response["success"] - run_id = response["result"]["moon"][-1]["run_id"] + run_id = _find_run_id(response["result"], "moon") # Get trace await client.send_json( @@ -134,7 +148,7 @@ async def test_get_automation_trace(hass, hass_ws_client): await client.send_json({"id": next_id(), "type": "automation/trace/list"}) response = await client.receive_json() assert response["success"] - run_id = response["result"]["moon"][-1]["run_id"] + run_id = _find_run_id(response["result"], "moon") # Get trace await client.send_json( @@ -168,7 +182,7 @@ async def test_get_automation_trace(hass, hass_ws_client): await client.send_json({"id": next_id(), "type": "automation/trace/list"}) response = await client.receive_json() assert response["success"] - run_id = response["result"]["moon"][-1]["run_id"] + run_id = _find_run_id(response["result"], "moon") # Get trace await client.send_json( @@ -237,7 +251,7 @@ async def test_automation_trace_overflow(hass, hass_ws_client): await client.send_json({"id": next_id(), "type": "automation/trace/list"}) response = await client.receive_json() assert response["success"] - assert response["result"] == {} + assert response["result"] == [] # Trigger "sun" and "moon" automation once hass.bus.async_fire("test_event") @@ -248,9 +262,9 @@ async def test_automation_trace_overflow(hass, hass_ws_client): await client.send_json({"id": next_id(), "type": "automation/trace/list"}) response = await client.receive_json() assert response["success"] - assert len(response["result"]["moon"]) == 1 - moon_run_id = response["result"]["moon"][0]["run_id"] - assert len(response["result"]["sun"]) == 1 + assert len(_find_traces_for_automation(response["result"], "moon")) == 1 + moon_run_id = _find_run_id(response["result"], "moon") + assert len(_find_traces_for_automation(response["result"], "sun")) == 1 # Trigger "moon" automation enough times to overflow the number of stored traces for _ in range(automation.trace.STORED_TRACES): @@ -260,13 +274,15 @@ async def test_automation_trace_overflow(hass, hass_ws_client): await client.send_json({"id": next_id(), "type": "automation/trace/list"}) response = await client.receive_json() assert response["success"] - assert len(response["result"]["moon"]) == automation.trace.STORED_TRACES - assert len(response["result"]["sun"]) == 1 - assert int(response["result"]["moon"][0]["run_id"]) == int(moon_run_id) + 1 + moon_traces = _find_traces_for_automation(response["result"], "moon") + assert len(moon_traces) == automation.trace.STORED_TRACES + assert moon_traces[0] + assert int(moon_traces[0]["run_id"]) == int(moon_run_id) + 1 assert ( - int(response["result"]["moon"][-1]["run_id"]) + int(moon_traces[-1]["run_id"]) == int(moon_run_id) + automation.trace.STORED_TRACES ) + assert len(_find_traces_for_automation(response["result"], "sun")) == 1 async def test_list_automation_traces(hass, hass_ws_client): @@ -315,7 +331,14 @@ async def test_list_automation_traces(hass, hass_ws_client): await client.send_json({"id": next_id(), "type": "automation/trace/list"}) response = await client.receive_json() assert response["success"] - assert response["result"] == {} + assert response["result"] == [] + + await client.send_json( + {"id": next_id(), "type": "automation/trace/list", "automation_id": "sun"} + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == [] # Trigger "sun" automation hass.bus.async_fire("test_event") @@ -325,8 +348,23 @@ async def test_list_automation_traces(hass, hass_ws_client): await client.send_json({"id": next_id(), "type": "automation/trace/list"}) response = await client.receive_json() assert response["success"] - assert "moon" not in response["result"] - assert len(response["result"]["sun"]) == 1 + assert len(response["result"]) == 1 + assert len(_find_traces_for_automation(response["result"], "sun")) == 1 + + await client.send_json( + {"id": next_id(), "type": "automation/trace/list", "automation_id": "sun"} + ) + response = await client.receive_json() + assert response["success"] + assert len(response["result"]) == 1 + assert len(_find_traces_for_automation(response["result"], "sun")) == 1 + + await client.send_json( + {"id": next_id(), "type": "automation/trace/list", "automation_id": "moon"} + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == [] # Trigger "moon" automation, with passing condition hass.bus.async_fire("test_event2") @@ -344,9 +382,9 @@ async def test_list_automation_traces(hass, hass_ws_client): await client.send_json({"id": next_id(), "type": "automation/trace/list"}) response = await client.receive_json() assert response["success"] - assert len(response["result"]["moon"]) == 3 - assert len(response["result"]["sun"]) == 1 - trace = response["result"]["sun"][0] + assert len(_find_traces_for_automation(response["result"], "moon")) == 3 + assert len(_find_traces_for_automation(response["result"], "sun")) == 1 + trace = _find_traces_for_automation(response["result"], "sun")[0] assert trace["last_action"] == "action/0" assert trace["last_condition"] is None assert trace["error"] == "Unable to find service test.automation" @@ -355,7 +393,7 @@ async def test_list_automation_traces(hass, hass_ws_client): assert trace["trigger"] == "event 'test_event'" assert trace["unique_id"] == "sun" - trace = response["result"]["moon"][0] + trace = _find_traces_for_automation(response["result"], "moon")[0] assert trace["last_action"] == "action/0" assert trace["last_condition"] == "condition/0" assert "error" not in trace @@ -364,7 +402,7 @@ async def test_list_automation_traces(hass, hass_ws_client): assert trace["trigger"] == "event 'test_event2'" assert trace["unique_id"] == "moon" - trace = response["result"]["moon"][1] + trace = _find_traces_for_automation(response["result"], "moon")[1] assert trace["last_action"] is None assert trace["last_condition"] == "condition/0" assert "error" not in trace @@ -373,7 +411,7 @@ async def test_list_automation_traces(hass, hass_ws_client): assert trace["trigger"] == "event 'test_event3'" assert trace["unique_id"] == "moon" - trace = response["result"]["moon"][2] + trace = _find_traces_for_automation(response["result"], "moon")[2] assert trace["last_action"] == "action/0" assert trace["last_condition"] == "condition/0" assert "error" not in trace @@ -396,7 +434,7 @@ async def test_automation_breakpoints(hass, hass_ws_client): await client.send_json({"id": next_id(), "type": "automation/trace/list"}) response = await client.receive_json() assert response["success"] - trace = response["result"][automation_id][-1] + trace = _find_traces_for_automation(response["result"], automation_id)[-1] assert trace["last_action"] == expected_action assert trace["state"] == expected_state return trace["run_id"] @@ -567,7 +605,7 @@ async def test_automation_breakpoints_2(hass, hass_ws_client): await client.send_json({"id": next_id(), "type": "automation/trace/list"}) response = await client.receive_json() assert response["success"] - trace = response["result"][automation_id][-1] + trace = _find_traces_for_automation(response["result"], automation_id)[-1] assert trace["last_action"] == expected_action assert trace["state"] == expected_state return trace["run_id"] @@ -674,7 +712,7 @@ async def test_automation_breakpoints_3(hass, hass_ws_client): await client.send_json({"id": next_id(), "type": "automation/trace/list"}) response = await client.receive_json() assert response["success"] - trace = response["result"][automation_id][-1] + trace = _find_traces_for_automation(response["result"], automation_id)[-1] assert trace["last_action"] == expected_action assert trace["state"] == expected_state return trace["run_id"]