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.
Files changed (71) hide show
  1. ha_mcp/__init__.py +51 -0
  2. ha_mcp/__main__.py +753 -0
  3. ha_mcp/_pypi_marker +0 -0
  4. ha_mcp/auth/__init__.py +10 -0
  5. ha_mcp/auth/consent_form.py +501 -0
  6. ha_mcp/auth/provider.py +786 -0
  7. ha_mcp/client/__init__.py +10 -0
  8. ha_mcp/client/rest_client.py +924 -0
  9. ha_mcp/client/websocket_client.py +656 -0
  10. ha_mcp/client/websocket_listener.py +340 -0
  11. ha_mcp/config.py +175 -0
  12. ha_mcp/errors.py +399 -0
  13. ha_mcp/py.typed +0 -0
  14. ha_mcp/resources/card_types.json +48 -0
  15. ha_mcp/resources/dashboard_guide.md +573 -0
  16. ha_mcp/server.py +189 -0
  17. ha_mcp/smoke_test.py +108 -0
  18. ha_mcp/tools/__init__.py +11 -0
  19. ha_mcp/tools/backup.py +457 -0
  20. ha_mcp/tools/device_control.py +687 -0
  21. ha_mcp/tools/enhanced.py +191 -0
  22. ha_mcp/tools/helpers.py +184 -0
  23. ha_mcp/tools/registry.py +199 -0
  24. ha_mcp/tools/smart_search.py +797 -0
  25. ha_mcp/tools/tools_addons.py +343 -0
  26. ha_mcp/tools/tools_areas.py +478 -0
  27. ha_mcp/tools/tools_blueprints.py +279 -0
  28. ha_mcp/tools/tools_bug_report.py +570 -0
  29. ha_mcp/tools/tools_calendar.py +391 -0
  30. ha_mcp/tools/tools_camera.py +149 -0
  31. ha_mcp/tools/tools_config_automations.py +508 -0
  32. ha_mcp/tools/tools_config_dashboards.py +1502 -0
  33. ha_mcp/tools/tools_config_entry_flow.py +223 -0
  34. ha_mcp/tools/tools_config_helpers.py +925 -0
  35. ha_mcp/tools/tools_config_info.py +258 -0
  36. ha_mcp/tools/tools_config_scripts.py +267 -0
  37. ha_mcp/tools/tools_entities.py +76 -0
  38. ha_mcp/tools/tools_filesystem.py +589 -0
  39. ha_mcp/tools/tools_groups.py +306 -0
  40. ha_mcp/tools/tools_hacs.py +787 -0
  41. ha_mcp/tools/tools_history.py +714 -0
  42. ha_mcp/tools/tools_integrations.py +297 -0
  43. ha_mcp/tools/tools_labels.py +713 -0
  44. ha_mcp/tools/tools_mcp_component.py +305 -0
  45. ha_mcp/tools/tools_registry.py +1115 -0
  46. ha_mcp/tools/tools_resources.py +622 -0
  47. ha_mcp/tools/tools_search.py +573 -0
  48. ha_mcp/tools/tools_service.py +211 -0
  49. ha_mcp/tools/tools_services.py +352 -0
  50. ha_mcp/tools/tools_system.py +363 -0
  51. ha_mcp/tools/tools_todo.py +530 -0
  52. ha_mcp/tools/tools_traces.py +496 -0
  53. ha_mcp/tools/tools_updates.py +476 -0
  54. ha_mcp/tools/tools_utility.py +519 -0
  55. ha_mcp/tools/tools_voice_assistant.py +345 -0
  56. ha_mcp/tools/tools_zones.py +387 -0
  57. ha_mcp/tools/util_helpers.py +199 -0
  58. ha_mcp/utils/__init__.py +30 -0
  59. ha_mcp/utils/domain_handlers.py +380 -0
  60. ha_mcp/utils/fuzzy_search.py +339 -0
  61. ha_mcp/utils/operation_manager.py +442 -0
  62. ha_mcp/utils/usage_logger.py +290 -0
  63. ha_mcp_dev-6.3.1.dev137.dist-info/METADATA +229 -0
  64. ha_mcp_dev-6.3.1.dev137.dist-info/RECORD +71 -0
  65. ha_mcp_dev-6.3.1.dev137.dist-info/WHEEL +5 -0
  66. ha_mcp_dev-6.3.1.dev137.dist-info/entry_points.txt +7 -0
  67. ha_mcp_dev-6.3.1.dev137.dist-info/licenses/LICENSE +21 -0
  68. ha_mcp_dev-6.3.1.dev137.dist-info/top_level.txt +2 -0
  69. tests/__init__.py +1 -0
  70. tests/test_constants.py +15 -0
  71. tests/test_env_manager.py +336 -0
