ha-mcp-dev 7.3.0.dev372__tar.gz → 7.3.0.dev374__tar.gz
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_dev-7.3.0.dev372/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.3.0.dev374}/PKG-INFO +1 -1
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/pyproject.toml +2 -7
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/tools/backup.py +122 -95
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/tools/best_practice_checker.py +39 -30
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/tools/device_control.py +305 -246
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/tools/helpers.py +111 -99
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/tools/registry.py +39 -33
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/tools/tools_history.py +1 -1
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/LICENSE +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/README.md +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/setup.cfg +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/tools/smart_search.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/tests/test_env_manager.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ha-mcp-dev"
|
|
7
|
-
version = "7.3.0.
|
|
7
|
+
version = "7.3.0.dev374"
|
|
8
8
|
description = "Home Assistant MCP Server - Complete control of Home Assistant through MCP"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.13,<3.14"
|
|
@@ -135,12 +135,6 @@ ignore = [
|
|
|
135
135
|
"tests/**/*" = ["E501", "B011"]
|
|
136
136
|
# C901 ignores for tools files with complex methods (see #925).
|
|
137
137
|
# Remove lines as individual methods are simplified below threshold.
|
|
138
|
-
"src/ha_mcp/tools/backup.py" = ["C901"]
|
|
139
|
-
"src/ha_mcp/tools/best_practice_checker.py" = ["C901"]
|
|
140
|
-
"src/ha_mcp/tools/device_control.py" = ["C901"]
|
|
141
|
-
"src/ha_mcp/tools/helpers.py" = ["C901"]
|
|
142
|
-
"src/ha_mcp/tools/registry.py" = ["C901"]
|
|
143
|
-
"src/ha_mcp/tools/smart_search.py" = ["C901"]
|
|
144
138
|
"src/ha_mcp/tools/tools_addons.py" = ["C901"]
|
|
145
139
|
"src/ha_mcp/tools/tools_config_dashboards.py" = ["C901"]
|
|
146
140
|
"src/ha_mcp/tools/tools_config_helpers.py" = ["C901"]
|
|
@@ -148,6 +142,7 @@ ignore = [
|
|
|
148
142
|
"src/ha_mcp/tools/tools_registry.py" = ["C901"]
|
|
149
143
|
"src/ha_mcp/tools/tools_search.py" = ["C901"]
|
|
150
144
|
"src/ha_mcp/tools/tools_utility.py" = ["C901"]
|
|
145
|
+
"src/ha_mcp/tools/smart_search.py" = ["C901"]
|
|
151
146
|
"src/ha_mcp/tools/util_helpers.py" = ["C901"]
|
|
152
147
|
|
|
153
148
|
[tool.pytest.ini_options]
|
|
@@ -27,6 +27,9 @@ if TYPE_CHECKING:
|
|
|
27
27
|
|
|
28
28
|
logger = logging.getLogger(__name__)
|
|
29
29
|
|
|
30
|
+
_BACKUP_MAX_WAIT_S = 120
|
|
31
|
+
_BACKUP_POLL_INTERVAL_S = 2
|
|
32
|
+
|
|
30
33
|
|
|
31
34
|
def _get_backup_hint_text() -> str:
|
|
32
35
|
"""
|
|
@@ -85,6 +88,80 @@ async def _get_backup_password(
|
|
|
85
88
|
return cast(str, default_password)
|
|
86
89
|
|
|
87
90
|
|
|
91
|
+
async def _poll_backup_completion(
|
|
92
|
+
ws_client: HomeAssistantWebSocketClient,
|
|
93
|
+
name: str,
|
|
94
|
+
backup_job_id: str,
|
|
95
|
+
max_wait_seconds: int,
|
|
96
|
+
poll_interval: int,
|
|
97
|
+
) -> dict[str, Any]:
|
|
98
|
+
"""Poll backup/info until the named backup completes, fails, or times out.
|
|
99
|
+
|
|
100
|
+
Raises ToolError on backup failure or timeout.
|
|
101
|
+
"""
|
|
102
|
+
waited = 0
|
|
103
|
+
|
|
104
|
+
while waited < max_wait_seconds:
|
|
105
|
+
await asyncio.sleep(poll_interval)
|
|
106
|
+
waited += poll_interval
|
|
107
|
+
|
|
108
|
+
info_result = await ws_client.send_command("backup/info")
|
|
109
|
+
if info_result.get("success"):
|
|
110
|
+
state = info_result.get("result", {}).get("state")
|
|
111
|
+
last_event = info_result.get("result", {}).get("last_action_event", {})
|
|
112
|
+
event_state = last_event.get("state")
|
|
113
|
+
|
|
114
|
+
logger.debug(
|
|
115
|
+
f"Backup state: {state}, event_state: {event_state}, waited: {waited}s"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
if state == "idle" and event_state == "completed":
|
|
119
|
+
backups = info_result.get("result", {}).get("backups", [])
|
|
120
|
+
created_backup = None
|
|
121
|
+
for backup in backups:
|
|
122
|
+
if backup.get("name") == name:
|
|
123
|
+
created_backup = backup
|
|
124
|
+
break
|
|
125
|
+
|
|
126
|
+
if created_backup:
|
|
127
|
+
logger.info(
|
|
128
|
+
f"Backup completed successfully: {created_backup.get('backup_id')}"
|
|
129
|
+
)
|
|
130
|
+
return {
|
|
131
|
+
"success": True,
|
|
132
|
+
"backup_id": created_backup.get("backup_id"),
|
|
133
|
+
"backup_job_id": backup_job_id,
|
|
134
|
+
"name": name,
|
|
135
|
+
"date": created_backup.get("date"),
|
|
136
|
+
"size_bytes": created_backup.get("agents", {})
|
|
137
|
+
.get("hassio.local", {})
|
|
138
|
+
.get("size"),
|
|
139
|
+
"status": "Backup completed successfully",
|
|
140
|
+
"duration_seconds": waited,
|
|
141
|
+
"note": "Backup uses your Home Assistant's default backup password",
|
|
142
|
+
}
|
|
143
|
+
else:
|
|
144
|
+
logger.warning(
|
|
145
|
+
"Backup completed but not found in backup list yet, waiting..."
|
|
146
|
+
)
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
elif event_state == "failed":
|
|
150
|
+
raise_tool_error(create_error_response(
|
|
151
|
+
ErrorCode.SERVICE_CALL_FAILED,
|
|
152
|
+
"Backup creation failed",
|
|
153
|
+
context={"backup_job_id": backup_job_id},
|
|
154
|
+
))
|
|
155
|
+
|
|
156
|
+
logger.warning(f"Backup did not complete within {max_wait_seconds} seconds")
|
|
157
|
+
raise_tool_error(create_error_response(
|
|
158
|
+
ErrorCode.TIMEOUT_OPERATION,
|
|
159
|
+
f"Backup creation timed out after {max_wait_seconds} seconds",
|
|
160
|
+
context={"backup_job_id": backup_job_id, "name": name},
|
|
161
|
+
suggestions=["Backup may still be in progress. Check Home Assistant backup status."],
|
|
162
|
+
))
|
|
163
|
+
|
|
164
|
+
|
|
88
165
|
async def create_backup(
|
|
89
166
|
client: HomeAssistantClient, name: str | None = None
|
|
90
167
|
) -> dict[str, Any]:
|
|
@@ -140,76 +217,13 @@ async def create_backup(
|
|
|
140
217
|
backup_job_id = result.get("result", {}).get("backup_job_id")
|
|
141
218
|
logger.info(f"Backup job started: {backup_job_id}, waiting for completion...")
|
|
142
219
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
waited += poll_interval
|
|
151
|
-
|
|
152
|
-
# Check backup status
|
|
153
|
-
info_result = await ws_client.send_command("backup/info")
|
|
154
|
-
if info_result.get("success"):
|
|
155
|
-
state = info_result.get("result", {}).get("state")
|
|
156
|
-
last_event = info_result.get("result", {}).get("last_action_event", {})
|
|
157
|
-
event_state = last_event.get("state")
|
|
158
|
-
|
|
159
|
-
logger.debug(
|
|
160
|
-
f"Backup state: {state}, event_state: {event_state}, waited: {waited}s"
|
|
161
|
-
)
|
|
162
|
-
|
|
163
|
-
# Check if backup is complete
|
|
164
|
-
if state == "idle" and event_state == "completed":
|
|
165
|
-
# Find the backup that was just created
|
|
166
|
-
backups = info_result.get("result", {}).get("backups", [])
|
|
167
|
-
created_backup = None
|
|
168
|
-
for backup in backups:
|
|
169
|
-
if backup.get("name") == name:
|
|
170
|
-
created_backup = backup
|
|
171
|
-
break
|
|
172
|
-
|
|
173
|
-
if created_backup:
|
|
174
|
-
logger.info(
|
|
175
|
-
f"Backup completed successfully: {created_backup.get('backup_id')}"
|
|
176
|
-
)
|
|
177
|
-
return {
|
|
178
|
-
"success": True,
|
|
179
|
-
"backup_id": created_backup.get("backup_id"),
|
|
180
|
-
"backup_job_id": backup_job_id,
|
|
181
|
-
"name": name,
|
|
182
|
-
"date": created_backup.get("date"),
|
|
183
|
-
"size_bytes": created_backup.get("agents", {})
|
|
184
|
-
.get("hassio.local", {})
|
|
185
|
-
.get("size"),
|
|
186
|
-
"status": "Backup completed successfully",
|
|
187
|
-
"duration_seconds": waited,
|
|
188
|
-
"note": "Backup uses your Home Assistant's default backup password",
|
|
189
|
-
}
|
|
190
|
-
else:
|
|
191
|
-
# Backup completed but not found in list yet
|
|
192
|
-
logger.warning(
|
|
193
|
-
"Backup completed but not found in backup list yet, waiting..."
|
|
194
|
-
)
|
|
195
|
-
continue
|
|
196
|
-
|
|
197
|
-
# Check if backup failed
|
|
198
|
-
elif event_state == "failed":
|
|
199
|
-
raise_tool_error(create_error_response(
|
|
200
|
-
ErrorCode.SERVICE_CALL_FAILED,
|
|
201
|
-
"Backup creation failed",
|
|
202
|
-
context={"backup_job_id": backup_job_id},
|
|
203
|
-
))
|
|
204
|
-
|
|
205
|
-
# Timeout waiting for backup
|
|
206
|
-
logger.warning(f"Backup did not complete within {max_wait_seconds} seconds")
|
|
207
|
-
raise_tool_error(create_error_response(
|
|
208
|
-
ErrorCode.TIMEOUT_OPERATION,
|
|
209
|
-
f"Backup creation timed out after {max_wait_seconds} seconds",
|
|
210
|
-
context={"backup_job_id": backup_job_id, "name": name},
|
|
211
|
-
suggestions=["Backup may still be in progress. Check Home Assistant backup status."],
|
|
212
|
-
))
|
|
220
|
+
return await _poll_backup_completion(
|
|
221
|
+
ws_client,
|
|
222
|
+
name,
|
|
223
|
+
backup_job_id,
|
|
224
|
+
max_wait_seconds=_BACKUP_MAX_WAIT_S,
|
|
225
|
+
poll_interval=_BACKUP_POLL_INTERVAL_S,
|
|
226
|
+
)
|
|
213
227
|
|
|
214
228
|
except ToolError:
|
|
215
229
|
raise
|
|
@@ -229,6 +243,43 @@ async def create_backup(
|
|
|
229
243
|
pass # Ignore errors during cleanup
|
|
230
244
|
|
|
231
245
|
|
|
246
|
+
async def _create_safety_backup(
|
|
247
|
+
ws_client: HomeAssistantWebSocketClient,
|
|
248
|
+
password: str | None,
|
|
249
|
+
) -> str | None:
|
|
250
|
+
"""Create a pre-restore safety backup.
|
|
251
|
+
|
|
252
|
+
Returns the safety backup ID, or None when password is None (backup intentionally
|
|
253
|
+
skipped). Raises ToolError if backup creation fails.
|
|
254
|
+
"""
|
|
255
|
+
if password is None:
|
|
256
|
+
return None
|
|
257
|
+
|
|
258
|
+
now = datetime.now()
|
|
259
|
+
safety_backup_name = f"PreRestore_Safety_{now.strftime('%Y-%m-%d_%H:%M:%S')}"
|
|
260
|
+
|
|
261
|
+
safety_backup = await ws_client.send_command(
|
|
262
|
+
"backup/generate",
|
|
263
|
+
name=safety_backup_name,
|
|
264
|
+
password=password,
|
|
265
|
+
agent_ids=["hassio.local"],
|
|
266
|
+
include_homeassistant=True,
|
|
267
|
+
include_database=True,
|
|
268
|
+
include_all_addons=True,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
if not safety_backup.get("success"):
|
|
272
|
+
raise_tool_error(create_error_response(
|
|
273
|
+
ErrorCode.SERVICE_CALL_FAILED,
|
|
274
|
+
safety_backup.get("error", "Failed to create safety backup before restore"),
|
|
275
|
+
suggestions=["Cannot proceed with restore without safety backup"],
|
|
276
|
+
))
|
|
277
|
+
|
|
278
|
+
safety_backup_id = safety_backup.get("result", {}).get("backup_job_id")
|
|
279
|
+
logger.info(f"Safety backup created: {safety_backup_id}")
|
|
280
|
+
return cast(str, safety_backup_id)
|
|
281
|
+
|
|
282
|
+
|
|
232
283
|
async def restore_backup(
|
|
233
284
|
client: HomeAssistantClient, backup_id: str, restore_database: bool = False
|
|
234
285
|
) -> dict[str, Any]:
|
|
@@ -277,38 +328,14 @@ async def restore_backup(
|
|
|
277
328
|
|
|
278
329
|
# Create safety backup BEFORE restoring
|
|
279
330
|
logger.info("Creating safety backup before restore...")
|
|
280
|
-
now = datetime.now()
|
|
281
|
-
safety_backup_name = f"PreRestore_Safety_{now.strftime('%Y-%m-%d_%H:%M:%S')}"
|
|
282
|
-
|
|
283
|
-
# Get backup password
|
|
284
331
|
try:
|
|
285
332
|
password = await _get_backup_password(ws_client)
|
|
286
333
|
except ToolError:
|
|
287
334
|
# Password error - log warning but continue (restore might still work)
|
|
288
335
|
logger.warning("No default password - proceeding without safety backup")
|
|
289
336
|
password = None
|
|
290
|
-
safety_backup_id = None
|
|
291
|
-
|
|
292
|
-
if password is not None:
|
|
293
|
-
safety_backup = await ws_client.send_command(
|
|
294
|
-
"backup/generate",
|
|
295
|
-
name=safety_backup_name,
|
|
296
|
-
password=password,
|
|
297
|
-
agent_ids=["hassio.local"],
|
|
298
|
-
include_homeassistant=True,
|
|
299
|
-
include_database=True, # Full backup for safety
|
|
300
|
-
include_all_addons=True,
|
|
301
|
-
)
|
|
302
|
-
|
|
303
|
-
if not safety_backup.get("success"):
|
|
304
|
-
raise_tool_error(create_error_response(
|
|
305
|
-
ErrorCode.SERVICE_CALL_FAILED,
|
|
306
|
-
safety_backup.get("error", "Failed to create safety backup before restore"),
|
|
307
|
-
suggestions=["Cannot proceed with restore without safety backup"],
|
|
308
|
-
))
|
|
309
337
|
|
|
310
|
-
|
|
311
|
-
logger.info(f"Safety backup created: {safety_backup_id}")
|
|
338
|
+
safety_backup_id = await _create_safety_backup(ws_client, password)
|
|
312
339
|
|
|
313
340
|
# Perform restore
|
|
314
341
|
restore_params = {
|
{ha_mcp_dev-7.3.0.dev372 → ha_mcp_dev-7.3.0.dev374}/src/ha_mcp/tools/best_practice_checker.py
RENAMED
|
@@ -216,6 +216,27 @@ def _check_template_string(
|
|
|
216
216
|
# ---------------------------------------------------------------------------
|
|
217
217
|
|
|
218
218
|
|
|
219
|
+
def _check_choose_actions(
|
|
220
|
+
choose: Any, warnings: list[str], skill_prefix: str | None
|
|
221
|
+
) -> None:
|
|
222
|
+
for option in _as_list(choose):
|
|
223
|
+
if isinstance(option, dict):
|
|
224
|
+
_check_condition_templates(
|
|
225
|
+
option.get("conditions", []), warnings, skill_prefix
|
|
226
|
+
)
|
|
227
|
+
_check_action_tree(
|
|
228
|
+
option.get("sequence", []), warnings, skill_prefix
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _check_repeat_actions(
|
|
233
|
+
repeat: dict, warnings: list[str], skill_prefix: str | None
|
|
234
|
+
) -> None:
|
|
235
|
+
_check_condition_templates(repeat.get("while", []), warnings, skill_prefix)
|
|
236
|
+
_check_condition_templates(repeat.get("until", []), warnings, skill_prefix)
|
|
237
|
+
_check_action_tree(repeat.get("sequence", []), warnings, skill_prefix)
|
|
238
|
+
|
|
239
|
+
|
|
219
240
|
def _check_action_tree(
|
|
220
241
|
actions: Any, warnings: list[str], skill_prefix: str | None
|
|
221
242
|
) -> None:
|
|
@@ -235,14 +256,7 @@ def _check_action_tree(
|
|
|
235
256
|
|
|
236
257
|
# Nested conditions in choose/if/repeat
|
|
237
258
|
if "choose" in action:
|
|
238
|
-
|
|
239
|
-
if isinstance(option, dict):
|
|
240
|
-
_check_condition_templates(
|
|
241
|
-
option.get("conditions", []), warnings, skill_prefix
|
|
242
|
-
)
|
|
243
|
-
_check_action_tree(
|
|
244
|
-
option.get("sequence", []), warnings, skill_prefix
|
|
245
|
-
)
|
|
259
|
+
_check_choose_actions(action["choose"], warnings, skill_prefix)
|
|
246
260
|
|
|
247
261
|
if "if" in action:
|
|
248
262
|
_check_condition_templates(action["if"], warnings, skill_prefix)
|
|
@@ -253,16 +267,7 @@ def _check_action_tree(
|
|
|
253
267
|
_check_action_tree(nested, warnings, skill_prefix)
|
|
254
268
|
|
|
255
269
|
if "repeat" in action and isinstance(action["repeat"], dict):
|
|
256
|
-
|
|
257
|
-
_check_condition_templates(
|
|
258
|
-
repeat.get("while", []), warnings, skill_prefix
|
|
259
|
-
)
|
|
260
|
-
_check_condition_templates(
|
|
261
|
-
repeat.get("until", []), warnings, skill_prefix
|
|
262
|
-
)
|
|
263
|
-
_check_action_tree(
|
|
264
|
-
repeat.get("sequence", []), warnings, skill_prefix
|
|
265
|
-
)
|
|
270
|
+
_check_repeat_actions(action["repeat"], warnings, skill_prefix)
|
|
266
271
|
|
|
267
272
|
|
|
268
273
|
# ---------------------------------------------------------------------------
|
|
@@ -347,6 +352,20 @@ def _check_mode_motion(
|
|
|
347
352
|
)
|
|
348
353
|
|
|
349
354
|
|
|
355
|
+
def _has_delay_or_wait_in_nested(action: dict) -> bool:
|
|
356
|
+
for key in ("then", "else", "default", "sequence"):
|
|
357
|
+
if key in action and _has_delay_or_wait(action[key]):
|
|
358
|
+
return True
|
|
359
|
+
if "choose" in action:
|
|
360
|
+
for opt in _as_list(action["choose"]):
|
|
361
|
+
if isinstance(opt, dict) and _has_delay_or_wait(opt.get("sequence", [])):
|
|
362
|
+
return True
|
|
363
|
+
if "repeat" in action and isinstance(action["repeat"], dict):
|
|
364
|
+
if _has_delay_or_wait(action["repeat"].get("sequence", [])):
|
|
365
|
+
return True
|
|
366
|
+
return False
|
|
367
|
+
|
|
368
|
+
|
|
350
369
|
def _has_delay_or_wait(actions: Any) -> bool:
|
|
351
370
|
"""Recursively check if any action uses delay or wait."""
|
|
352
371
|
for action in _as_list(actions):
|
|
@@ -354,18 +373,8 @@ def _has_delay_or_wait(actions: Any) -> bool:
|
|
|
354
373
|
continue
|
|
355
374
|
if any(k in action for k in ("delay", "wait_for_trigger", "wait_template")):
|
|
356
375
|
return True
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
return True
|
|
360
|
-
if "choose" in action:
|
|
361
|
-
for opt in _as_list(action["choose"]):
|
|
362
|
-
if isinstance(opt, dict) and _has_delay_or_wait(
|
|
363
|
-
opt.get("sequence", [])
|
|
364
|
-
):
|
|
365
|
-
return True
|
|
366
|
-
if "repeat" in action and isinstance(action["repeat"], dict):
|
|
367
|
-
if _has_delay_or_wait(action["repeat"].get("sequence", [])):
|
|
368
|
-
return True
|
|
376
|
+
if _has_delay_or_wait_in_nested(action):
|
|
377
|
+
return True
|
|
369
378
|
return False
|
|
370
379
|
|
|
371
380
|
|