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
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)
|