ha-mcp-dev 6.3.1.dev137__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. ha_mcp/__init__.py +51 -0
  2. ha_mcp/__main__.py +753 -0
  3. ha_mcp/_pypi_marker +0 -0
  4. ha_mcp/auth/__init__.py +10 -0
  5. ha_mcp/auth/consent_form.py +501 -0
  6. ha_mcp/auth/provider.py +786 -0
  7. ha_mcp/client/__init__.py +10 -0
  8. ha_mcp/client/rest_client.py +924 -0
  9. ha_mcp/client/websocket_client.py +656 -0
  10. ha_mcp/client/websocket_listener.py +340 -0
  11. ha_mcp/config.py +175 -0
  12. ha_mcp/errors.py +399 -0
  13. ha_mcp/py.typed +0 -0
  14. ha_mcp/resources/card_types.json +48 -0
  15. ha_mcp/resources/dashboard_guide.md +573 -0
  16. ha_mcp/server.py +189 -0
  17. ha_mcp/smoke_test.py +108 -0
  18. ha_mcp/tools/__init__.py +11 -0
  19. ha_mcp/tools/backup.py +457 -0
  20. ha_mcp/tools/device_control.py +687 -0
  21. ha_mcp/tools/enhanced.py +191 -0
  22. ha_mcp/tools/helpers.py +184 -0
  23. ha_mcp/tools/registry.py +199 -0
  24. ha_mcp/tools/smart_search.py +797 -0
  25. ha_mcp/tools/tools_addons.py +343 -0
  26. ha_mcp/tools/tools_areas.py +478 -0
  27. ha_mcp/tools/tools_blueprints.py +279 -0
  28. ha_mcp/tools/tools_bug_report.py +570 -0
  29. ha_mcp/tools/tools_calendar.py +391 -0
  30. ha_mcp/tools/tools_camera.py +149 -0
  31. ha_mcp/tools/tools_config_automations.py +508 -0
  32. ha_mcp/tools/tools_config_dashboards.py +1502 -0
  33. ha_mcp/tools/tools_config_entry_flow.py +223 -0
  34. ha_mcp/tools/tools_config_helpers.py +925 -0
  35. ha_mcp/tools/tools_config_info.py +258 -0
  36. ha_mcp/tools/tools_config_scripts.py +267 -0
  37. ha_mcp/tools/tools_entities.py +76 -0
  38. ha_mcp/tools/tools_filesystem.py +589 -0
  39. ha_mcp/tools/tools_groups.py +306 -0
  40. ha_mcp/tools/tools_hacs.py +787 -0
  41. ha_mcp/tools/tools_history.py +714 -0
  42. ha_mcp/tools/tools_integrations.py +297 -0
  43. ha_mcp/tools/tools_labels.py +713 -0
  44. ha_mcp/tools/tools_mcp_component.py +305 -0
  45. ha_mcp/tools/tools_registry.py +1115 -0
  46. ha_mcp/tools/tools_resources.py +622 -0
  47. ha_mcp/tools/tools_search.py +573 -0
  48. ha_mcp/tools/tools_service.py +211 -0
  49. ha_mcp/tools/tools_services.py +352 -0
  50. ha_mcp/tools/tools_system.py +363 -0
  51. ha_mcp/tools/tools_todo.py +530 -0
  52. ha_mcp/tools/tools_traces.py +496 -0
  53. ha_mcp/tools/tools_updates.py +476 -0
  54. ha_mcp/tools/tools_utility.py +519 -0
  55. ha_mcp/tools/tools_voice_assistant.py +345 -0
  56. ha_mcp/tools/tools_zones.py +387 -0
  57. ha_mcp/tools/util_helpers.py +199 -0
  58. ha_mcp/utils/__init__.py +30 -0
  59. ha_mcp/utils/domain_handlers.py +380 -0
  60. ha_mcp/utils/fuzzy_search.py +339 -0
  61. ha_mcp/utils/operation_manager.py +442 -0
  62. ha_mcp/utils/usage_logger.py +290 -0
  63. ha_mcp_dev-6.3.1.dev137.dist-info/METADATA +229 -0
  64. ha_mcp_dev-6.3.1.dev137.dist-info/RECORD +71 -0
  65. ha_mcp_dev-6.3.1.dev137.dist-info/WHEEL +5 -0
  66. ha_mcp_dev-6.3.1.dev137.dist-info/entry_points.txt +7 -0
  67. ha_mcp_dev-6.3.1.dev137.dist-info/licenses/LICENSE +21 -0
  68. ha_mcp_dev-6.3.1.dev137.dist-info/top_level.txt +2 -0
  69. tests/__init__.py +1 -0
  70. tests/test_constants.py +15 -0
  71. tests/test_env_manager.py +336 -0
@@ -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