diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 1dd81afaa70..f983e230897 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -55,7 +55,12 @@ from homeassistant.helpers.script import ( ) from homeassistant.helpers.script_variables import ScriptVariables from homeassistant.helpers.service import async_register_admin_service -from homeassistant.helpers.trace import trace_get, trace_path +from homeassistant.helpers.trace import ( + TraceElement, + trace_append_element, + trace_get, + trace_path, +) from homeassistant.helpers.trigger import async_initialize_triggers from homeassistant.helpers.typing import TemplateVarsType from homeassistant.loader import bind_hass @@ -406,10 +411,16 @@ class AutomationEntity(ToggleEntity, RestoreEntity): return else: variables = run_variables - automation_trace.set_variables(variables) + # Prepare tracing the automation + automation_trace.set_trace(trace_get()) - # Prepare tracing the evaluation of the automation's conditions - automation_trace.set_condition_trace(trace_get()) + # Set trigger reason + trigger_description = variables.get("trigger", {}).get("description") + automation_trace.set_trigger_description(trigger_description) + + # Add initial variables as the trigger step + trace_element = TraceElement(variables, "trigger") + trace_append_element(trace_element) if ( not skip_condition @@ -422,9 +433,6 @@ class AutomationEntity(ToggleEntity, RestoreEntity): ) return - # Prepare tracing the execution of the automation's actions - automation_trace.set_action_trace(trace_get()) - self.async_set_context(trigger_context) event_data = { ATTR_NAME: self._name, diff --git a/homeassistant/components/automation/trace.py b/homeassistant/components/automation/trace.py index 31c20eb1402..631d3122e4c 100644 --- a/homeassistant/components/automation/trace.py +++ b/homeassistant/components/automation/trace.py @@ -2,11 +2,10 @@ from __future__ import annotations from contextlib import contextmanager -from typing import Any, Deque +from typing import Any from homeassistant.components.trace import ActionTrace, async_store_trace from homeassistant.core import Context -from homeassistant.helpers.trace import TraceElement # mypy: allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs, no-warn-return-any @@ -24,42 +23,16 @@ class AutomationTrace(ActionTrace): """Container for automation trace.""" key = ("automation", item_id) super().__init__(key, config, context) - self._condition_trace: dict[str, Deque[TraceElement]] | None = None + self._trigger_description: str | None = None - def set_condition_trace(self, trace: dict[str, Deque[TraceElement]]) -> None: - """Set condition trace.""" - self._condition_trace = trace - - def as_dict(self) -> dict[str, Any]: - """Return dictionary version of this AutomationTrace.""" - - result = super().as_dict() - - condition_traces = {} - - if self._condition_trace: - for key, trace_list in self._condition_trace.items(): - condition_traces[key] = [item.as_dict() for item in trace_list] - result["condition_trace"] = condition_traces - - return result + def set_trigger_description(self, trigger: str) -> None: + """Set trigger description.""" + self._trigger_description = trigger def as_short_dict(self) -> dict[str, Any]: """Return a brief dictionary version of this AutomationTrace.""" - result = super().as_short_dict() - - last_condition = None - trigger = None - - if self._condition_trace: - last_condition = list(self._condition_trace)[-1] - if self._variables: - trigger = self._variables.get("trigger", {}).get("description") - - result["trigger"] = trigger - result["last_condition"] = last_condition - + result["trigger"] = self._trigger_description return result diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index e92ccb12bee..d8af3e4a96d 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -354,9 +354,8 @@ class ScriptEntity(ToggleEntity): with trace_script( self.hass, self.object_id, self._raw_config, context ) as script_trace: - script_trace.set_variables(variables) # Prepare tracing the execution of the script's sequence - script_trace.set_action_trace(trace_get()) + script_trace.set_trace(trace_get()) with trace_path("sequence"): return await self.script.async_run(variables, context) diff --git a/homeassistant/components/trace/__init__.py b/homeassistant/components/trace/__init__.py index 9505c1c1264..cdae44fff6b 100644 --- a/homeassistant/components/trace/__init__.py +++ b/homeassistant/components/trace/__init__.py @@ -50,7 +50,7 @@ class ActionTrace: context: Context, ): """Container for script trace.""" - self._action_trace: dict[str, Deque[TraceElement]] | None = None + self._trace: dict[str, Deque[TraceElement]] | None = None self._config: dict[str, Any] = config self.context: Context = context self._error: Exception | None = None @@ -59,23 +59,18 @@ class ActionTrace: self._timestamp_finish: dt.datetime | None = None self._timestamp_start: dt.datetime = dt_util.utcnow() self.key: tuple[str, str] = key - self._variables: dict[str, Any] | None = None if trace_id_get(): trace_set_child_id(self.key, self.run_id) trace_id_set((key, self.run_id)) - def set_action_trace(self, trace: dict[str, Deque[TraceElement]]) -> None: - """Set action trace.""" - self._action_trace = trace + def set_trace(self, trace: dict[str, Deque[TraceElement]]) -> None: + """Set trace.""" + self._trace = trace def set_error(self, ex: Exception) -> None: """Set error.""" self._error = ex - def set_variables(self, variables: dict[str, Any]) -> None: - """Set variables.""" - self._variables = variables - def finished(self) -> None: """Set finish time.""" self._timestamp_finish = dt_util.utcnow() @@ -86,17 +81,16 @@ class ActionTrace: result = self.as_short_dict() - action_traces = {} - if self._action_trace: - for key, trace_list in self._action_trace.items(): - action_traces[key] = [item.as_dict() for item in trace_list] + traces = {} + if self._trace: + for key, trace_list in self._trace.items(): + traces[key] = [item.as_dict() for item in trace_list] result.update( { - "action_trace": action_traces, + "trace": traces, "config": self._config, "context": self.context, - "variables": self._variables, } ) if self._error is not None: @@ -106,13 +100,13 @@ class ActionTrace: def as_short_dict(self) -> dict[str, Any]: """Return a brief dictionary version of this ActionTrace.""" - last_action = None + last_step = None - if self._action_trace: - last_action = list(self._action_trace)[-1] + if self._trace: + last_step = list(self._trace)[-1] result = { - "last_action": last_action, + "last_step": last_step, "run_id": self.run_id, "state": self._state, "timestamp": { @@ -124,7 +118,7 @@ class ActionTrace: } if self._error is not None: result["error"] = str(self._error) - if last_action is not None: - result["last_action"] = last_action + if last_step is not None: + result["last_step"] = last_step return result diff --git a/tests/components/trace/test_websocket_api.py b/tests/components/trace/test_websocket_api.py index 38c6a0f097e..1ff86060eec 100644 --- a/tests/components/trace/test_websocket_api.py +++ b/tests/components/trace/test_websocket_api.py @@ -110,18 +110,21 @@ async def test_get_trace(hass, hass_ws_client, domain, prefix): response = await client.receive_json() assert response["success"] trace = response["result"] - assert len(trace["action_trace"]) == 1 - assert len(trace["action_trace"][f"{prefix}/0"]) == 1 - assert trace["action_trace"][f"{prefix}/0"][0]["error"] - assert trace["action_trace"][f"{prefix}/0"][0]["result"] == sun_action + if domain == "automation": + assert len(trace["trace"]) == 2 + assert set(trace["trace"]) == {"trigger", f"{prefix}/0"} + else: + assert len(trace["trace"]) == 1 + assert set(trace["trace"]) == {f"{prefix}/0"} + assert len(trace["trace"][f"{prefix}/0"]) == 1 + assert trace["trace"][f"{prefix}/0"][0]["error"] + assert trace["trace"][f"{prefix}/0"][0]["result"] == sun_action assert trace["config"] == sun_config assert trace["context"] assert trace["error"] == "Unable to find service test.automation" assert trace["state"] == "stopped" assert trace["item_id"] == "sun" - assert trace["variables"] is not None if domain == "automation": - assert trace["condition_trace"] == {} assert trace["context"]["parent_id"] == context.id assert trace["trigger"] == "event 'test_event'" else: @@ -158,21 +161,24 @@ async def test_get_trace(hass, hass_ws_client, domain, prefix): response = await client.receive_json() assert response["success"] trace = response["result"] - assert len(trace["action_trace"]) == 1 - assert len(trace["action_trace"][f"{prefix}/0"]) == 1 - assert "error" not in trace["action_trace"][f"{prefix}/0"][0] - assert trace["action_trace"][f"{prefix}/0"][0]["result"] == moon_action + if domain == "automation": + assert len(trace["trace"]) == 3 + assert set(trace["trace"]) == {"trigger", "condition/0", f"{prefix}/0"} + else: + assert len(trace["trace"]) == 1 + assert set(trace["trace"]) == {f"{prefix}/0"} + assert len(trace["trace"][f"{prefix}/0"]) == 1 + assert "error" not in trace["trace"][f"{prefix}/0"][0] + assert trace["trace"][f"{prefix}/0"][0]["result"] == moon_action assert trace["config"] == moon_config assert trace["context"] assert "error" not in trace assert trace["state"] == "stopped" assert trace["item_id"] == "moon" - assert trace["variables"] is not None if domain == "automation": - assert len(trace["condition_trace"]) == 1 - assert len(trace["condition_trace"]["condition/0"]) == 1 - assert trace["condition_trace"]["condition/0"][0]["result"] == {"result": True} + assert len(trace["trace"]["condition/0"]) == 1 + assert trace["trace"]["condition/0"][0]["result"] == {"result": True} assert trace["trigger"] == "event 'test_event2'" contexts[trace["context"]["id"]] = { "run_id": trace["run_id"], @@ -211,17 +217,20 @@ async def test_get_trace(hass, hass_ws_client, domain, prefix): response = await client.receive_json() assert response["success"] trace = response["result"] - assert len(trace["action_trace"]) == 0 - assert len(trace["condition_trace"]) == 1 - assert len(trace["condition_trace"]["condition/0"]) == 1 - assert trace["condition_trace"]["condition/0"][0]["result"] == {"result": False} + if domain == "automation": + assert len(trace["trace"]) == 2 + assert set(trace["trace"]) == {"trigger", "condition/0"} + else: + assert len(trace["trace"]) == 1 + assert set(trace["trace"]) == {f"{prefix}/0"} + assert len(trace["trace"]["condition/0"]) == 1 + assert trace["trace"]["condition/0"][0]["result"] == {"result": False} assert trace["config"] == moon_config assert trace["context"] assert "error" not in trace assert trace["state"] == "stopped" assert trace["trigger"] == "event 'test_event3'" assert trace["item_id"] == "moon" - assert trace["variables"] contexts[trace["context"]["id"]] = { "run_id": trace["run_id"], "domain": domain, @@ -251,20 +260,19 @@ async def test_get_trace(hass, hass_ws_client, domain, prefix): response = await client.receive_json() assert response["success"] trace = response["result"] - assert len(trace["action_trace"]) == 1 - assert len(trace["action_trace"][f"{prefix}/0"]) == 1 - assert "error" not in trace["action_trace"][f"{prefix}/0"][0] - assert trace["action_trace"][f"{prefix}/0"][0]["result"] == moon_action - assert len(trace["condition_trace"]) == 1 - assert len(trace["condition_trace"]["condition/0"]) == 1 - assert trace["condition_trace"]["condition/0"][0]["result"] == {"result": True} + assert len(trace["trace"]) == 3 + assert set(trace["trace"]) == {"trigger", "condition/0", f"{prefix}/0"} + assert len(trace["trace"][f"{prefix}/0"]) == 1 + assert "error" not in trace["trace"][f"{prefix}/0"][0] + assert trace["trace"][f"{prefix}/0"][0]["result"] == moon_action + assert len(trace["trace"]["condition/0"]) == 1 + assert trace["trace"]["condition/0"][0]["result"] == {"result": True} assert trace["config"] == moon_config assert trace["context"] assert "error" not in trace assert trace["state"] == "stopped" assert trace["trigger"] == "event 'test_event2'" assert trace["item_id"] == "moon" - assert trace["variables"] contexts[trace["context"]["id"]] = { "run_id": trace["run_id"], "domain": domain, @@ -467,23 +475,21 @@ async def test_list_traces(hass, hass_ws_client, domain, prefix): assert len(_find_traces(response["result"], domain, "moon")) == 3 assert len(_find_traces(response["result"], domain, "sun")) == 1 trace = _find_traces(response["result"], domain, "sun")[0] - assert trace["last_action"] == f"{prefix}/0" + assert trace["last_step"] == f"{prefix}/0" assert trace["error"] == "Unable to find service test.automation" assert trace["state"] == "stopped" assert trace["timestamp"] assert trace["item_id"] == "sun" if domain == "automation": - assert trace["last_condition"] is None assert trace["trigger"] == "event 'test_event'" trace = _find_traces(response["result"], domain, "moon")[0] - assert trace["last_action"] == f"{prefix}/0" + assert trace["last_step"] == f"{prefix}/0" assert "error" not in trace assert trace["state"] == "stopped" assert trace["timestamp"] assert trace["item_id"] == "moon" if domain == "automation": - assert trace["last_condition"] == "condition/0" assert trace["trigger"] == "event 'test_event2'" trace = _find_traces(response["result"], domain, "moon")[1] @@ -492,20 +498,18 @@ async def test_list_traces(hass, hass_ws_client, domain, prefix): assert trace["timestamp"] assert trace["item_id"] == "moon" if domain == "automation": - assert trace["last_action"] is None - assert trace["last_condition"] == "condition/0" + assert trace["last_step"] == "condition/0" assert trace["trigger"] == "event 'test_event3'" else: - assert trace["last_action"] == f"{prefix}/0" + assert trace["last_step"] == f"{prefix}/0" trace = _find_traces(response["result"], domain, "moon")[2] - assert trace["last_action"] == f"{prefix}/0" + assert trace["last_step"] == f"{prefix}/0" assert "error" not in trace assert trace["state"] == "stopped" assert trace["timestamp"] assert trace["item_id"] == "moon" if domain == "automation": - assert trace["last_condition"] == "condition/0" assert trace["trigger"] == "event 'test_event2'" @@ -585,9 +589,14 @@ async def test_nested_traces(hass, hass_ws_client, domain, prefix): response = await client.receive_json() assert response["success"] trace = response["result"] - assert len(trace["action_trace"]) == 1 - assert len(trace["action_trace"][f"{prefix}/0"]) == 1 - child_id = trace["action_trace"][f"{prefix}/0"][0]["child_id"] + if domain == "automation": + assert len(trace["trace"]) == 2 + assert set(trace["trace"]) == {"trigger", f"{prefix}/0"} + else: + assert len(trace["trace"]) == 1 + assert set(trace["trace"]) == {f"{prefix}/0"} + assert len(trace["trace"][f"{prefix}/0"]) == 1 + child_id = trace["trace"][f"{prefix}/0"][0]["child_id"] assert child_id == {"domain": "script", "item_id": "moon", "run_id": moon_run_id} @@ -603,14 +612,14 @@ async def test_breakpoints(hass, hass_ws_client, domain, prefix): id += 1 return id - async def assert_last_action(item_id, expected_action, expected_state): + async def assert_last_step(item_id, expected_action, expected_state): await client.send_json( {"id": next_id(), "type": "trace/list", "domain": domain} ) response = await client.receive_json() assert response["success"] trace = _find_traces(response["result"], domain, item_id)[-1] - assert trace["last_action"] == expected_action + assert trace["last_step"] == expected_action assert trace["state"] == expected_state return trace["run_id"] @@ -704,7 +713,7 @@ async def test_breakpoints(hass, hass_ws_client, domain, prefix): await hass.services.async_call("script", "sun") response = await client.receive_json() - run_id = await assert_last_action("sun", f"{prefix}/1", "running") + run_id = await assert_last_step("sun", f"{prefix}/1", "running") assert response["event"] == { "domain": domain, "item_id": "sun", @@ -725,7 +734,7 @@ async def test_breakpoints(hass, hass_ws_client, domain, prefix): assert response["success"] response = await client.receive_json() - run_id = await assert_last_action("sun", f"{prefix}/2", "running") + run_id = await assert_last_step("sun", f"{prefix}/2", "running") assert response["event"] == { "domain": domain, "item_id": "sun", @@ -746,7 +755,7 @@ async def test_breakpoints(hass, hass_ws_client, domain, prefix): assert response["success"] response = await client.receive_json() - run_id = await assert_last_action("sun", f"{prefix}/5", "running") + run_id = await assert_last_step("sun", f"{prefix}/5", "running") assert response["event"] == { "domain": domain, "item_id": "sun", @@ -766,7 +775,7 @@ async def test_breakpoints(hass, hass_ws_client, domain, prefix): response = await client.receive_json() assert response["success"] await hass.async_block_till_done() - await assert_last_action("sun", f"{prefix}/5", "stopped") + await assert_last_step("sun", f"{prefix}/5", "stopped") @pytest.mark.parametrize( @@ -781,14 +790,14 @@ async def test_breakpoints_2(hass, hass_ws_client, domain, prefix): id += 1 return id - async def assert_last_action(item_id, expected_action, expected_state): + async def assert_last_step(item_id, expected_action, expected_state): await client.send_json( {"id": next_id(), "type": "trace/list", "domain": domain} ) response = await client.receive_json() assert response["success"] trace = _find_traces(response["result"], domain, item_id)[-1] - assert trace["last_action"] == expected_action + assert trace["last_step"] == expected_action assert trace["state"] == expected_state return trace["run_id"] @@ -843,7 +852,7 @@ async def test_breakpoints_2(hass, hass_ws_client, domain, prefix): await hass.services.async_call("script", "sun") response = await client.receive_json() - run_id = await assert_last_action("sun", f"{prefix}/1", "running") + run_id = await assert_last_step("sun", f"{prefix}/1", "running") assert response["event"] == { "domain": domain, "item_id": "sun", @@ -858,7 +867,7 @@ async def test_breakpoints_2(hass, hass_ws_client, domain, prefix): response = await client.receive_json() assert response["success"] await hass.async_block_till_done() - await assert_last_action("sun", f"{prefix}/8", "stopped") + await assert_last_step("sun", f"{prefix}/8", "stopped") # Should not be possible to set breakpoints await client.send_json( @@ -880,7 +889,7 @@ async def test_breakpoints_2(hass, hass_ws_client, domain, prefix): await hass.services.async_call("script", "sun") await hass.async_block_till_done() - new_run_id = await assert_last_action("sun", f"{prefix}/8", "stopped") + new_run_id = await assert_last_step("sun", f"{prefix}/8", "stopped") assert new_run_id != run_id @@ -896,14 +905,14 @@ async def test_breakpoints_3(hass, hass_ws_client, domain, prefix): id += 1 return id - async def assert_last_action(item_id, expected_action, expected_state): + async def assert_last_step(item_id, expected_action, expected_state): await client.send_json( {"id": next_id(), "type": "trace/list", "domain": domain} ) response = await client.receive_json() assert response["success"] trace = _find_traces(response["result"], domain, item_id)[-1] - assert trace["last_action"] == expected_action + assert trace["last_step"] == expected_action assert trace["state"] == expected_state return trace["run_id"] @@ -970,7 +979,7 @@ async def test_breakpoints_3(hass, hass_ws_client, domain, prefix): await hass.services.async_call("script", "sun") response = await client.receive_json() - run_id = await assert_last_action("sun", f"{prefix}/1", "running") + run_id = await assert_last_step("sun", f"{prefix}/1", "running") assert response["event"] == { "domain": domain, "item_id": "sun", @@ -991,7 +1000,7 @@ async def test_breakpoints_3(hass, hass_ws_client, domain, prefix): assert response["success"] response = await client.receive_json() - run_id = await assert_last_action("sun", f"{prefix}/5", "running") + run_id = await assert_last_step("sun", f"{prefix}/5", "running") assert response["event"] == { "domain": domain, "item_id": "sun", @@ -1011,7 +1020,7 @@ async def test_breakpoints_3(hass, hass_ws_client, domain, prefix): response = await client.receive_json() assert response["success"] await hass.async_block_till_done() - await assert_last_action("sun", f"{prefix}/5", "stopped") + await assert_last_step("sun", f"{prefix}/5", "stopped") # Clear 1st breakpoint await client.send_json( @@ -1033,7 +1042,7 @@ async def test_breakpoints_3(hass, hass_ws_client, domain, prefix): await hass.services.async_call("script", "sun") response = await client.receive_json() - run_id = await assert_last_action("sun", f"{prefix}/5", "running") + run_id = await assert_last_step("sun", f"{prefix}/5", "running") assert response["event"] == { "domain": domain, "item_id": "sun",