ha-mcp-dev 7.4.1.dev484__tar.gz → 7.4.1.dev486__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.
Files changed (110) hide show
  1. {ha_mcp_dev-7.4.1.dev484/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.4.1.dev486}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/tools/backup.py +99 -9
  4. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/tools/tools_integrations.py +110 -6
  5. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  6. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/LICENSE +0 -0
  7. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/MANIFEST.in +0 -0
  8. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/README.md +0 -0
  9. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/setup.cfg +0 -0
  10. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/__init__.py +0 -0
  11. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/__main__.py +0 -0
  12. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/_pypi_marker +0 -0
  13. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/_version.py +0 -0
  14. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/auth/__init__.py +0 -0
  15. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/auth/consent_form.py +0 -0
  16. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/auth/provider.py +0 -0
  17. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/client/__init__.py +0 -0
  18. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/client/rest_client.py +0 -0
  19. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/client/supervisor_client.py +0 -0
  20. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/client/websocket_client.py +0 -0
  21. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/client/websocket_listener.py +0 -0
  22. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/config.py +0 -0
  23. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/errors.py +0 -0
  24. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/py.typed +0 -0
  25. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  26. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  27. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  28. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  29. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  30. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  31. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  32. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  33. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  34. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  35. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  36. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  37. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  38. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  39. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  40. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  41. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  42. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  43. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  44. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  45. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/server.py +0 -0
  46. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/settings_ui.py +0 -0
  47. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/smoke_test.py +0 -0
  48. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/tools/__init__.py +0 -0
  49. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  50. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/tools/device_control.py +0 -0
  51. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/tools/enhanced.py +0 -0
  52. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/tools/helpers.py +0 -0
  53. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/tools/reference_validator.py +0 -0
  54. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/tools/registry.py +0 -0
  55. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/tools/smart_search.py +0 -0
  56. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/tools/tools_addons.py +0 -0
  57. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/tools/tools_areas.py +0 -0
  58. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  59. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  60. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/tools/tools_calendar.py +0 -0
  61. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/tools/tools_camera.py +0 -0
  62. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/tools/tools_categories.py +0 -0
  63. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/tools/tools_code.py +0 -0
  64. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  65. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  66. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
  67. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  68. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  69. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  70. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/tools/tools_energy.py +0 -0
  71. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/tools/tools_entities.py +0 -0
  72. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  73. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/tools/tools_groups.py +0 -0
  74. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/tools/tools_hacs.py +0 -0
  75. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/tools/tools_history.py +0 -0
  76. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/tools/tools_labels.py +0 -0
  77. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  78. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/tools/tools_registry.py +0 -0
  79. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/tools/tools_resources.py +0 -0
  80. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/tools/tools_search.py +0 -0
  81. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/tools/tools_service.py +0 -0
  82. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/tools/tools_services.py +0 -0
  83. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/tools/tools_system.py +0 -0
  84. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/tools/tools_todo.py +0 -0
  85. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/tools/tools_traces.py +0 -0
  86. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/tools/tools_updates.py +0 -0
  87. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/tools/tools_utility.py +0 -0
  88. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  89. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  90. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/tools/tools_zones.py +0 -0
  91. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/tools/util_helpers.py +0 -0
  92. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/transforms/__init__.py +0 -0
  93. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/transforms/categorized_search.py +0 -0
  94. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/utils/__init__.py +0 -0
  95. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/utils/config_hash.py +0 -0
  96. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/utils/data_paths.py +0 -0
  97. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/utils/domain_handlers.py +0 -0
  98. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  99. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  100. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/utils/operation_manager.py +0 -0
  101. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/utils/python_sandbox.py +0 -0
  102. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp/utils/usage_logger.py +0 -0
  103. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  104. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  105. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  106. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  107. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  108. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/tests/__init__.py +0 -0
  109. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/tests/test_constants.py +0 -0
  110. {ha_mcp_dev-7.4.1.dev484 → ha_mcp_dev-7.4.1.dev486}/tests/test_env_manager.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.4.1.dev484
3
+ Version: 7.4.1.dev486
4
4
  Summary: Home Assistant MCP Server - Complete control of Home Assistant through MCP
5
5
  Author-email: Julien <github@qc-h.net>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ha-mcp-dev"