@@ -0,0 +1,508 @@
1
+ """
2
+ Configuration management tools for Home Assistant automations.
3
+
4
+ This module provides tools for retrieving, creating, updating, and removing
5
+ Home Assistant automation configurations.
6
+ """
7
+
8
+ import logging
9
+ from typing import Annotated, Any, cast
10
+
11
+ from pydantic import Field
12
+
13
+ from ..errors import (
14
+ create_config_error,
15
+ create_resource_not_found_error,
16
+ create_validation_error,
17
+ )
18
+ from .helpers import exception_to_structured_error, log_tool_usage
19
+ from .util_helpers import parse_json_param
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ def _normalize_automation_config(
25
+ config: Any, parent_key: str | None = None, in_choose_or_if: bool = False
26
+ ) -> Any:
27
+ """
28
+ Recursively normalize automation config field names to HA API format.
29
+
30
+ Home Assistant accepts both singular ('trigger', 'action', 'condition')
31
+ and plural ('triggers', 'actions', 'conditions') field names in YAML,
32
+ but the API expects singular forms at the root level.
33
+
34
+ IMPORTANT: Inside 'choose' and 'if' action blocks, the 'conditions' key
35
+ (plural) is required by the HA schema and should NOT be normalized to
36
+ 'condition' (singular).
37
+
38
+ IMPORTANT: Inside compound condition blocks ('or', 'and', 'not'), the
39
+ 'conditions' key (plural) is required and should NOT be normalized to
40
+ 'condition' (singular).
41
+
42
+ Args:
43
+ config: Automation configuration (dict, list, or primitive)
44
+ parent_key: The parent dictionary key (for context tracking)
45
+ in_choose_or_if: Whether we're inside a choose/if option that requires
46
+ 'conditions' (plural) to remain unchanged
47
+
48
+ Returns:
49
+ Normalized configuration with singular field names at root level,
50
+ but preserving 'conditions' (plural) inside choose/if blocks and
51
+ compound condition blocks (or/and/not)
52
+ """
53
+ # Handle lists - recursively process each item
54
+ if isinstance(config, list):
55
+ # If parent is 'choose' or 'if', items are options that need 'conditions' preserved
56
+ is_option_list = parent_key in ("choose", "if")
57
+ return [
58
+ _normalize_automation_config(item, parent_key, is_option_list)
59
+ for item in config
60
+ ]
61
+
62
+ # Handle primitives (strings, numbers, etc.)
63
+ if not isinstance(config, dict):
64
+ return config
65
+
66
+ # Process dictionary
67
+ normalized = config.copy()
68
+
69
+ # Check if this dict is a compound condition block (or/and/not)
70
+ # that needs its nested 'conditions' key preserved
71
+ is_compound_condition_block = normalized.get("condition") in ("or", "and", "not")
72
+
73
+ # Map plural field names to singular (HA API format)
74
+ # EXCEPT 'conditions' when inside choose/if blocks OR in compound condition blocks
75
+ field_mappings = {
76
+ "triggers": "trigger",
77
+ "actions": "action",
78
+ # Note: 'sequence' is already singular, but some users might use 'sequences'
79
+ "sequences": "sequence",
80
+ }
81
+
82
+ # Only add 'conditions' mapping if NOT inside a choose/if option
83
+ # AND NOT a compound condition block (or/and/not)
84
+ if not in_choose_or_if and not is_compound_condition_block:
85
+ field_mappings["conditions"] = "condition"
86
+
87
+ # Apply field mapping to current level
88
+ for plural, singular in field_mappings.items():
89
+ if plural in normalized and singular not in normalized:
90
+ normalized[singular] = normalized.pop(plural)
91
+ elif plural in normalized and singular in normalized:
92
+ # Both exist - prefer singular, remove plural
93
+ del normalized[plural]
94
+
95
+ # Recursively process all values in the dictionary
96
+ for key, value in normalized.items():
97
+ normalized[key] = _normalize_automation_config(value, key)
98
+
99
+ return normalized
100
+
101
+
102
+ def _normalize_trigger_keys(triggers: list[dict[str, Any]]) -> list[dict[str, Any]]:
103
+ """
104
+ Normalize trigger objects for round-trip compatibility.
105
+
106
+ Home Assistant GET API returns triggers with 'trigger' key for the platform type,
107
+ but the SET API expects 'platform' key. This function converts between formats.
108
+
109
+ Args:
110
+ triggers: List of trigger configuration dicts
111
+
112
+ Returns:
113
+ List of triggers with 'platform' key instead of 'trigger' key
114
+ """
115
+ normalized_triggers = []
116
+ for trigger in triggers:
117
+ normalized_trigger = trigger.copy()
118
+ # Convert 'trigger' key to 'platform' if present and 'platform' is not
119
+ if "trigger" in normalized_trigger and "platform" not in normalized_trigger:
120
+ normalized_trigger["platform"] = normalized_trigger.pop("trigger")
121
+ normalized_triggers.append(normalized_trigger)
122
+ return normalized_triggers
123
+
124
+
125
+ def _normalize_config_for_roundtrip(config: dict[str, Any]) -> dict[str, Any]:
126
+ """
127
+ Normalize automation config from GET response for direct use in SET.
128
+
129
+ This ensures a config retrieved via ha_config_get_automation can be
130
+ directly passed to ha_config_set_automation without modification.
131
+
132
+ Transformations:
133
+ 1. Field names: triggers -> trigger, actions -> action, conditions -> condition
134
+ 2. Trigger keys: trigger -> platform (inside each trigger object)
135
+
136
+ Args:
137
+ config: Raw automation configuration from HA API
138
+
139
+ Returns:
140
+ Normalized configuration compatible with SET API
141
+ """
142
+ # First normalize field names (plural -> singular)
143
+ normalized = _normalize_automation_config(config)
144
+
145
+ # Then normalize trigger keys (trigger -> platform)
146
+ if "trigger" in normalized and isinstance(normalized["trigger"], list):
147
+ normalized["trigger"] = _normalize_trigger_keys(normalized["trigger"])
148
+
149
+ return normalized
150
+
151
+
152
+ def _strip_empty_automation_fields(config: dict[str, Any]) -> dict[str, Any]:
153
+ """
154
+ Strip empty trigger/action/condition arrays from automation config.
155
+
156
+ Blueprint-based automations should not have trigger/action/condition fields
157
+ since these come from the blueprint itself. If empty arrays are present,
158
+ they override the blueprint's configuration and break the automation.
159
+
160
+ Args:
161
+ config: Automation configuration dict
162
+
163
+ Returns:
164
+ Configuration with empty trigger/action/condition arrays removed
165
+ """
166
+ cleaned = config.copy()
167
+
168
+ # Remove empty arrays for blueprint automations
169
+ for field in ["trigger", "action", "condition"]:
170
+ if field in cleaned and cleaned[field] == []:
171
+ del cleaned[field]
172
+
173
+ return cleaned
174
+
175
+
176
+ def register_config_automation_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
177
+ """Register Home Assistant automation configuration tools."""
178
+
179
+ @mcp.tool(
180
+ annotations={
181
+ "idempotentHint": True,
182
+ "readOnlyHint": True,
183
+ "tags": ["automation"],
184
+ "title": "Get Automation Config",
185
+ }
186
+ )
187
+ @log_tool_usage
188
+ async def ha_config_get_automation(
189
+ identifier: Annotated[
190
+ str,
191
+ Field(
192
+ description="Automation entity_id (e.g., 'automation.morning_routine') or unique_id"
193
+ ),
194
+ ],
195
+ ) -> dict[str, Any]:
196
+ """
197
+ Retrieve Home Assistant automation configuration.
198
+
199
+ Returns the complete configuration including triggers, conditions, actions, and mode settings.
200
+
201
+ EXAMPLES:
202
+ - Get automation: ha_config_get_automation("automation.morning_routine")
203
+ - Get by unique_id: ha_config_get_automation("my_unique_automation_id")
204
+
205
+ For comprehensive automation documentation, use: ha_get_domain_docs("automation")
206
+ """
207
+ try:
208
+ config_result = await client.get_automation_config(identifier)
209
+ # Normalize config for round-trip compatibility (GET → SET)
210
+ normalized_config = _normalize_config_for_roundtrip(config_result)
211
+ return {
212
+ "success": True,
213
+ "action": "get",
214
+ "identifier": identifier,
215
+ "config": normalized_config,
216
+ }
217
+ except Exception as e:
218
+ # Handle 404 errors gracefully (often used to verify deletion)
219
+ error_str = str(e)
220
+ if (
221
+ "404" in error_str
222
+ or "not found" in error_str.lower()
223
+ or "entity not found" in error_str.lower()
224
+ ):
225
+ logger.debug(
226
+ f"Automation {identifier} not found (expected for deletion verification)"
227
+ )
228
+ error_response = create_resource_not_found_error(
229
+ "Automation",
230
+ identifier,
231
+ details=f"Automation '{identifier}' does not exist in Home Assistant",
232
+ )
233
+ error_response["action"] = "get"
234
+ error_response["reason"] = "not_found"
235
+ return error_response
236
+
237
+ logger.error(f"Error getting automation: {e}")
238
+ error_response = exception_to_structured_error(
239
+ e,
240
+ context={"identifier": identifier, "action": "get"},
241
+ )
242
+ # Add automation-specific suggestions
243
+ if "error" in error_response and isinstance(error_response["error"], dict):
244
+ error_response["error"]["suggestions"] = [
245
+ "Verify automation exists using ha_search_entities(domain_filter='automation')",
246
+ "Check Home Assistant connection",
247
+ "Use ha_get_domain_docs('automation') for configuration help",
248
+ ]
249
+ return error_response
250
+
251
+ @mcp.tool(
252
+ annotations={
253
+ "destructiveHint": True,
254
+ "tags": ["automation"],
255
+ "title": "Create or Update Automation",
256
+ }
257
+ )
258
+ @log_tool_usage
259
+ async def ha_config_set_automation(
260
+ config: Annotated[
261
+ str | dict[str, Any],
262
+ Field(
263
+ description="Complete automation configuration with required fields: 'alias', 'trigger', 'action'. Optional: 'description', 'condition', 'mode', 'max', 'initial_state', 'variables'"
264
+ ),
265
+ ],
266
+ identifier: Annotated[
267
+ str | None,
268
+ Field(
269
+ description="Automation entity_id or unique_id for updates. Omit to create new automation with generated unique_id.",
270
+ default=None,
271
+ ),
272
+ ] = None,
273
+ ) -> dict[str, Any]:
274
+ """
275
+ Create or update a Home Assistant automation.
276
+
277
+ Creates a new automation (if identifier omitted) or updates existing automation with provided configuration.
278
+
279
+ AUTOMATION TYPES:
280
+
281
+ 1. Regular Automations - Define triggers and actions directly
282
+ 2. Blueprint Automations - Use pre-built templates with customizable inputs
283
+
284
+ REQUIRED FIELDS (Regular Automations):
285
+ - alias: Human-readable automation name
286
+ - trigger: List of trigger conditions (time, state, event, etc.)
287
+ - action: List of actions to execute
288
+
289
+ REQUIRED FIELDS (Blueprint Automations):
290
+ - alias: Human-readable automation name
291
+ - use_blueprint: Blueprint configuration
292
+ - path: Blueprint file path (e.g., "motion_light.yaml")
293
+ - input: Dictionary of input values for the blueprint
294
+
295
+ OPTIONAL CONFIG FIELDS (Regular Automations):
296
+ - description: Detailed description of the user's intent (RECOMMENDED: helps safely modify implementation later)
297
+ - condition: Additional conditions that must be met
298
+ - mode: 'single' (default), 'restart', 'queued', 'parallel'
299
+ - max: Maximum concurrent executions (for queued/parallel modes)
300
+ - initial_state: Whether automation starts enabled (true/false)
301
+ - variables: Variables for use in automation
302
+
303
+ BASIC EXAMPLES:
304
+
305
+ Simple time-based automation:
306
+ ha_config_set_automation({
307
+ "alias": "Morning Lights",
308
+ "description": "Turn on bedroom lights at 7 AM to help wake up",
309
+ "trigger": [{"platform": "time", "at": "07:00:00"}],
310
+ "action": [{"service": "light.turn_on", "target": {"area_id": "bedroom"}}]
311
+ })
312
+
313
+ Motion-activated lighting with condition:
314
+ ha_config_set_automation({
315
+ "alias": "Motion Light",
316
+ "trigger": [{"platform": "state", "entity_id": "binary_sensor.motion", "to": "on"}],
317
+ "condition": [{"condition": "sun", "after": "sunset"}],
318
+ "action": [
319
+ {"service": "light.turn_on", "target": {"entity_id": "light.hallway"}},
320
+ {"delay": {"minutes": 5}},
321
+ {"service": "light.turn_off", "target": {"entity_id": "light.hallway"}}
322
+ ],
323
+ "mode": "restart"
324
+ })
325
+
326
+ Update existing automation:
327
+ ha_config_set_automation(
328
+ identifier="automation.morning_routine",
329
+ config={
330
+ "alias": "Updated Morning Routine",
331
+ "trigger": [{"platform": "time", "at": "06:30:00"}],
332
+ "action": [
333
+ {"service": "light.turn_on", "target": {"area_id": "bedroom"}},
334
+ {"service": "climate.set_temperature", "target": {"entity_id": "climate.bedroom"}, "data": {"temperature": 22}}
335
+ ]
336
+ }
337
+ )
338
+
339
+ BLUEPRINT AUTOMATION EXAMPLES:
340
+
341
+ Create automation from blueprint:
342
+ ha_config_set_automation({
343
+ "alias": "Motion Light Kitchen",
344
+ "use_blueprint": {
345
+ "path": "homeassistant/motion_light.yaml",
346
+ "input": {
347
+ "motion_entity": "binary_sensor.kitchen_motion",
348
+ "light_target": {"entity_id": "light.kitchen"},
349
+ "no_motion_wait": 120
350
+ }
351
+ }
352
+ })
353
+
354
+ Update blueprint automation inputs:
355
+ ha_config_set_automation(
356
+ identifier="automation.motion_light_kitchen",
357
+ config={
358
+ "alias": "Motion Light Kitchen",
359
+ "use_blueprint": {
360
+ "path": "homeassistant/motion_light.yaml",
361
+ "input": {
362
+ "motion_entity": "binary_sensor.kitchen_motion",
363
+ "light_target": {"entity_id": "light.kitchen"},
364
+ "no_motion_wait": 300
365
+ }
366
+ }
367
+ }
368
+ })
369
+
370
+ PREFER NATIVE SOLUTIONS OVER TEMPLATES:
371
+ Before using template triggers/conditions/actions, check if a native option exists:
372
+ - Use `condition: state` with `state: [list]` instead of template for multiple states
373
+ - Use `condition: state` with `attribute:` instead of template for attribute checks
374
+ - Use `condition: numeric_state` instead of template for number comparisons
375
+ - Use `wait_for_trigger` instead of `wait_template` when waiting for state changes
376
+ - Use `choose` action instead of template-based service names
377
+
378
+ TRIGGER TYPES: time, time_pattern, sun, state, numeric_state, event, device, zone, template, and more
379
+ CONDITION TYPES: state, numeric_state, time, sun, template, device, zone, and more
380
+ ACTION TYPES: service calls, delays, wait_for_trigger, wait_template, if/then/else, choose, repeat, parallel
381
+
382
+ For comprehensive automation documentation with all trigger/condition/action types and advanced examples:
383
+ - Use: ha_get_domain_docs("automation")
384
+ - Or visit: https://www.home-assistant.io/docs/automation/
385
+
386
+ TROUBLESHOOTING:
387
+ - Use ha_get_state() to verify entity_ids exist
388
+ - Use ha_search_entities() to find correct entity_ids
389
+ - Use ha_eval_template() to test Jinja2 templates before using in automations
390
+ - Use ha_search_entities(domain_filter='automation') to find existing automations
391
+ """
392
+ try:
393
+ # Parse JSON config if provided as string
394
+ try:
395
+ parsed_config = parse_json_param(config, "config")
396
+ except ValueError as e:
397
+ return create_validation_error(
398
+ f"Invalid config parameter: {e}",
399
+ parameter="config",
400
+ invalid_json=True,
401
+ )
402
+
403
+ # Ensure config is a dict
404
+ if parsed_config is None or not isinstance(parsed_config, dict):
405
+ return create_validation_error(
406
+ "Config parameter must be a JSON object",
407
+ parameter="config",
408
+ details=f"Received type: {type(parsed_config).__name__}",
409
+ )
410
+
411
+ config_dict = cast(dict[str, Any], parsed_config)
412
+
413
+ # Normalize field names (triggers -> trigger, actions -> action, etc.)
414
+ config_dict = _normalize_automation_config(config_dict)
415
+
416
+ # Validate required fields based on automation type
417
+ # Blueprint automations only need alias, regular automations need trigger and action
418
+ if "use_blueprint" in config_dict:
419
+ required_fields = ["alias"]
420
+ # Strip empty trigger/action/condition arrays that would override blueprint
421
+ config_dict = _strip_empty_automation_fields(config_dict)
422
+ else:
423
+ required_fields = ["alias", "trigger", "action"]
424
+
425
+ missing_fields = [f for f in required_fields if f not in config_dict]
426
+ if missing_fields:
427
+ return create_config_error(
428
+ f"Missing required fields: {', '.join(missing_fields)}",
429
+ identifier=identifier,
430
+ missing_fields=missing_fields,
431
+ )
432
+
433
+ result = await client.upsert_automation_config(config_dict, identifier)
434
+ return {
435
+ "success": True,
436
+ **result,
437
+ "config_provided": config_dict,
438
+ }
439
+
440
+ except Exception as e:
441
+ logger.error(f"Error upserting automation: {e}")
442
+ error_response = exception_to_structured_error(
443
+ e,
444
+ context={"identifier": identifier},
445
+ )
446
+ # Add automation-specific suggestions
447
+ if "error" in error_response and isinstance(error_response["error"], dict):
448
+ error_response["error"]["suggestions"] = [
449
+ "Check automation configuration format",
450
+ "Ensure required fields: alias, trigger, action",
451
+ "Use entity_id format: automation.morning_routine or unique_id",
452
+ "Use ha_search_entities(domain_filter='automation') to find automations",
453
+ "Use ha_get_domain_docs('automation') for comprehensive configuration help",
454
+ ]
455
+ return error_response
456
+
457
+ @mcp.tool(
458
+ annotations={
459
+ "destructiveHint": True,
460
+ "idempotentHint": True,
461
+ "tags": ["automation"],
462
+ "title": "Remove Automation",
463
+ }
464
+ )
465
+ @log_tool_usage
466
+ async def ha_config_remove_automation(
467
+ identifier: Annotated[
468
+ str,
469
+ Field(
470
+ description="Automation entity_id (e.g., 'automation.old_automation') or unique_id to delete"
471
+ ),
472
+ ],
473
+ ) -> dict[str, Any]:
474
+ """
475
+ Delete a Home Assistant automation.
476
+
477
+ EXAMPLES:
478
+ - Delete automation: ha_config_remove_automation("automation.old_automation")
479
+ - Delete by unique_id: ha_config_remove_automation("my_unique_id")
480
+
481
+ **WARNING:** Deleting an automation removes it permanently from your Home Assistant configuration.
482
+ """
483
+ try:
484
+ result = await client.delete_automation_config(identifier)
485
+ return {"success": True, "action": "delete", **result}
486
+ except Exception as e:
487
+ logger.error(f"Error deleting automation: {e}")
488
+ error_str = str(e).lower()
489
+ if "404" in error_str or "not found" in error_str:
490
+ error_response = create_resource_not_found_error(
491
+ "Automation",
492
+ identifier,
493
+ details=f"Automation '{identifier}' does not exist",
494
+ )
495
+ else:
496
+ error_response = exception_to_structured_error(
497
+ e,
498
+ context={"identifier": identifier},
499
+ )
500
+ error_response["action"] = "delete"
501
+ # Add automation-specific suggestions
502
+ if "error" in error_response and isinstance(error_response["error"], dict):
503
+ error_response["error"]["suggestions"] = [
504
+ "Verify automation exists using ha_search_entities(domain_filter='automation')",
505
+ "Use entity_id format: automation.morning_routine or unique_id",
506
+ "Check Home Assistant connection",
507
+ ]
508
+ return error_response