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
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
"""
|
|
2
|
+
System management tools for Home Assistant MCP Server.
|
|
3
|
+
|
|
4
|
+
This module provides tools for Home Assistant system administration including:
|
|
5
|
+
- Configuration validation
|
|
6
|
+
- Service restarts and reloads
|
|
7
|
+
- System health monitoring
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from .helpers import get_connected_ws_client, log_tool_usage
|
|
14
|
+
from .util_helpers import coerce_bool_param
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
# Mapping of reload targets to their service domains and services
|
|
19
|
+
RELOAD_TARGETS = {
|
|
20
|
+
"all": None, # Special case - reload all
|
|
21
|
+
"automations": ("automation", "reload"),
|
|
22
|
+
"scripts": ("script", "reload"),
|
|
23
|
+
"scenes": ("scene", "reload"),
|
|
24
|
+
"groups": ("group", "reload"),
|
|
25
|
+
"input_booleans": ("input_boolean", "reload"),
|
|
26
|
+
"input_numbers": ("input_number", "reload"),
|
|
27
|
+
"input_texts": ("input_text", "reload"),
|
|
28
|
+
"input_selects": ("input_select", "reload"),
|
|
29
|
+
"input_datetimes": ("input_datetime", "reload"),
|
|
30
|
+
"input_buttons": ("input_button", "reload"),
|
|
31
|
+
"timers": ("timer", "reload"),
|
|
32
|
+
"counters": ("counter", "reload"),
|
|
33
|
+
"templates": ("template", "reload"),
|
|
34
|
+
"persons": ("person", "reload"),
|
|
35
|
+
"zones": ("zone", "reload"),
|
|
36
|
+
"core": ("homeassistant", "reload_core_config"),
|
|
37
|
+
"themes": ("frontend", "reload_themes"),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def register_system_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
42
|
+
"""Register Home Assistant system management tools."""
|
|
43
|
+
|
|
44
|
+
@mcp.tool(annotations={"idempotentHint": True, "readOnlyHint": True, "tags": ["system"], "title": "Check Configuration"})
|
|
45
|
+
@log_tool_usage
|
|
46
|
+
async def ha_check_config() -> dict[str, Any]:
|
|
47
|
+
"""
|
|
48
|
+
Check Home Assistant configuration for errors.
|
|
49
|
+
|
|
50
|
+
Validates configuration files without applying changes.
|
|
51
|
+
Always run this before ha_restart() to ensure configuration is valid.
|
|
52
|
+
"""
|
|
53
|
+
try:
|
|
54
|
+
config_result = await client.check_config()
|
|
55
|
+
|
|
56
|
+
# The API returns {"result": "valid"} or {"result": "invalid", "errors": [...]}
|
|
57
|
+
is_valid = config_result.get("result") == "valid"
|
|
58
|
+
errors = config_result.get("errors") or [] # Handle None case
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
"success": True,
|
|
62
|
+
"result": "valid" if is_valid else "invalid",
|
|
63
|
+
"is_valid": is_valid,
|
|
64
|
+
"errors": errors,
|
|
65
|
+
"message": (
|
|
66
|
+
"Configuration is valid"
|
|
67
|
+
if is_valid
|
|
68
|
+
else f"Configuration has {len(errors)} error(s)"
|
|
69
|
+
),
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
except Exception as e:
|
|
73
|
+
logger.error(f"Failed to check configuration: {e}")
|
|
74
|
+
return {
|
|
75
|
+
"success": False,
|
|
76
|
+
"error": f"Failed to check configuration: {str(e)}",
|
|
77
|
+
"suggestions": [
|
|
78
|
+
"Ensure Home Assistant is running and accessible",
|
|
79
|
+
"Check your connection settings",
|
|
80
|
+
],
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@mcp.tool(annotations={"destructiveHint": True, "tags": ["system"], "title": "Restart Home Assistant"})
|
|
84
|
+
@log_tool_usage
|
|
85
|
+
async def ha_restart(
|
|
86
|
+
confirm: bool | str = False,
|
|
87
|
+
) -> dict[str, Any]:
|
|
88
|
+
"""
|
|
89
|
+
Restart Home Assistant.
|
|
90
|
+
|
|
91
|
+
**WARNING: This will restart the entire Home Assistant instance!**
|
|
92
|
+
All automations will be temporarily unavailable during restart.
|
|
93
|
+
The restart typically takes 1-5 minutes depending on your setup.
|
|
94
|
+
|
|
95
|
+
**Parameters:**
|
|
96
|
+
- confirm: Must be set to True to confirm the restart. This is a safety
|
|
97
|
+
measure to prevent accidental restarts.
|
|
98
|
+
|
|
99
|
+
**Best Practices:**
|
|
100
|
+
1. Always run ha_check_config() first to ensure configuration is valid
|
|
101
|
+
2. Notify users before restarting (if applicable)
|
|
102
|
+
3. Schedule restarts during low-activity periods
|
|
103
|
+
|
|
104
|
+
**Example Usage:**
|
|
105
|
+
```python
|
|
106
|
+
# Always check config first
|
|
107
|
+
config = ha_check_config()
|
|
108
|
+
if config["result"] == "valid":
|
|
109
|
+
# Restart with confirmation
|
|
110
|
+
result = ha_restart(confirm=True)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**Alternative:** For configuration changes, consider using ha_reload_core()
|
|
114
|
+
instead, which reloads specific components without a full restart.
|
|
115
|
+
"""
|
|
116
|
+
# Coerce boolean parameter that may come as string from XML-style calls
|
|
117
|
+
confirm_bool = coerce_bool_param(confirm, "confirm", default=False) or False
|
|
118
|
+
|
|
119
|
+
if not confirm_bool:
|
|
120
|
+
return {
|
|
121
|
+
"success": False,
|
|
122
|
+
"error": "Restart not confirmed",
|
|
123
|
+
"message": (
|
|
124
|
+
"You must set confirm=True to restart Home Assistant. "
|
|
125
|
+
"This is a safety measure to prevent accidental restarts."
|
|
126
|
+
),
|
|
127
|
+
"suggestions": [
|
|
128
|
+
"Run ha_check_config() first to validate configuration",
|
|
129
|
+
"Call ha_restart(confirm=True) to proceed with restart",
|
|
130
|
+
"Consider using ha_reload_core() for config-only changes",
|
|
131
|
+
],
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
restart_initiated = False
|
|
135
|
+
try:
|
|
136
|
+
# Check configuration first as a safety measure
|
|
137
|
+
config_result = await client.check_config()
|
|
138
|
+
if config_result.get("result") != "valid":
|
|
139
|
+
errors = config_result.get("errors") or []
|
|
140
|
+
return {
|
|
141
|
+
"success": False,
|
|
142
|
+
"error": "Configuration is invalid - restart aborted",
|
|
143
|
+
"config_errors": errors,
|
|
144
|
+
"message": (
|
|
145
|
+
"Home Assistant configuration has errors. "
|
|
146
|
+
"Fix the errors before restarting."
|
|
147
|
+
),
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
# Call the restart service - mark as initiated before the call
|
|
151
|
+
# as the connection may be closed before we get a response
|
|
152
|
+
restart_initiated = True
|
|
153
|
+
await client.call_service("homeassistant", "restart", {})
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
"success": True,
|
|
157
|
+
"message": (
|
|
158
|
+
"Home Assistant restart initiated. "
|
|
159
|
+
"The system will be unavailable for 1-5 minutes."
|
|
160
|
+
),
|
|
161
|
+
"warning": (
|
|
162
|
+
"Connection will be lost during restart. "
|
|
163
|
+
"Wait for Home Assistant to become available again."
|
|
164
|
+
),
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
except Exception as e:
|
|
168
|
+
error_msg = str(e)
|
|
169
|
+
# Connection errors after restart initiated are expected
|
|
170
|
+
# (HA closes connections during restart)
|
|
171
|
+
if restart_initiated and (
|
|
172
|
+
"connect" in error_msg.lower() or "closed" in error_msg.lower()
|
|
173
|
+
):
|
|
174
|
+
return {
|
|
175
|
+
"success": True,
|
|
176
|
+
"message": (
|
|
177
|
+
"Home Assistant restart initiated. "
|
|
178
|
+
"Connection was closed as expected during restart."
|
|
179
|
+
),
|
|
180
|
+
"warning": "Wait 1-5 minutes for Home Assistant to restart.",
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
logger.error(f"Failed to restart Home Assistant: {e}")
|
|
184
|
+
return {
|
|
185
|
+
"success": False,
|
|
186
|
+
"error": f"Failed to restart Home Assistant: {str(e)}",
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
@mcp.tool(annotations={"destructiveHint": True, "tags": ["system"], "title": "Reload Core Components"})
|
|
190
|
+
@log_tool_usage
|
|
191
|
+
async def ha_reload_core(
|
|
192
|
+
target: str = "all",
|
|
193
|
+
) -> dict[str, Any]:
|
|
194
|
+
"""
|
|
195
|
+
Reload Home Assistant configuration without full restart.
|
|
196
|
+
|
|
197
|
+
This tool reloads specific configuration components, allowing changes
|
|
198
|
+
to take effect without restarting the entire Home Assistant instance.
|
|
199
|
+
This is much faster than a full restart.
|
|
200
|
+
|
|
201
|
+
**Parameters:**
|
|
202
|
+
- target: What to reload. Options:
|
|
203
|
+
- "all": Reload all reloadable components
|
|
204
|
+
- "automations": Reload automation configurations
|
|
205
|
+
- "scripts": Reload script configurations
|
|
206
|
+
- "scenes": Reload scene configurations
|
|
207
|
+
- "groups": Reload group configurations
|
|
208
|
+
- "input_booleans": Reload input_boolean helpers
|
|
209
|
+
- "input_numbers": Reload input_number helpers
|
|
210
|
+
- "input_texts": Reload input_text helpers
|
|
211
|
+
- "input_selects": Reload input_select helpers
|
|
212
|
+
- "input_datetimes": Reload input_datetime helpers
|
|
213
|
+
- "input_buttons": Reload input_button helpers
|
|
214
|
+
- "timers": Reload timer helpers
|
|
215
|
+
- "counters": Reload counter helpers
|
|
216
|
+
- "templates": Reload template sensors/entities
|
|
217
|
+
- "persons": Reload person configurations
|
|
218
|
+
- "zones": Reload zone configurations
|
|
219
|
+
- "core": Reload core configuration (customize, packages)
|
|
220
|
+
- "themes": Reload frontend themes
|
|
221
|
+
|
|
222
|
+
**Example Usage:**
|
|
223
|
+
```python
|
|
224
|
+
# Reload just automations after editing
|
|
225
|
+
ha_reload_core(target="automations")
|
|
226
|
+
|
|
227
|
+
# Reload all configurations
|
|
228
|
+
ha_reload_core(target="all")
|
|
229
|
+
|
|
230
|
+
# Reload input helpers after adding new ones
|
|
231
|
+
ha_reload_core(target="input_booleans")
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
**When to Use:**
|
|
235
|
+
- After editing automation/script YAML files
|
|
236
|
+
- After adding new input helpers via YAML
|
|
237
|
+
- After modifying customize.yaml
|
|
238
|
+
- After theme changes
|
|
239
|
+
"""
|
|
240
|
+
target = target.lower().strip()
|
|
241
|
+
|
|
242
|
+
if target not in RELOAD_TARGETS:
|
|
243
|
+
return {
|
|
244
|
+
"success": False,
|
|
245
|
+
"error": f"Invalid reload target: {target}",
|
|
246
|
+
"valid_targets": list(RELOAD_TARGETS.keys()),
|
|
247
|
+
"suggestion": f"Use one of: {', '.join(RELOAD_TARGETS.keys())}",
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
if target == "all":
|
|
252
|
+
# Reload all reloadable components
|
|
253
|
+
results = []
|
|
254
|
+
errors = []
|
|
255
|
+
|
|
256
|
+
for reload_target, service_info in RELOAD_TARGETS.items():
|
|
257
|
+
if service_info is None: # Skip "all" itself
|
|
258
|
+
continue
|
|
259
|
+
|
|
260
|
+
domain, service = service_info
|
|
261
|
+
try:
|
|
262
|
+
await client.call_service(domain, service, {})
|
|
263
|
+
results.append(reload_target)
|
|
264
|
+
except Exception as e:
|
|
265
|
+
# Some services might not be available in all installations
|
|
266
|
+
error_msg = str(e)
|
|
267
|
+
if "not found" not in error_msg.lower():
|
|
268
|
+
errors.append(f"{reload_target}: {error_msg}")
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
"success": True,
|
|
272
|
+
"message": f"Reloaded {len(results)} components",
|
|
273
|
+
"reloaded": results,
|
|
274
|
+
"warnings": errors if errors else None,
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
else:
|
|
278
|
+
# Reload specific component
|
|
279
|
+
service_info = RELOAD_TARGETS[target]
|
|
280
|
+
if service_info is None:
|
|
281
|
+
# This shouldn't happen as we check for "all" above
|
|
282
|
+
return {
|
|
283
|
+
"success": False,
|
|
284
|
+
"error": f"Invalid target configuration for: {target}",
|
|
285
|
+
}
|
|
286
|
+
domain, service = service_info
|
|
287
|
+
await client.call_service(domain, service, {})
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
"success": True,
|
|
291
|
+
"message": f"Successfully reloaded {target}",
|
|
292
|
+
"target": target,
|
|
293
|
+
"service": f"{domain}.{service}",
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
except Exception as e:
|
|
297
|
+
logger.error(f"Failed to reload {target}: {e}")
|
|
298
|
+
return {
|
|
299
|
+
"success": False,
|
|
300
|
+
"error": f"Failed to reload {target}: {str(e)}",
|
|
301
|
+
"suggestions": [
|
|
302
|
+
f"Ensure the {target} integration is loaded",
|
|
303
|
+
"Check Home Assistant logs for details",
|
|
304
|
+
],
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
@mcp.tool(annotations={"idempotentHint": True, "readOnlyHint": True, "tags": ["system"], "title": "Get System Health"})
|
|
308
|
+
@log_tool_usage
|
|
309
|
+
async def ha_get_system_health() -> dict[str, Any]:
|
|
310
|
+
"""
|
|
311
|
+
Get Home Assistant system health information.
|
|
312
|
+
|
|
313
|
+
Returns health check results from integrations, system resources, and connectivity.
|
|
314
|
+
Available information varies by installation type and loaded integrations.
|
|
315
|
+
"""
|
|
316
|
+
ws_client = None
|
|
317
|
+
|
|
318
|
+
try:
|
|
319
|
+
# Connect to WebSocket for system_health/info
|
|
320
|
+
ws_client, error = await get_connected_ws_client(
|
|
321
|
+
client.base_url, client.token
|
|
322
|
+
)
|
|
323
|
+
if error or ws_client is None:
|
|
324
|
+
return error or {
|
|
325
|
+
"success": False,
|
|
326
|
+
"error": "Failed to establish WebSocket connection",
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
# Request system health info via WebSocket
|
|
330
|
+
health_response = await ws_client.send_command("system_health/info")
|
|
331
|
+
|
|
332
|
+
if not health_response.get("success"):
|
|
333
|
+
return {
|
|
334
|
+
"success": False,
|
|
335
|
+
"error": "Failed to retrieve system health",
|
|
336
|
+
"details": health_response,
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
health_info = health_response.get("result") or {}
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
"success": True,
|
|
343
|
+
"health_info": health_info,
|
|
344
|
+
"component_count": len(health_info) if isinstance(health_info, dict) else 0,
|
|
345
|
+
"message": f"Retrieved health info for {len(health_info) if isinstance(health_info, dict) else 0} components",
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
except Exception as e:
|
|
349
|
+
logger.error(f"Failed to get system health: {e}")
|
|
350
|
+
return {
|
|
351
|
+
"success": False,
|
|
352
|
+
"error": f"Failed to get system health: {str(e)}",
|
|
353
|
+
"suggestions": [
|
|
354
|
+
"System health may not be available in all HA installations",
|
|
355
|
+
"Try ha_get_overview() for basic system information",
|
|
356
|
+
],
|
|
357
|
+
}
|
|
358
|
+
finally:
|
|
359
|
+
if ws_client:
|
|
360
|
+
try:
|
|
361
|
+
await ws_client.disconnect()
|
|
362
|
+
except Exception:
|
|
363
|
+
pass
|