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
ha_mcp/tools/backup.py ADDED
@@ -0,0 +1,457 @@
1
+ """
2
+ Backup and restore tools for Home Assistant MCP Server.
3
+
4
+ Provides backup creation and restoration capabilities with safety mechanisms.
5
+ """
6
+
7
+ import asyncio
8
+ import logging
9
+ from datetime import datetime
10
+ from typing import TYPE_CHECKING, Any, Annotated
11
+
12
+ from pydantic import Field
13
+
14
+ from ..client.rest_client import HomeAssistantClient
15
+ from ..client.websocket_client import HomeAssistantWebSocketClient
16
+ from .helpers import get_connected_ws_client, log_tool_usage
17
+
18
+ if TYPE_CHECKING:
19
+ from mcp.server.fastmcp import FastMCP
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ def _get_backup_hint_text() -> str:
25
+ """
26
+ Generate dynamic backup hint text based on BACKUP_HINT config.
27
+
28
+ Returns:
29
+ Backup hint text appropriate for the configured hint level.
30
+ """
31
+ import os
32
+
33
+ # Get hint from environment directly to avoid requiring full settings
34
+ hint = os.getenv("BACKUP_HINT", "normal").lower()
35
+
36
+ hints = {
37
+ "strong": "Run this backup before the FIRST modification of the day/session. This is usually not required since most operations can be rolled back (the model fetches definitions before modifying). Users with daily backups configured should use 'normal' or 'weak' instead.",
38
+ "normal": "Run before operations that CANNOT be undone (e.g., deleting devices). If the current definition was fetched or can be fetched, this tool is usually not needed.",
39
+ "weak": "Backups are usually not required for configuration changes since most operations can be manually undone. Only run this if specifically requested or before irreversible system operations.",
40
+ "auto": "Run before operations that CANNOT be undone (e.g., deleting devices). If the current definition was fetched or can be fetched, this tool is usually not needed.", # Same as normal for now, will auto-detect in future
41
+ }
42
+ return hints.get(hint, hints["normal"])
43
+
44
+
45
+ async def _get_backup_password(
46
+ ws_client: HomeAssistantWebSocketClient,
47
+ ) -> tuple[str | None, dict[str, Any] | None]:
48
+ """
49
+ Retrieve default backup password from Home Assistant configuration.
50
+
51
+ Args:
52
+ ws_client: Connected WebSocket client
53
+
54
+ Returns:
55
+ Tuple of (password, error_dict). If retrieval fails, password is None.
56
+ """
57
+ backup_config = await ws_client.send_command("backup/config/info")
58
+ if not backup_config.get("success"):
59
+ return None, {
60
+ "success": False,
61
+ "error": "Failed to retrieve backup configuration",
62
+ "details": backup_config,
63
+ }
64
+
65
+ config_data = backup_config.get("result", {}).get("config", {})
66
+ default_password = config_data.get("create_backup", {}).get("password")
67
+
68
+ if not default_password:
69
+ return None, {
70
+ "success": False,
71
+ "error": "No default backup password configured in Home Assistant",
72
+ "suggestion": "Configure automatic backups in Home Assistant settings to set a default password",
73
+ }
74
+
75
+ return default_password, None
76
+
77
+
78
+ async def create_backup(
79
+ client: HomeAssistantClient, name: str | None = None
80
+ ) -> dict[str, Any]:
81
+ """
82
+ Create a fast Home Assistant backup (local only, excludes database).
83
+
84
+ Args:
85
+ client: Home Assistant REST client
86
+ name: Optional backup name (auto-generated if not provided)
87
+
88
+ Returns:
89
+ Dictionary with backup result including backup_id, status, duration, etc.
90
+ """
91
+ ws_client = None
92
+
93
+ try:
94
+ # Connect to WebSocket
95
+ ws_client, error = await get_connected_ws_client(client.base_url, client.token)
96
+ if error:
97
+ return error
98
+
99
+ # Get backup password
100
+ password, error = await _get_backup_password(ws_client)
101
+ if error:
102
+ return error
103
+
104
+ # Generate backup name if not provided
105
+ if not name:
106
+ now = datetime.now()
107
+ name = f"MCP_Backup_{now.strftime('%Y-%m-%d_%H:%M:%S')}"
108
+
109
+ # Create backup request
110
+ backup_params = {
111
+ "name": name,
112
+ "password": password,
113
+ "agent_ids": ["hassio.local"], # Local only
114
+ "include_homeassistant": True,
115
+ "include_database": False, # Fast backup
116
+ "include_all_addons": True,
117
+ }
118
+
119
+ # Send backup request
120
+ result = await ws_client.send_command("backup/generate", **backup_params)
121
+
122
+ if not result.get("success"):
123
+ return {
124
+ "success": False,
125
+ "error": "Backup creation failed",
126
+ "details": result,
127
+ }
128
+
129
+ backup_job_id = result.get("result", {}).get("backup_job_id")
130
+ logger.info(f"Backup job started: {backup_job_id}, waiting for completion...")
131
+
132
+ # Wait for backup to complete by polling backup/info
133
+ max_wait_seconds = 120 # 2 minutes max wait
134
+ poll_interval = 2 # Check every 2 seconds
135
+ waited = 0
136
+
137
+ while waited < max_wait_seconds:
138
+ await asyncio.sleep(poll_interval)
139
+ waited += poll_interval
140
+
141
+ # Check backup status
142
+ info_result = await ws_client.send_command("backup/info")
143
+ if info_result.get("success"):
144
+ state = info_result.get("result", {}).get("state")
145
+ last_event = info_result.get("result", {}).get("last_action_event", {})
146
+ event_state = last_event.get("state")
147
+
148
+ logger.debug(
149
+ f"Backup state: {state}, event_state: {event_state}, waited: {waited}s"
150
+ )
151
+
152
+ # Check if backup is complete
153
+ if state == "idle" and event_state == "completed":
154
+ # Find the backup that was just created
155
+ backups = info_result.get("result", {}).get("backups", [])
156
+ created_backup = None
157
+ for backup in backups:
158
+ if backup.get("name") == name:
159
+ created_backup = backup
160
+ break
161
+
162
+ if created_backup:
163
+ logger.info(
164
+ f"Backup completed successfully: {created_backup.get('backup_id')}"
165
+ )
166
+ return {
167
+ "success": True,
168
+ "backup_id": created_backup.get("backup_id"),
169
+ "backup_job_id": backup_job_id,
170
+ "name": name,
171
+ "date": created_backup.get("date"),
172
+ "size_bytes": created_backup.get("agents", {})
173
+ .get("hassio.local", {})
174
+ .get("size"),
175
+ "status": "Backup completed successfully",
176
+ "duration_seconds": waited,
177
+ "note": "Backup uses your Home Assistant's default backup password",
178
+ }
179
+ else:
180
+ # Backup completed but not found in list yet
181
+ logger.warning(
182
+ "Backup completed but not found in backup list yet, waiting..."
183
+ )
184
+ continue
185
+
186
+ # Check if backup failed
187
+ elif event_state == "failed":
188
+ return {
189
+ "success": False,
190
+ "error": "Backup creation failed",
191
+ "backup_job_id": backup_job_id,
192
+ "last_event": last_event,
193
+ }
194
+
195
+ # Timeout waiting for backup
196
+ logger.warning(f"Backup did not complete within {max_wait_seconds} seconds")
197
+ return {
198
+ "success": False,
199
+ "error": f"Backup creation timed out after {max_wait_seconds} seconds",
200
+ "backup_job_id": backup_job_id,
201
+ "name": name,
202
+ "suggestion": "Backup may still be in progress. Check Home Assistant backup status.",
203
+ }
204
+
205
+ except Exception as e:
206
+ logger.error(f"Error creating backup: {e}")
207
+ return {
208
+ "success": False,
209
+ "error": f"Failed to create backup: {str(e)}",
210
+ "suggestion": "Check Home Assistant connection and backup configuration",
211
+ }
212
+ finally:
213
+ # Always disconnect WebSocket
214
+ if ws_client:
215
+ try:
216
+ await ws_client.disconnect()
217
+ except:
218
+ pass # Ignore errors during cleanup
219
+
220
+
221
+ async def restore_backup(
222
+ client: HomeAssistantClient, backup_id: str, restore_database: bool = False
223
+ ) -> dict[str, Any]:
224
+ """
225
+ Restore Home Assistant from a backup (DESTRUCTIVE - use with caution).
226
+
227
+ Creates a safety backup before restore to allow rollback if needed.
228
+
229
+ Args:
230
+ client: Home Assistant REST client
231
+ backup_id: Backup ID to restore
232
+ restore_database: Whether to restore database (historical data)
233
+
234
+ Returns:
235
+ Dictionary with restore result including safety_backup_id, status, etc.
236
+ """
237
+ ws_client = None
238
+
239
+ try:
240
+ # Connect to WebSocket
241
+ ws_client, error = await get_connected_ws_client(client.base_url, client.token)
242
+ if error:
243
+ return error
244
+
245
+ # Verify backup exists
246
+ backup_info = await ws_client.send_command("backup/info")
247
+ if not backup_info.get("success"):
248
+ return {
249
+ "success": False,
250
+ "error": "Failed to retrieve backup information",
251
+ "details": backup_info,
252
+ }
253
+
254
+ backups = backup_info.get("result", {}).get("backups", [])
255
+ backup_exists = any(b.get("backup_id") == backup_id for b in backups)
256
+
257
+ if not backup_exists:
258
+ available_backups = [
259
+ {
260
+ "backup_id": b.get("backup_id"),
261
+ "name": b.get("name"),
262
+ "date": b.get("date"),
263
+ }
264
+ for b in backups[:5]
265
+ ]
266
+ return {
267
+ "success": False,
268
+ "error": f"Backup '{backup_id}' not found",
269
+ "available_backups": available_backups,
270
+ "suggestion": "Use one of the available backup IDs listed above",
271
+ }
272
+
273
+ # Create safety backup BEFORE restoring
274
+ logger.info("Creating safety backup before restore...")
275
+ now = datetime.now()
276
+ safety_backup_name = f"PreRestore_Safety_{now.strftime('%Y-%m-%d_%H:%M:%S')}"
277
+
278
+ # Get backup password
279
+ password, error = await _get_backup_password(ws_client)
280
+ if error:
281
+ # Password error - log warning but continue (restore might still work)
282
+ logger.warning("No default password - proceeding without safety backup")
283
+ safety_backup_id = None
284
+ else:
285
+ safety_backup = await ws_client.send_command(
286
+ "backup/generate",
287
+ name=safety_backup_name,
288
+ password=password,
289
+ agent_ids=["hassio.local"],
290
+ include_homeassistant=True,
291
+ include_database=True, # Full backup for safety
292
+ include_all_addons=True,
293
+ )
294
+
295
+ if not safety_backup.get("success"):
296
+ return {
297
+ "success": False,
298
+ "error": "Failed to create safety backup before restore",
299
+ "details": safety_backup,
300
+ "suggestion": "Cannot proceed with restore without safety backup",
301
+ }
302
+
303
+ safety_backup_id = safety_backup.get("result", {}).get("backup_job_id")
304
+ logger.info(f"Safety backup created: {safety_backup_id}")
305
+
306
+ # Perform restore
307
+ restore_params = {
308
+ "backup_id": backup_id,
309
+ "agent_id": "hassio.local",
310
+ "restore_database": restore_database,
311
+ "restore_homeassistant": True,
312
+ "restore_addons": [], # Restore all addons from backup
313
+ "restore_folders": [], # Restore all folders from backup
314
+ }
315
+
316
+ result = await ws_client.send_command("backup/restore", **restore_params)
317
+
318
+ if result.get("success"):
319
+ return {
320
+ "success": True,
321
+ "backup_id": backup_id,
322
+ "status": "Restore initiated - Home Assistant will restart",
323
+ "safety_backup_id": safety_backup_id,
324
+ "restore_database": restore_database,
325
+ "warning": "Home Assistant is restarting. Connection will be temporarily lost.",
326
+ "note": "A safety backup was created before restore. You can restore from it if needed.",
327
+ }
328
+ else:
329
+ return {
330
+ "success": False,
331
+ "error": "Restore operation failed",
332
+ "details": result,
333
+ "safety_backup_id": safety_backup_id,
334
+ }
335
+
336
+ except Exception as e:
337
+ logger.error(f"Error restoring backup: {e}")
338
+ return {
339
+ "success": False,
340
+ "error": f"Failed to restore backup: {str(e)}",
341
+ "suggestion": "Check Home Assistant connection and backup availability",
342
+ }
343
+ finally:
344
+ # Always disconnect WebSocket
345
+ if ws_client:
346
+ try:
347
+ await ws_client.disconnect()
348
+ except:
349
+ pass # Ignore errors during cleanup
350
+
351
+
352
+ def register_backup_tools(mcp: "FastMCP", client: HomeAssistantClient, **kwargs) -> None:
353
+ """
354
+ Register backup and restore tools with the MCP server.
355
+
356
+ Args:
357
+ mcp: FastMCP server instance
358
+ client: Home Assistant REST client
359
+ **kwargs: Additional arguments (ignored, for auto-discovery compatibility)
360
+ """
361
+ # Generate dynamic backup description based on BACKUP_HINT config
362
+ backup_hint_text = _get_backup_hint_text()
363
+ backup_create_description = f"""Create a fast Home Assistant backup (local only).
364
+
365
+ **What's Included:**
366
+ - Home Assistant configuration (core settings)
367
+ - All add-ons
368
+ - SSL certificates
369
+ - Database is EXCLUDED for faster backup (excludes historical sensor data, statistics, state history)
370
+
371
+ **Password:** Uses Home Assistant's default backup password (if configured)
372
+
373
+ **Storage:** Local only (hassio.local agent)
374
+
375
+ **Duration:** Typically takes several seconds to complete (without database)
376
+
377
+ **When to Use:**
378
+ {backup_hint_text}
379
+
380
+ **Example Usage:**
381
+ - Before deleting device: ha_backup_create("Before_Device_Delete")
382
+ - Before modifying system settings: ha_backup_create("Pre_System_Change")
383
+ - Quick safety backup: ha_backup_create()
384
+
385
+ **Returns:** Backup ID and job status"""
386
+
387
+ @mcp.tool(description=backup_create_description, annotations={"destructiveHint": True, "title": "Create Backup"})
388
+ @log_tool_usage
389
+ async def ha_backup_create(
390
+ name: Annotated[
391
+ str | None,
392
+ Field(
393
+ description="Backup name (auto-generated if not provided, e.g., 'MCP_Backup_2025-10-05_04:30')",
394
+ default=None,
395
+ ),
396
+ ] = None,
397
+ ) -> dict[str, Any]:
398
+ """Create a fast Home Assistant backup (local only)."""
399
+ return await create_backup(client, name)
400
+
401
+ @mcp.tool(annotations={"destructiveHint": True, "title": "Restore Backup"})
402
+ @log_tool_usage
403
+ async def ha_backup_restore(
404
+ backup_id: Annotated[
405
+ str,
406
+ Field(
407
+ description="Backup ID to restore (e.g., 'dd7550ed' from backup list or ha_backup_create result)"
408
+ ),
409
+ ],
410
+ restore_database: Annotated[
411
+ bool,
412
+ Field(
413
+ description="Restore database (default: false for config-only restore)",
414
+ default=False,
415
+ ),
416
+ ] = False,
417
+ ) -> dict[str, Any]:
418
+ """
419
+ Restore Home Assistant from a backup (LAST RESORT - use with extreme caution).
420
+
421
+ **⚠️ WARNING - DESTRUCTIVE OPERATION ⚠️**
422
+
423
+ **This tool restarts Home Assistant and restores configuration to a previous state.**
424
+
425
+ **IMPORTANT CONSIDERATIONS:**
426
+ 1. **Try undo operations first** - Often you can just reverse what you did:
427
+ - Deleted automation? Recreate it with ha_config_set_automation
428
+ - Modified script? Use ha_config_set_script to fix it
429
+ - Most config changes can be rolled back without using restore
430
+
431
+ 2. **Safety mechanism:** A NEW backup is automatically created BEFORE restore
432
+ - This allows you to rollback the restore if needed
433
+ - You can restore from this pre-restore backup if something goes wrong
434
+
435
+ 3. **What gets restored:**
436
+ - Home Assistant configuration (automations, scripts, etc.)
437
+ - Add-ons (if they were in the backup)
438
+ - Optional: Database - historical sensor data, statistics, state history (set restore_database=true)
439
+
440
+ 4. **Side effects:**
441
+ - Home Assistant will RESTART during restore
442
+ - Any changes made after the backup was created will be LOST
443
+ - Temporary disconnection from all integrations during restart
444
+
445
+ **Recommended workflow:**
446
+ 1. Try to undo your changes manually first
447
+ 2. If you must restore, use the most recent backup
448
+ 3. Set restore_database=false unless you need historical data
449
+ 4. Expect a restart and temporary downtime
450
+
451
+ **Example Usage:**
452
+ - Restore config only: ha_backup_restore("dd7550ed")
453
+ - Full restore with DB: ha_backup_restore("dd7550ed", restore_database=true)
454
+
455
+ **Returns:** Restore job status
456
+ """
457
+ return await restore_backup(client, backup_id, restore_database)