Skip to content

Commit 7ceba5b

Browse files
julienldclaude
andauthored
feat: add diagnostic mode for empty automation traces (#235)
* feat: add diagnostic mode for empty automation traces When ha_get_automation_traces returns no traces, the tool now includes diagnostic information to help users understand why traces are missing: - automation_exists: Whether the automation entity exists - automation_enabled: Whether the automation is enabled (on/off) - trace_storage_enabled: Whether trace storage is enabled - last_triggered: Timestamp of last trigger - suggestion: Actionable hint based on the diagnostics This helps users troubleshoot common issues like disabled automations, automations that have never triggered, or expired/cleared traces. Closes #212 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * test: add E2E tests for automation/script trace retrieval Add comprehensive E2E tests to verify ha_get_automation_traces tool: - test_automation_trace_after_trigger: Create, trigger, verify non-empty traces - test_empty_traces_with_diagnostics: Verify diagnostics when traces are empty - test_script_traces: Verify trace retrieval works for scripts too Tests validate: - Traces are recorded after automation.trigger service call - Trace structure includes run_id, timestamp, state - Detailed trace retrieval by run_id - Diagnostic mode for never-triggered automations 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: correct parameter format for ha_config_set_script in trace tests The ha_config_set_script tool expects {"script_id": ..., "config": ...} format, not the config directly. Also fixed variable naming to avoid confusion between script_id_base and script_entity_id. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: use correct CleanupTracker.track() method signature CleanupTracker has track(entity_type, entity_id) not track_automation() or track_script() methods. Updated all calls to use the correct API. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: use correct ha_call_service parameter format The ha_call_service tool uses entity_id as a direct parameter, not nested inside a target dict. Fixed both automation trigger and script turn_on calls in trace tests. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent c68b5bf commit 7ceba5b

3 files changed

Lines changed: 724 additions & 3 deletions

File tree

src/ha_mcp/tools/tools_traces.py

Lines changed: 138 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from pydantic import Field
1212

13+
from ..client.websocket_client import HomeAssistantWebSocketClient
1314
from .helpers import get_connected_ws_client, log_tool_usage
1415

1516
logger = logging.getLogger(__name__)
@@ -157,6 +158,16 @@ async def ha_get_automation_traces(
157158
}
158159

159160
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+
160171
return _format_trace_list(automation_id, traces_data, limit)
161172

162173
finally:
@@ -224,10 +235,128 @@ async def _resolve_trace_item_id(
224235
return fallback_object_id
225236

226237

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+
227346
def _format_trace_list(
228-
automation_id: str, traces: list[dict[str, Any]], limit: int
347+
automation_id: str,
348+
traces: list[dict[str, Any]],
349+
limit: int,
350+
diagnostics: dict[str, Any] | None = None,
229351
) -> dict[str, Any]:
230-
"""Format trace list for AI consumption."""
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+
"""
231360
formatted_traces = []
232361

233362
for trace in traces[:limit]:
@@ -254,7 +383,7 @@ def _format_trace_list(
254383

255384
formatted_traces.append(trace_info)
256385

257-
return {
386+
result: dict[str, Any] = {
258387
"success": True,
259388
"automation_id": automation_id,
260389
"trace_count": len(formatted_traces),
@@ -263,6 +392,12 @@ def _format_trace_list(
263392
"hint": "Use run_id with this tool to get detailed trace information",
264393
}
265394

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+
266401

267402
def _format_detailed_trace(
268403
automation_id: str, run_id: str, trace: dict[str, Any]

0 commit comments

Comments
 (0)