From a243adc5518c7c286c2e53a0b684a3ac505a9ec8 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 8 Mar 2021 20:30:52 +0100 Subject: [PATCH] Add WS command to get a summary of automation traces (#47557) * Add WS command to get a summary of automation traces * Update tests * Correct rebase mistake, update tests --- .../components/automation/__init__.py | 46 +++++- homeassistant/components/config/automation.py | 16 +- tests/components/config/test_automation.py | 141 +++++++++++++++--- 3 files changed, 178 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 9963c942f08..8b0fa1687ee 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -303,8 +303,8 @@ class AutomationTrace: "condition_trace": condition_traces, "config": self._config, "context": self._context, - "state": self._state, "run_id": self.runid, + "state": self._state, "timestamp": { "start": self._timestamp_start, "finish": self._timestamp_finish, @@ -317,6 +317,37 @@ class AutomationTrace: result["error"] = str(self._error) return result + def as_short_dict(self) -> Dict[str, Any]: + """Return a brief dictionary version of this AutomationTrace.""" + + last_action = None + last_condition = None + + if self._action_trace: + last_action = list(self._action_trace.keys())[-1] + if self._condition_trace: + last_condition = list(self._condition_trace.keys())[-1] + + result = { + "last_action": last_action, + "last_condition": last_condition, + "run_id": self.runid, + "state": self._state, + "timestamp": { + "start": self._timestamp_start, + "finish": self._timestamp_finish, + }, + "trigger": self._trigger.get("description"), + "unique_id": self._unique_id, + } + if self._error is not None: + result["error"] = str(self._error) + if last_action is not None: + result["last_action"] = last_action + result["last_condition"] = last_condition + + return result + class LimitedSizeDict(OrderedDict): """OrderedDict limited in size.""" @@ -857,22 +888,27 @@ def _trigger_extract_entities(trigger_conf: dict) -> List[str]: @callback -def get_debug_traces_for_automation(hass, automation_id): +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(): - traces.append(trace.as_dict()) + if summary: + traces.append(trace.as_short_dict()) + else: + traces.append(trace.as_dict()) return traces @callback -def get_debug_traces(hass): +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[automation_id] = get_debug_traces_for_automation(hass, automation_id) + traces[automation_id] = get_debug_traces_for_automation( + hass, automation_id, summary + ) return traces diff --git a/homeassistant/components/config/automation.py b/homeassistant/components/config/automation.py index 23baa0c8843..708ad55aaeb 100644 --- a/homeassistant/components/config/automation.py +++ b/homeassistant/components/config/automation.py @@ -24,7 +24,8 @@ from . import ACTION_DELETE, EditIdBasedConfigView async def async_setup(hass): """Set up the Automation config API.""" - websocket_api.async_register_command(hass, websocket_automation_trace) + websocket_api.async_register_command(hass, websocket_automation_trace_get) + websocket_api.async_register_command(hass, websocket_automation_trace_list) async def hook(action, config_key): """post_write_hook for Config View that reloads automations.""" @@ -92,10 +93,10 @@ class EditAutomationConfigView(EditIdBasedConfigView): @websocket_api.websocket_command( - {vol.Required("type"): "automation/trace", vol.Optional("automation_id"): str} + {vol.Required("type"): "automation/trace/get", vol.Optional("automation_id"): str} ) @websocket_api.async_response -async def websocket_automation_trace(hass, connection, msg): +async def websocket_automation_trace_get(hass, connection, msg): """Get automation traces.""" automation_id = msg.get("automation_id") @@ -107,3 +108,12 @@ async def websocket_automation_trace(hass, connection, msg): } connection.send_result(msg["id"], automation_traces) + + +@websocket_api.websocket_command({vol.Required("type"): "automation/trace/list"}) +@websocket_api.async_response +async def websocket_automation_trace_list(hass, connection, msg): + """Summarize automation traces.""" + automation_traces = get_debug_traces(hass, summary=True) + + connection.send_result(msg["id"], automation_traces) diff --git a/tests/components/config/test_automation.py b/tests/components/config/test_automation.py index cb192befade..aaf97e22ccd 100644 --- a/tests/components/config/test_automation.py +++ b/tests/components/config/test_automation.py @@ -167,7 +167,7 @@ async def test_delete_automation(hass, hass_client): async def test_get_automation_trace(hass, hass_ws_client): - """Test deleting an automation.""" + """Test tracing an automation.""" id = 1 def next_id(): @@ -209,13 +209,13 @@ async def test_get_automation_trace(hass, hass_ws_client): client = await hass_ws_client() - await client.send_json({"id": next_id(), "type": "automation/trace"}) + await client.send_json({"id": next_id(), "type": "automation/trace/get"}) response = await client.receive_json() assert response["success"] assert response["result"] == {} await client.send_json( - {"id": next_id(), "type": "automation/trace", "automation_id": "sun"} + {"id": next_id(), "type": "automation/trace/get", "automation_id": "sun"} ) response = await client.receive_json() assert response["success"] @@ -226,7 +226,7 @@ async def test_get_automation_trace(hass, hass_ws_client): await hass.async_block_till_done() # Get trace - await client.send_json({"id": next_id(), "type": "automation/trace"}) + await client.send_json({"id": next_id(), "type": "automation/trace/get"}) response = await client.receive_json() assert response["success"] assert "moon" not in response["result"] @@ -251,7 +251,7 @@ async def test_get_automation_trace(hass, hass_ws_client): # Get trace await client.send_json( - {"id": next_id(), "type": "automation/trace", "automation_id": "moon"} + {"id": next_id(), "type": "automation/trace/get", "automation_id": "moon"} ) response = await client.receive_json() assert response["success"] @@ -279,7 +279,7 @@ async def test_get_automation_trace(hass, hass_ws_client): # Get trace await client.send_json( - {"id": next_id(), "type": "automation/trace", "automation_id": "moon"} + {"id": next_id(), "type": "automation/trace/get", "automation_id": "moon"} ) response = await client.receive_json() assert response["success"] @@ -304,7 +304,7 @@ async def test_get_automation_trace(hass, hass_ws_client): # Get trace await client.send_json( - {"id": next_id(), "type": "automation/trace", "automation_id": "moon"} + {"id": next_id(), "type": "automation/trace/get", "automation_id": "moon"} ) response = await client.receive_json() assert response["success"] @@ -363,25 +363,18 @@ async def test_automation_trace_overflow(hass, hass_ws_client): client = await hass_ws_client() - await client.send_json({"id": next_id(), "type": "automation/trace"}) + await client.send_json({"id": next_id(), "type": "automation/trace/list"}) response = await client.receive_json() assert response["success"] assert response["result"] == {} - await client.send_json( - {"id": next_id(), "type": "automation/trace", "automation_id": "sun"} - ) - response = await client.receive_json() - assert response["success"] - assert response["result"] == {"sun": []} - # Trigger "sun" and "moon" automation once hass.bus.async_fire("test_event") hass.bus.async_fire("test_event2") await hass.async_block_till_done() # Get traces - await client.send_json({"id": next_id(), "type": "automation/trace"}) + 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 @@ -393,7 +386,7 @@ async def test_automation_trace_overflow(hass, hass_ws_client): hass.bus.async_fire("test_event2") await hass.async_block_till_done() - await client.send_json({"id": next_id(), "type": "automation/trace"}) + 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.STORED_TRACES @@ -403,3 +396,117 @@ async def test_automation_trace_overflow(hass, hass_ws_client): int(response["result"]["moon"][-1]["run_id"]) == int(moon_run_id) + automation.STORED_TRACES ) + + +async def test_list_automation_traces(hass, hass_ws_client): + """Test listing automation traces.""" + id = 1 + + def next_id(): + nonlocal id + id += 1 + return id + + sun_config = { + "id": "sun", + "trigger": {"platform": "event", "event_type": "test_event"}, + "action": {"service": "test.automation"}, + } + moon_config = { + "id": "moon", + "trigger": [ + {"platform": "event", "event_type": "test_event2"}, + {"platform": "event", "event_type": "test_event3"}, + ], + "condition": { + "condition": "template", + "value_template": "{{ trigger.event.event_type=='test_event2' }}", + }, + "action": {"event": "another_event"}, + } + + assert await async_setup_component( + hass, + "automation", + { + "automation": [ + sun_config, + moon_config, + ] + }, + ) + + with patch.object(config, "SECTIONS", ["automation"]): + await async_setup_component(hass, "config", {}) + + client = await 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"] == {} + + # Trigger "sun" automation + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + + # Get trace + 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 + + # Trigger "moon" automation, with passing condition + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + + # Trigger "moon" automation, with failing condition + hass.bus.async_fire("test_event3") + await hass.async_block_till_done() + + # Trigger "moon" automation, with passing condition + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + + # Get trace + 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 trace["last_action"] == "action/0" + assert trace["last_condition"] is None + assert trace["error"] == "Unable to find service test.automation" + assert trace["state"] == "stopped" + assert trace["timestamp"] + assert trace["trigger"] == "event 'test_event'" + assert trace["unique_id"] == "sun" + + trace = response["result"]["moon"][0] + assert trace["last_action"] == "action/0" + assert trace["last_condition"] == "condition/0" + assert "error" not in trace + assert trace["state"] == "stopped" + assert trace["timestamp"] + assert trace["trigger"] == "event 'test_event2'" + assert trace["unique_id"] == "moon" + + trace = response["result"]["moon"][1] + assert trace["last_action"] is None + assert trace["last_condition"] == "condition/0" + assert "error" not in trace + assert trace["state"] == "stopped" + assert trace["timestamp"] + assert trace["trigger"] == "event 'test_event3'" + assert trace["unique_id"] == "moon" + + trace = response["result"]["moon"][2] + assert trace["last_action"] == "action/0" + assert trace["last_condition"] == "condition/0" + assert "error" not in trace + assert trace["state"] == "stopped" + assert trace["timestamp"] + assert trace["trigger"] == "event 'test_event2'" + assert trace["unique_id"] == "moon"