7
- version = "7.4.1.dev484"
7
+ version = "7.4.1.dev486"
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"
@@ -52,6 +52,67 @@ def _get_backup_hint_text() -> str:
52
52
  return hints.get(hint, hints["normal"])
53
53
 
54
54
 
55
+ async def _get_local_backup_agent_id(
56
+ ws_client: HomeAssistantWebSocketClient,
57
+ ) -> str:
58
+ """Discover the local backup agent_id at call time.
59
+
60
+ HA Supervised registers ``hassio.local`` and HA Core registers
61
+ ``backup.local`` — both have ``name: "local"``. Hardcoding either breaks
62
+ the other deployment. We probe ``backup/agents/info`` and pick the agent
63
+ whose name is exactly ``"local"``, preferring ``hassio.local`` if both
64
+ happen to be registered.
65
+
66
+ Raises ToolError if no local agent is available.
67
+ """
68
+ response = await ws_client.send_command("backup/agents/info")
69
+ if not response.get("success"):
70
+ raise_tool_error(
71
+ create_error_response(
72
+ ErrorCode.SERVICE_CALL_FAILED,
73
+ "Failed to enumerate backup agents",
74
+ context={"details": response},
75
+ )
76
+ )
77
+
78
+ agents = response.get("result", {}).get("agents", [])
79
+ if not agents:
80
+ raise_tool_error(
81
+ create_error_response(
82
+ ErrorCode.SERVICE_CALL_FAILED,
83
+ "No backup agents registered with Home Assistant",
84
+ suggestions=[
85
+ "The HA backup integration may not be fully set up; "
86
+ "check the backup panel in Home Assistant",
87
+ ],
88
+ )
89
+ )
90
+
91
+ local_agents: list[str] = [
92
+ a["agent_id"]
93
+ for a in agents
94
+ if a.get("name") == "local" and a.get("agent_id")
95
+ ]
96
+ # Prefer hassio.local (Supervisor) over backup.local (Core) when both exist
97
+ for preferred in ("hassio.local", "backup.local"):
98
+ if preferred in local_agents:
99
+ return preferred
100
+ if local_agents:
101
+ return local_agents[0]
102
+
103
+ raise_tool_error(
104
+ create_error_response(
105
+ ErrorCode.SERVICE_CALL_FAILED,
106
+ "No local backup agent found",
107
+ context={"available_agents": [a.get("agent_id") for a in agents if a.get("agent_id")]},
108
+ suggestions=[
109
+ "Backup creation requires a local agent (hassio.local on "
110
+ "Supervised, backup.local on Core); none is registered",
111
+ ],
112
+ )
113
+ )
114
+
115
+
55
116
  async def _get_backup_password(
56
117
  ws_client: HomeAssistantWebSocketClient,
57
118
  ) -> str:
@@ -94,9 +155,14 @@ async def _poll_backup_completion(
94
155
  backup_job_id: str,
95
156
  max_wait_seconds: int,
96
157
  poll_interval: int,
158
+ agent_id: str,
97
159
  ) -> dict[str, Any]:
98
160
  """Poll backup/info until the named backup completes, fails, or times out.
99
161
 
162
+ ``agent_id`` is the local agent that owns this backup (e.g.
163
+ ``hassio.local`` on Supervised, ``backup.local`` on Core); used to look
164
+ up the per-agent size in the backup-info payload.
165
+
100
166
  Raises ToolError on backup failure or timeout.
101
167
  """
102
168
  waited = 0
