ha-mcp-dev 6.3.1.dev137__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ha_mcp/__init__.py +51 -0
- ha_mcp/__main__.py +753 -0
- ha_mcp/_pypi_marker +0 -0
- ha_mcp/auth/__init__.py +10 -0
- ha_mcp/auth/consent_form.py +501 -0
- ha_mcp/auth/provider.py +786 -0
- ha_mcp/client/__init__.py +10 -0
- ha_mcp/client/rest_client.py +924 -0
- ha_mcp/client/websocket_client.py +656 -0
- ha_mcp/client/websocket_listener.py +340 -0
- ha_mcp/config.py +175 -0
- ha_mcp/errors.py +399 -0
- ha_mcp/py.typed +0 -0
- ha_mcp/resources/card_types.json +48 -0
- ha_mcp/resources/dashboard_guide.md +573 -0
- ha_mcp/server.py +189 -0
- ha_mcp/smoke_test.py +108 -0
- ha_mcp/tools/__init__.py +11 -0
- ha_mcp/tools/backup.py +457 -0
- ha_mcp/tools/device_control.py +687 -0
- ha_mcp/tools/enhanced.py +191 -0
- ha_mcp/tools/helpers.py +184 -0
- ha_mcp/tools/registry.py +199 -0
- ha_mcp/tools/smart_search.py +797 -0
- ha_mcp/tools/tools_addons.py +343 -0
- ha_mcp/tools/tools_areas.py +478 -0
- ha_mcp/tools/tools_blueprints.py +279 -0
- ha_mcp/tools/tools_bug_report.py +570 -0
- ha_mcp/tools/tools_calendar.py +391 -0
- ha_mcp/tools/tools_camera.py +149 -0
- ha_mcp/tools/tools_config_automations.py +508 -0
- ha_mcp/tools/tools_config_dashboards.py +1502 -0
- ha_mcp/tools/tools_config_entry_flow.py +223 -0
- ha_mcp/tools/tools_config_helpers.py +925 -0
- ha_mcp/tools/tools_config_info.py +258 -0
- ha_mcp/tools/tools_config_scripts.py +267 -0
- ha_mcp/tools/tools_entities.py +76 -0
- ha_mcp/tools/tools_filesystem.py +589 -0
- ha_mcp/tools/tools_groups.py +306 -0
- ha_mcp/tools/tools_hacs.py +787 -0
- ha_mcp/tools/tools_history.py +714 -0
- ha_mcp/tools/tools_integrations.py +297 -0
- ha_mcp/tools/tools_labels.py +713 -0
- ha_mcp/tools/tools_mcp_component.py +305 -0
- ha_mcp/tools/tools_registry.py +1115 -0
- ha_mcp/tools/tools_resources.py +622 -0
- ha_mcp/tools/tools_search.py +573 -0
- ha_mcp/tools/tools_service.py +211 -0
- ha_mcp/tools/tools_services.py +352 -0
- ha_mcp/tools/tools_system.py +363 -0
- ha_mcp/tools/tools_todo.py +530 -0
- ha_mcp/tools/tools_traces.py +496 -0
- ha_mcp/tools/tools_updates.py +476 -0
- ha_mcp/tools/tools_utility.py +519 -0
- ha_mcp/tools/tools_voice_assistant.py +345 -0
- ha_mcp/tools/tools_zones.py +387 -0
- ha_mcp/tools/util_helpers.py +199 -0
- ha_mcp/utils/__init__.py +30 -0
- ha_mcp/utils/domain_handlers.py +380 -0
- ha_mcp/utils/fuzzy_search.py +339 -0
- ha_mcp/utils/operation_manager.py +442 -0
- ha_mcp/utils/usage_logger.py +290 -0
- ha_mcp_dev-6.3.1.dev137.dist-info/METADATA +229 -0
- ha_mcp_dev-6.3.1.dev137.dist-info/RECORD +71 -0
- ha_mcp_dev-6.3.1.dev137.dist-info/WHEEL +5 -0
- ha_mcp_dev-6.3.1.dev137.dist-info/entry_points.txt +7 -0
- ha_mcp_dev-6.3.1.dev137.dist-info/licenses/LICENSE +21 -0
- ha_mcp_dev-6.3.1.dev137.dist-info/top_level.txt +2 -0
- tests/__init__.py +1 -0
- tests/test_constants.py +15 -0
- tests/test_env_manager.py +336 -0
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Trace retrieval tools for debugging Home Assistant automations and scripts.
|
|
3
|
+
|
|
4
|
+
This module provides tools for retrieving execution traces from Home Assistant
|
|
5
|
+
to help debug automation and script issues.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Annotated, Any
|
|
10
|
+
|
|
11
|
+
from pydantic import Field
|
|
12
|
+
|
|
13
|
+
from ..client.websocket_client import HomeAssistantWebSocketClient
|
|
14
|
+
from .helpers import get_connected_ws_client, log_tool_usage
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def register_trace_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
20
|
+
"""Register Home Assistant trace debugging tools."""
|
|
21
|
+
|
|
22
|
+
@mcp.tool(annotations={"idempotentHint": True, "readOnlyHint": True, "tags": ["trace"], "title": "Get Automation Traces"})
|
|
23
|
+
@log_tool_usage
|
|
24
|
+
async def ha_get_automation_traces(
|
|
25
|
+
automation_id: Annotated[
|
|
26
|
+
str,
|
|
27
|
+
Field(
|
|
28
|
+
description="Automation or script entity_id (e.g., 'automation.motion_light' or 'script.morning_routine')"
|
|
29
|
+
),
|
|
30
|
+
],
|
|
31
|
+
run_id: Annotated[
|
|
32
|
+
str | None,
|
|
33
|
+
Field(
|
|
34
|
+
description="Specific trace run_id to retrieve detailed trace. Omit to list recent traces.",
|
|
35
|
+
default=None,
|
|
36
|
+
),
|
|
37
|
+
] = None,
|
|
38
|
+
limit: Annotated[
|
|
39
|
+
int,
|
|
40
|
+
Field(
|
|
41
|
+
description="Maximum number of traces to return when listing (default: 10, max: 50)",
|
|
42
|
+
default=10,
|
|
43
|
+
ge=1,
|
|
44
|
+
le=50,
|
|
45
|
+
),
|
|
46
|
+
] = 10,
|
|
47
|
+
) -> dict[str, Any]:
|
|
48
|
+
"""
|
|
49
|
+
Retrieve execution traces for automations and scripts to debug issues.
|
|
50
|
+
|
|
51
|
+
Traces show what happened during automation/script runs:
|
|
52
|
+
- What triggered the automation
|
|
53
|
+
- Which conditions passed or failed
|
|
54
|
+
- What actions were executed
|
|
55
|
+
- Any errors that occurred
|
|
56
|
+
- Variable values during execution
|
|
57
|
+
|
|
58
|
+
USAGE MODES:
|
|
59
|
+
|
|
60
|
+
1. List recent traces (omit run_id):
|
|
61
|
+
ha_get_automation_traces("automation.motion_light")
|
|
62
|
+
Returns a summary of recent execution runs with timestamps, triggers, and status.
|
|
63
|
+
|
|
64
|
+
2. Get detailed trace (provide run_id):
|
|
65
|
+
ha_get_automation_traces("automation.motion_light", run_id="1705312800.123456")
|
|
66
|
+
Returns full execution details including trigger info, condition results,
|
|
67
|
+
action trace with timing, and context variables.
|
|
68
|
+
|
|
69
|
+
DEBUGGING EXAMPLES:
|
|
70
|
+
|
|
71
|
+
Automation not triggering:
|
|
72
|
+
- Check if traces exist (automation may not be triggered)
|
|
73
|
+
- Look at trigger info to see what event was received
|
|
74
|
+
|
|
75
|
+
Automation runs but conditions fail:
|
|
76
|
+
- Get detailed trace to see condition_results
|
|
77
|
+
- Each condition shows whether it passed (true) or failed (false)
|
|
78
|
+
|
|
79
|
+
Unexpected behavior in actions:
|
|
80
|
+
- Get detailed trace to see action_trace
|
|
81
|
+
- Shows each action step with result and any errors
|
|
82
|
+
- For 'choose' actions, shows which branch was taken
|
|
83
|
+
|
|
84
|
+
Template debugging:
|
|
85
|
+
- Detailed trace shows evaluated template values in context
|
|
86
|
+
- Trigger variables available under trigger_variables
|
|
87
|
+
|
|
88
|
+
NOTES:
|
|
89
|
+
- Traces are stored for a limited time by Home Assistant
|
|
90
|
+
- Works for both automations and scripts (use full entity_id)
|
|
91
|
+
- The 'state' field shows: 'stopped' (completed), 'running', or error state
|
|
92
|
+
"""
|
|
93
|
+
try:
|
|
94
|
+
# Determine domain from entity_id
|
|
95
|
+
if automation_id.startswith("automation."):
|
|
96
|
+
domain = "automation"
|
|
97
|
+
elif automation_id.startswith("script."):
|
|
98
|
+
domain = "script"
|
|
99
|
+
else:
|
|
100
|
+
return {
|
|
101
|
+
"success": False,
|
|
102
|
+
"error": f"Invalid entity_id format: {automation_id}",
|
|
103
|
+
"hint": "Entity ID must start with 'automation.' or 'script.'",
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# Extract the object_id (part after the domain) as fallback
|
|
107
|
+
object_id = automation_id.split(".", 1)[1]
|
|
108
|
+
|
|
109
|
+
# Connect to WebSocket
|
|
110
|
+
ws_client, error = await get_connected_ws_client(
|
|
111
|
+
client.base_url, client.token
|
|
112
|
+
)
|
|
113
|
+
if error or ws_client is None:
|
|
114
|
+
return error or {
|
|
115
|
+
"success": False,
|
|
116
|
+
"error": "Failed to connect to WebSocket",
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
# Home Assistant stores traces by unique_id, not entity_id.
|
|
121
|
+
# We need to resolve entity_id -> unique_id via entity registry.
|
|
122
|
+
item_id = await _resolve_trace_item_id(
|
|
123
|
+
ws_client, automation_id, object_id
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
if run_id:
|
|
127
|
+
# Get specific trace details
|
|
128
|
+
result = await ws_client.send_command(
|
|
129
|
+
"trace/get",
|
|
130
|
+
domain=domain,
|
|
131
|
+
item_id=item_id,
|
|
132
|
+
run_id=run_id,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
if not result.get("success"):
|
|
136
|
+
return {
|
|
137
|
+
"success": False,
|
|
138
|
+
"automation_id": automation_id,
|
|
139
|
+
"run_id": run_id,
|
|
140
|
+
"error": result.get("error", "Failed to retrieve trace"),
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
trace_data = result.get("result", {})
|
|
144
|
+
return _format_detailed_trace(automation_id, run_id, trace_data)
|
|
145
|
+
else:
|
|
146
|
+
# List recent traces
|
|
147
|
+
result = await ws_client.send_command(
|
|
148
|
+
"trace/list",
|
|
149
|
+
domain=domain,
|
|
150
|
+
item_id=item_id,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
if not result.get("success"):
|
|
154
|
+
return {
|
|
155
|
+
"success": False,
|
|
156
|
+
"automation_id": automation_id,
|
|
157
|
+
"error": result.get("error", "Failed to list traces"),
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
traces_data = result.get("result", [])
|
|
161
|
+
|
|
162
|
+
# If traces are empty, gather diagnostic information
|
|
163
|
+
if not traces_data:
|
|
164
|
+
diagnostics = await _gather_diagnostics(
|
|
165
|
+
ws_client, client, automation_id, domain
|
|
166
|
+
)
|
|
167
|
+
return _format_trace_list(
|
|
168
|
+
automation_id, traces_data, limit, diagnostics
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
return _format_trace_list(automation_id, traces_data, limit)
|
|
172
|
+
|
|
173
|
+
finally:
|
|
174
|
+
await ws_client.disconnect()
|
|
175
|
+
|
|
176
|
+
except Exception as e:
|
|
177
|
+
logger.error(f"Error getting traces for {automation_id}: {e}")
|
|
178
|
+
return {
|
|
179
|
+
"success": False,
|
|
180
|
+
"automation_id": automation_id,
|
|
181
|
+
"error": str(e),
|
|
182
|
+
"suggestions": [
|
|
183
|
+
"Verify the automation/script entity_id exists",
|
|
184
|
+
"Check if traces are available (automation must have run recently)",
|
|
185
|
+
"Ensure Home Assistant connection is working",
|
|
186
|
+
],
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
async def _resolve_trace_item_id(
|
|
191
|
+
ws_client: Any, entity_id: str, fallback_object_id: str
|
|
192
|
+
) -> str:
|
|
193
|
+
"""
|
|
194
|
+
Resolve entity_id to the unique_id used for trace storage.
|
|
195
|
+
|
|
196
|
+
Home Assistant stores traces using the automation/script's unique_id,
|
|
197
|
+
not the entity_id. This function looks up the unique_id from the
|
|
198
|
+
entity registry and falls back to object_id if not found.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
ws_client: Connected WebSocket client
|
|
202
|
+
entity_id: Full entity_id (e.g., 'automation.morning_routine')
|
|
203
|
+
fallback_object_id: Object ID to use if unique_id lookup fails
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
The unique_id for trace lookup, or fallback_object_id
|
|
207
|
+
"""
|
|
208
|
+
try:
|
|
209
|
+
# Query entity registry to get unique_id
|
|
210
|
+
result = await ws_client.send_command(
|
|
211
|
+
"config/entity_registry/get",
|
|
212
|
+
entity_id=entity_id,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
if result.get("success") and result.get("result"):
|
|
216
|
+
unique_id = result["result"].get("unique_id")
|
|
217
|
+
if unique_id:
|
|
218
|
+
logger.debug(
|
|
219
|
+
f"Resolved {entity_id} to unique_id: {unique_id}"
|
|
220
|
+
)
|
|
221
|
+
return unique_id
|
|
222
|
+
|
|
223
|
+
# Fallback to object_id if no unique_id found
|
|
224
|
+
logger.debug(
|
|
225
|
+
f"No unique_id found for {entity_id}, using object_id: {fallback_object_id}"
|
|
226
|
+
)
|
|
227
|
+
return fallback_object_id
|
|
228
|
+
|
|
229
|
+
except Exception as e:
|
|
230
|
+
# On any error, fall back to object_id
|
|
231
|
+
logger.warning(
|
|
232
|
+
f"Failed to resolve unique_id for {entity_id}: {e}, "
|
|
233
|
+
f"using object_id: {fallback_object_id}"
|
|
234
|
+
)
|
|
235
|
+
return fallback_object_id
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
async def _gather_diagnostics(
|
|
239
|
+
ws_client: HomeAssistantWebSocketClient,
|
|
240
|
+
client: Any,
|
|
241
|
+
automation_id: str,
|
|
242
|
+
domain: str,
|
|
243
|
+
) -> dict[str, Any]:
|
|
244
|
+
"""
|
|
245
|
+
Gather diagnostic information when traces are empty.
|
|
246
|
+
|
|
247
|
+
This helps users understand why there are no traces available for
|
|
248
|
+
an automation or script.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
ws_client: Connected WebSocket client
|
|
252
|
+
client: REST API client
|
|
253
|
+
automation_id: Full entity_id (e.g., 'automation.motion_light')
|
|
254
|
+
domain: Either 'automation' or 'script'
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
Dictionary containing diagnostic information:
|
|
258
|
+
- automation_exists: Whether the entity exists
|
|
259
|
+
- automation_enabled: Whether the automation is enabled (on/off state)
|
|
260
|
+
- trace_storage_enabled: Whether trace storage is enabled for this item
|
|
261
|
+
- last_triggered: Last trigger timestamp if available
|
|
262
|
+
- suggestion: Helpful hint based on the diagnostics
|
|
263
|
+
"""
|
|
264
|
+
diagnostics: dict[str, Any] = {
|
|
265
|
+
"automation_exists": False,
|
|
266
|
+
"automation_enabled": False,
|
|
267
|
+
"trace_storage_enabled": True, # Default assumption
|
|
268
|
+
"last_triggered": None,
|
|
269
|
+
"suggestion": "",
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
try:
|
|
273
|
+
# Get entity state to check existence and enabled status
|
|
274
|
+
entity_state = await client.get_entity_state(automation_id)
|
|
275
|
+
|
|
276
|
+
if entity_state:
|
|
277
|
+
diagnostics["automation_exists"] = True
|
|
278
|
+
|
|
279
|
+
# Check if enabled (state is 'on' for automations, 'off' is disabled)
|
|
280
|
+
state = entity_state.get("state", "unknown")
|
|
281
|
+
diagnostics["automation_enabled"] = state == "on"
|
|
282
|
+
|
|
283
|
+
# Get last_triggered from attributes
|
|
284
|
+
attributes = entity_state.get("attributes", {})
|
|
285
|
+
last_triggered = attributes.get("last_triggered")
|
|
286
|
+
if last_triggered:
|
|
287
|
+
diagnostics["last_triggered"] = last_triggered
|
|
288
|
+
|
|
289
|
+
# Check if tracing is stored - only for automations
|
|
290
|
+
# (scripts always store traces when enabled)
|
|
291
|
+
if domain == "automation":
|
|
292
|
+
# Try to get automation config to check stored_traces setting
|
|
293
|
+
try:
|
|
294
|
+
unique_id = attributes.get("id")
|
|
295
|
+
if unique_id:
|
|
296
|
+
config_result = await ws_client.send_command(
|
|
297
|
+
"automation/config",
|
|
298
|
+
entity_id=automation_id,
|
|
299
|
+
)
|
|
300
|
+
if config_result.get("success"):
|
|
301
|
+
config = config_result.get("result", {})
|
|
302
|
+
# stored_traces defaults to True if not specified
|
|
303
|
+
stored_traces = config.get("stored_traces")
|
|
304
|
+
if stored_traces is not None and stored_traces <= 0:
|
|
305
|
+
diagnostics["trace_storage_enabled"] = False
|
|
306
|
+
except Exception as e:
|
|
307
|
+
logger.debug(f"Could not get automation config: {e}")
|
|
308
|
+
|
|
309
|
+
# Generate suggestion based on diagnostics
|
|
310
|
+
suggestions = []
|
|
311
|
+
|
|
312
|
+
if not diagnostics["automation_enabled"]:
|
|
313
|
+
suggestions.append(
|
|
314
|
+
f"The {domain} is currently disabled (state: off). "
|
|
315
|
+
"Enable it to start recording traces."
|
|
316
|
+
)
|
|
317
|
+
elif diagnostics["last_triggered"] is None:
|
|
318
|
+
suggestions.append(
|
|
319
|
+
f"The {domain} has never been triggered. "
|
|
320
|
+
"Wait for it to trigger or manually trigger it to generate traces."
|
|
321
|
+
)
|
|
322
|
+
elif not diagnostics["trace_storage_enabled"]:
|
|
323
|
+
suggestions.append(
|
|
324
|
+
"Trace storage is disabled for this automation. "
|
|
325
|
+
"Set 'stored_traces' to a positive number in the automation config."
|
|
326
|
+
)
|
|
327
|
+
else:
|
|
328
|
+
suggestions.append(
|
|
329
|
+
"Traces may have been cleared or expired. "
|
|
330
|
+
"Home Assistant only keeps a limited number of recent traces."
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
diagnostics["suggestion"] = " ".join(suggestions)
|
|
334
|
+
|
|
335
|
+
except Exception as e:
|
|
336
|
+
# Entity doesn't exist or error occurred
|
|
337
|
+
logger.debug(f"Error getting entity state for diagnostics: {e}")
|
|
338
|
+
diagnostics["suggestion"] = (
|
|
339
|
+
f"Could not find {automation_id}. "
|
|
340
|
+
"Verify the entity_id is correct using ha_search_entities()."
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
return diagnostics
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _format_trace_list(
|
|
347
|
+
automation_id: str,
|
|
348
|
+
traces: list[dict[str, Any]],
|
|
349
|
+
limit: int,
|
|
350
|
+
diagnostics: dict[str, Any] | None = None,
|
|
351
|
+
) -> dict[str, Any]:
|
|
352
|
+
"""Format trace list for AI consumption.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
automation_id: The automation or script entity_id
|
|
356
|
+
traces: List of trace data from Home Assistant
|
|
357
|
+
limit: Maximum number of traces to include
|
|
358
|
+
diagnostics: Optional diagnostic information when traces are empty
|
|
359
|
+
"""
|
|
360
|
+
formatted_traces = []
|
|
361
|
+
|
|
362
|
+
for trace in traces[:limit]:
|
|
363
|
+
# Extract key information from trace
|
|
364
|
+
trace_info: dict[str, Any] = {
|
|
365
|
+
"run_id": trace.get("run_id"),
|
|
366
|
+
"timestamp": trace.get("timestamp"),
|
|
367
|
+
"state": trace.get("state"),
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
# Extract trigger description if available
|
|
371
|
+
trigger_str = trace.get("trigger")
|
|
372
|
+
if trigger_str:
|
|
373
|
+
trace_info["trigger"] = trigger_str
|
|
374
|
+
|
|
375
|
+
# Check for errors
|
|
376
|
+
error = trace.get("error")
|
|
377
|
+
if error:
|
|
378
|
+
trace_info["error"] = error
|
|
379
|
+
|
|
380
|
+
# Add script-specific execution duration
|
|
381
|
+
if "script_execution" in trace:
|
|
382
|
+
trace_info["execution"] = trace.get("script_execution")
|
|
383
|
+
|
|
384
|
+
formatted_traces.append(trace_info)
|
|
385
|
+
|
|
386
|
+
result: dict[str, Any] = {
|
|
387
|
+
"success": True,
|
|
388
|
+
"automation_id": automation_id,
|
|
389
|
+
"trace_count": len(formatted_traces),
|
|
390
|
+
"total_available": len(traces),
|
|
391
|
+
"traces": formatted_traces,
|
|
392
|
+
"hint": "Use run_id with this tool to get detailed trace information",
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
# Include diagnostics when traces are empty
|
|
396
|
+
if diagnostics is not None and len(traces) == 0:
|
|
397
|
+
result["diagnostics"] = diagnostics
|
|
398
|
+
|
|
399
|
+
return result
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _format_detailed_trace(
|
|
403
|
+
automation_id: str, run_id: str, trace: dict[str, Any]
|
|
404
|
+
) -> dict[str, Any]:
|
|
405
|
+
"""Format detailed trace for AI consumption."""
|
|
406
|
+
result: dict[str, Any] = {
|
|
407
|
+
"success": True,
|
|
408
|
+
"automation_id": automation_id,
|
|
409
|
+
"run_id": run_id,
|
|
410
|
+
"timestamp": trace.get("timestamp"),
|
|
411
|
+
"state": trace.get("state"),
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
# Extract trigger information
|
|
415
|
+
trigger_trace = trace.get("trace", {}).get("trigger", [])
|
|
416
|
+
if trigger_trace:
|
|
417
|
+
trigger_step = trigger_trace[0]
|
|
418
|
+
trigger_vars = trigger_step.get("variables", {}).get("trigger", {})
|
|
419
|
+
result["trigger"] = {
|
|
420
|
+
"platform": trigger_vars.get("platform"),
|
|
421
|
+
"description": trigger_vars.get("description"),
|
|
422
|
+
}
|
|
423
|
+
# Add state change details if present (use safe dictionary access)
|
|
424
|
+
if "to_state" in trigger_vars:
|
|
425
|
+
result["trigger"]["to_state"] = trigger_vars.get("to_state", {}).get("state")
|
|
426
|
+
if "from_state" in trigger_vars:
|
|
427
|
+
result["trigger"]["from_state"] = trigger_vars.get("from_state", {}).get("state")
|
|
428
|
+
if "entity_id" in trigger_vars:
|
|
429
|
+
result["trigger"]["entity_id"] = trigger_vars["entity_id"]
|
|
430
|
+
|
|
431
|
+
# Extract condition results
|
|
432
|
+
condition_trace = trace.get("trace", {}).get("condition", [])
|
|
433
|
+
if condition_trace:
|
|
434
|
+
condition_results = []
|
|
435
|
+
for cond in condition_trace:
|
|
436
|
+
cond_result = {
|
|
437
|
+
"result": cond.get("result", {}).get("result"),
|
|
438
|
+
}
|
|
439
|
+
# Try to get condition type from the path
|
|
440
|
+
if "path" in cond:
|
|
441
|
+
cond_result["path"] = cond["path"]
|
|
442
|
+
condition_results.append(cond_result)
|
|
443
|
+
result["condition_results"] = condition_results
|
|
444
|
+
|
|
445
|
+
# Extract action trace
|
|
446
|
+
action_trace = trace.get("trace", {}).get("action", [])
|
|
447
|
+
if action_trace:
|
|
448
|
+
action_results = []
|
|
449
|
+
for action in action_trace:
|
|
450
|
+
action_info: dict[str, Any] = {
|
|
451
|
+
"path": action.get("path"),
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
# Add timing information
|
|
455
|
+
if "timestamp" in action:
|
|
456
|
+
action_info["timestamp"] = action["timestamp"]
|
|
457
|
+
|
|
458
|
+
# Extract action result
|
|
459
|
+
action_result = action.get("result", {})
|
|
460
|
+
if action_result:
|
|
461
|
+
action_info["result"] = action_result
|
|
462
|
+
|
|
463
|
+
# Check for errors
|
|
464
|
+
if "error" in action:
|
|
465
|
+
action_info["error"] = action["error"]
|
|
466
|
+
|
|
467
|
+
# Extract variables if they contain useful debugging info
|
|
468
|
+
variables = action.get("variables", {})
|
|
469
|
+
if variables and "trigger" not in variables: # Skip trigger vars (already shown)
|
|
470
|
+
# Only include non-empty variable sets
|
|
471
|
+
useful_vars = {k: v for k, v in variables.items() if v is not None}
|
|
472
|
+
if useful_vars:
|
|
473
|
+
action_info["variables"] = useful_vars
|
|
474
|
+
|
|
475
|
+
action_results.append(action_info)
|
|
476
|
+
|
|
477
|
+
result["action_trace"] = action_results
|
|
478
|
+
|
|
479
|
+
# Add context with trigger variables for template debugging
|
|
480
|
+
config = trace.get("config", {})
|
|
481
|
+
if config:
|
|
482
|
+
# Include config summary for context
|
|
483
|
+
result["config_summary"] = {
|
|
484
|
+
"alias": config.get("alias"),
|
|
485
|
+
"mode": config.get("mode", "single"),
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
# Check for overall error
|
|
489
|
+
if trace.get("error"):
|
|
490
|
+
result["error"] = trace["error"]
|
|
491
|
+
|
|
492
|
+
# Add script execution info if present
|
|
493
|
+
if trace.get("script_execution"):
|
|
494
|
+
result["script_execution"] = trace["script_execution"]
|
|
495
|
+
|
|
496
|
+
return result
|