@@ -134,7 +200,7 @@ async def _poll_backup_completion(
134
200
  "name": name,
135
201
  "date": created_backup.get("date"),
136
202
  "size_bytes": created_backup.get("agents", {})
137
- .get("hassio.local", {})
203
+ .get(agent_id, {})
138
204
  .get("size"),
139
205
  "status": "Backup completed successfully",
140
206
  "duration_seconds": waited,
@@ -192,19 +258,31 @@ async def create_backup(
192
258
  # Get backup password (raises ToolError on failure)
193
259
  password = await _get_backup_password(ws_client)
194
260
 
261
+ # Discover the local backup agent at call time. HA Core registers
262
+ # `backup.local`; HA Supervised registers `hassio.local`. Hardcoding
263
+ # either breaks the other deployment.
264
+ local_agent = await _get_local_backup_agent_id(ws_client)
265
+
195
266
  # Generate backup name if not provided
196
267
  if not name:
197
268
  now = datetime.now()
198
269
  name = f"MCP_Backup_{now.strftime('%Y-%m-%d_%H:%M:%S')}"
199
270
 
200
- # Create backup request
271
+ # Addons + addon folders are Supervisor concepts — HA Core errors
272
+ # with "Addons and folders are not supported by core backup" if we
273
+ # ask for them. Toggle off when we detect the Core local agent.
274
+ is_supervised = local_agent == "hassio.local"
275
+ logger.info(
276
+ f"Detected {'Supervised' if is_supervised else 'Core'} install "
277
+ f"via backup agent '{local_agent}'"
278
+ )
201
279
  backup_params = {
202
280
  "name": name,
203
281
  "password": password,
204
- "agent_ids": ["hassio.local"], # Local only
282
+ "agent_ids": [local_agent],
205
283
  "include_homeassistant": True,
206
284
  "include_database": False, # Fast backup
207
- "include_all_addons": True,
285
+ "include_all_addons": is_supervised,
208
286
  }
209
287
 
210
288
  # Send backup request
@@ -225,6 +303,7 @@ async def create_backup(
225
303
  backup_job_id,
226
304
  max_wait_seconds=_BACKUP_MAX_WAIT_S,
227
305
  poll_interval=_BACKUP_POLL_INTERVAL_S,
306
+ agent_id=local_agent,
228
307
  )
229
308
 
230
309
  except ToolError:
@@ -248,9 +327,13 @@ async def create_backup(
248
327
  async def _create_safety_backup(
249
328
  ws_client: HomeAssistantWebSocketClient,
250
329
  password: str | None,
330
+ agent_id: str,
251
331
  ) -> str | None:
252
332
  """Create a pre-restore safety backup.
253
333
 
334
+ ``agent_id`` is the local backup agent (Supervisor's ``hassio.local`` or
335
+ Core's ``backup.local``) discovered by the caller.
336
+
254
337
  Returns the safety backup ID, or None when password is None (backup intentionally
255
338
  skipped). Raises ToolError if backup creation fails.
256
339
  """
@@ -260,14 +343,16 @@ async def _create_safety_backup(
260
343
  now = datetime.now()
261
344
  safety_backup_name = f"PreRestore_Safety_{now.strftime('%Y-%m-%d_%H:%M:%S')}"
262
345
 
346
+ # include_all_addons is a Supervisor concept; HA Core rejects it.
347
+ is_supervised = agent_id == "hassio.local"
263
348
  safety_backup = await ws_client.send_command(
264
349
  "backup/generate",
265
350
  name=safety_backup_name,
266
351
  password=password,
267
- agent_ids=["hassio.local"],
352
+ agent_ids=[agent_id],
268
353
  include_homeassistant=True,
269
354
  include_database=True,
270
- include_all_addons=True,
355
+ include_all_addons=is_supervised,
271
356
  )
272
357
 
273
358
  if not safety_backup.get("success"):
@@ -330,6 +415,11 @@ async def restore_backup(
330
415
  suggestions=["Use ha_backup_list() to see available backups"],
331
416
  ))
332
417
 
418
+ # Discover the local backup agent (Supervisor's hassio.local on
419
+ # Supervised, backup.local on Core). Used for both the safety backup
420
+ # and the restore call below.
421
+ local_agent = await _get_local_backup_agent_id(ws_client)
422
+
333
423
  # Create safety backup BEFORE restoring
334
424
  logger.info("Creating safety backup before restore...")
335
425
  try:
@@ -339,12 +429,12 @@ async def restore_backup(
339
429
  logger.warning("No default password - proceeding without safety backup")
340
430
  password = None
341
431
 
342
- safety_backup_id = await _create_safety_backup(ws_client, password)
432
+ safety_backup_id = await _create_safety_backup(ws_client, password, local_agent)
343
433
 
344
434
  # Perform restore
345
435
  restore_params = {
346
436
  "backup_id": backup_id,
347
- "agent_id": "hassio.local",
437
+ "agent_id": local_agent,
348
438
  "restore_database": restore_database,
349
439
  "restore_homeassistant": True,
350
440
  "restore_addons": [], # Restore all addons from backup
@@ -409,7 +499,7 @@ def register_backup_tools(mcp: "FastMCP", client: HomeAssistantClient, **kwargs:
409
499
 
410
500
  **Password:** Uses Home Assistant's default backup password (if configured)
411
501
 
412
- **Storage:** Local only (hassio.local agent)
502
+ **Storage:** Local only (the local backup agent — `hassio.local` on HA Supervised, `backup.local` on HA Core)
413
503
 
414
504
  **Duration:** Typically takes several seconds to complete (without database)
415
505
 
@@ -295,6 +295,17 @@ class IntegrationTools:
295
295
  try:
296
296
  result = await self._client.get_config_entry(entry_id)
297
297
  entry_domain = result.get("domain") if isinstance(result, dict) else None
298
+
299
+ # Surface `options` on every per-entry response (HA's REST endpoint
300
+ # omits the field). For entries with supports_options=True we probe
301
+ # via OptionsFlow — see `_fetch_entry_options`. When include_schema
302
+ # is also requested, `_fetch_options_schema` below populates options
303
+ # from the same flow init so we don't pay for two round-trips.
304
+ if isinstance(result, dict):
305
+ result.setdefault("options", {})
306
+ if result.get("supports_options") and not include_schema:
307
+ result["options"] = await self._fetch_entry_options(entry_id)
308
+
298
309
  resp: dict[str, Any] = {
299
310
  "success": True,
300
311
  "entry_id": entry_id,
@@ -326,21 +337,95 @@ class IntegrationTools:
326
337
  ],
327
338
  )
328
339
 
340
+ @staticmethod
341
+ def _options_from_form_flow(flow: dict[str, Any]) -> dict[str, Any]:
342
+ """Extract ``{field_name: current_value}`` from a form-type OptionsFlow.
343
+
344
+ Reads each ``data_schema`` entry's ``default`` key, falling back to
345
+ ``value`` only when the ``default`` key is absent (constant-type
346
+ fields ship ``value`` instead of ``default``). Fields with a missing
347
+ or ``None`` value are skipped.
348
+ """
349
+ out: dict[str, Any] = {}
350
+ for field in flow.get("data_schema") or []:
351
+ name = field.get("name")
352
+ if name is None:
353
+ continue
354
+ value = field.get("default", field.get("value"))
355
+ if value is not None:
356
+ out[name] = value
357
+ return out
358
+
359
+ async def _fetch_entry_options(self, entry_id: str) -> dict[str, Any]:
360
+ """Read the current ``options`` for a config entry via its OptionsFlow.
361
+
362
+ Home Assistant does not expose ``ConfigEntry.options`` through any
363
+ read-only REST or WebSocket endpoint — ``/api/config/config_entries/entry``
364
+ deliberately omits the field. The closest approximation that the HA UI
365
+ itself uses is the ``default`` values populated into the OptionsFlow's
366
+ first-step ``data_schema``: integrations build that schema from the
367
+ existing options dict, so the defaults match the persisted state.
368
+
369
+ Starts the flow, harvests ``{name: default}`` from the first step,
370
+ and aborts the flow in ``finally`` so it doesn't sit half-open.
371
+
372
+ Returns ``{}`` on any failure (unsupported entry, non-form first step
373
+ such as a menu, init/abort errors) so callers can treat the return as
374
+ the canonical "options" field without further checks. Unexpected
375
+ exception types are logged at ``warning`` so probe breakage is
376
+ discoverable.
377
+ """
378
+ flow_id: str | None = None
379
+ try:
380
+ flow = await self._client.start_options_flow(entry_id)
381
+ flow_id = flow.get("flow_id")
382
+ flow_type = flow.get("type")
383
+ if flow_type != "form":
384
+ logger.debug(
385
+ f"OptionsFlow for {entry_id} returned type={flow_type!r}, "
386
+ f"not a form — cannot extract option defaults"
387
+ )
388
+ return {}
389
+ return self._options_from_form_flow(flow)
390
+ except Exception as exc:
391
+ logger.warning(
392
+ f"Failed to fetch options for {entry_id}: "
393
+ f"{type(exc).__name__}: {exc}"
394
+ )
395
+ return {}
396
+ finally:
397
+ if flow_id:
398
+ try:
399
+ await self._client.abort_options_flow(flow_id)
400
+ except Exception as abort_err:
401
+ logger.warning(
402
+ f"Failed to abort options flow {flow_id}: "
403
+ f"{type(abort_err).__name__}: {abort_err}"
404
+ )
405
+
329
406
  async def _fetch_options_schema(
330
407
  self, entry_id: str, resp: dict[str, Any]
331
408
  ) -> None:
332
- """Start an options flow to read the schema, then abort it."""
409
+ """Start an options flow to read the schema, then abort it.
410
+
411
+ Also populates ``resp["entry"]["options"]`` for form-type flows from
412
+ the same flow result so callers requesting both schema and options
413
+ don't pay for two round-trips.
414
+ """
333
415
  flow_id = None
334
416
  try:
335
417
  flow_result = await self._client.start_options_flow(entry_id)
336
418
  flow_id = flow_result.get("flow_id")
337
419
  flow_type = flow_result.get("type")
420
+ entry = resp.get("entry") if isinstance(resp.get("entry"), dict) else None
338
421
  if flow_type == "form":
339
422
  resp["options_schema"] = {
340
423
  "flow_type": "form",
341
424
  "step_id": flow_result.get("step_id"),
342
425
  "data_schema": flow_result.get("data_schema", []),
343
426
  }
427
+ if entry is not None:
428
+ entry["options"] = self._options_from_form_flow(flow_result)
344
429
  elif flow_type == "menu":
345
430
  resp["options_schema"] = {
346
431
  "flow_type": "menu",
@@ -348,16 +433,18 @@ class IntegrationTools:
348
433
  "menu_options": flow_result.get("menu_options", []),
349
434
  }
350
435
  except Exception as schema_err:
351
- logger.debug(
352
- f"Failed to fetch options schema for {entry_id}: {schema_err}"
436
+ logger.warning(
437
+ f"Failed to fetch options schema for {entry_id}: "
438
+ f"{type(schema_err).__name__}: {schema_err}"
353
439
  )
354
440
  finally:
355
441
  if flow_id:
356
442
  try:
357
443
  await self._client.abort_options_flow(flow_id)
358
444
  except Exception as abort_err:
359
- logger.debug(
360
- f"Failed to abort options flow {flow_id}: {abort_err}"
445
+ logger.warning(
446
+ f"Failed to abort options flow {flow_id}: "
447
+ f"{type(abort_err).__name__}: {abort_err}"
361
448
  )
362
449
 
363
450
  async def _list_entries(
@@ -394,11 +481,28 @@ class IntegrationTools:
394
481
  # Fetch current logger levels once; enrich each entry with its effective level.
395
482
  logger_levels = await get_logger_levels(self._client)
396
483
 
397
- # Format entries for response
484
+ # `_format_entry` is sync and cannot probe the OptionsFlow; options
485
+ # are filled in by a second async pass below for entries that
486
+ # advertise supports_options=True. See `_fetch_entry_options`.
398
487
  formatted_entries = [
399
488
  self._format_entry(entry, include_opts, logger_levels) for entry in entries
400
489
  ]
401
490
 
491
+ if include_opts:
492
+ options_targets = [
493
+ e for e in formatted_entries if e.get("supports_options")
494
+ ]
495
+ if options_targets:
496
+ fetched = await asyncio.gather(
497
+ *(
498
+ self._fetch_entry_options(e["entry_id"])
499
+ for e in options_targets
500
+ ),
501
+ return_exceptions=False,
502
+ )
503
+ for entry, opts in zip(options_targets, fetched, strict=True):
504
+ entry["options"] = opts
505
+
402
506
  # Apply search filter if query provided
403
507
  if query and query.strip():
404
508
  formatted_entries = self._filter_by_query(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.4.1.dev484
3
+ Version: 7.4.1.dev486
4
4
  Summary: Home Assistant MCP Server - Complete control of Home Assistant through MCP
5
5
  Author-email: Julien <github@qc-h.net>
6
6
  License: MIT