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,343 @@
1
+ """
2
+ Add-on management tools for Home Assistant MCP Server.
3
+
4
+ Provides tools to list installed and available add-ons via the Supervisor API.
5
+
6
+ Note: These tools only work with Home Assistant OS or Supervised installations.
7
+ """
8
+
9
+ import logging
10
+ from typing import Annotated, Any
11
+
12
+ from pydantic import Field
13
+
14
+ from ..client.rest_client import HomeAssistantClient
15
+ from .helpers import get_connected_ws_client, log_tool_usage
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ async def list_addons(
21
+ client: HomeAssistantClient, include_stats: bool = False
22
+ ) -> dict[str, Any]:
23
+ """
24
+ List installed Home Assistant add-ons.
25
+
26
+ Args:
27
+ client: Home Assistant REST client
28
+ include_stats: Include CPU/memory usage statistics
29
+
30
+ Returns:
31
+ Dictionary with installed add-ons and their status.
32
+ """
33
+ ws_client = None
34
+
35
+ try:
36
+ # Connect to WebSocket
37
+ ws_client, error = await get_connected_ws_client(client.base_url, client.token)
38
+ if error or ws_client is None:
39
+ return error or {
40
+ "success": False,
41
+ "error": "Failed to establish WebSocket connection",
42
+ }
43
+
44
+ # Call Supervisor API to get installed add-ons
45
+ result = await ws_client.send_command(
46
+ "supervisor/api",
47
+ endpoint="/addons",
48
+ method="GET",
49
+ )
50
+
51
+ if not result.get("success"):
52
+ # Check if this is a non-Supervisor installation
53
+ error_msg = str(result.get("error", ""))
54
+ if "not_found" in error_msg.lower() or "unknown" in error_msg.lower():
55
+ return {
56
+ "success": False,
57
+ "error": "Supervisor API not available",
58
+ "suggestion": "This feature requires Home Assistant OS or Supervised installation",
59
+ "details": result,
60
+ }
61
+ return {
62
+ "success": False,
63
+ "error": "Failed to retrieve add-ons list",
64
+ "details": result,
65
+ }
66
+
67
+ # Response structure: result.addons (not result.data.addons)
68
+ data = result.get("result", {})
69
+ addons = data.get("addons", [])
70
+
71
+ # Format add-on information
72
+ formatted_addons = []
73
+ for addon in addons:
74
+ addon_info = {
75
+ "name": addon.get("name"),
76
+ "slug": addon.get("slug"),
77
+ "description": addon.get("description"),
78
+ "version": addon.get("version"),
79
+ "installed": True,
80
+ "state": addon.get("state"),
81
+ "update_available": addon.get("update_available", False),
82
+ "repository": addon.get("repository"),
83
+ }
84
+
85
+ # Include stats if requested
86
+ if include_stats:
87
+ addon_info["stats"] = {
88
+ "cpu_percent": addon.get("cpu_percent"),
89
+ "memory_percent": addon.get("memory_percent"),
90
+ "memory_usage": addon.get("memory_usage"),
91
+ "memory_limit": addon.get("memory_limit"),
92
+ }
93
+
94
+ formatted_addons.append(addon_info)
95
+
96
+ # Count add-ons by state
97
+ running_count = sum(1 for a in addons if a.get("state") == "started")
98
+ update_count = sum(1 for a in addons if a.get("update_available"))
99
+
100
+ return {
101
+ "success": True,
102
+ "addons": formatted_addons,
103
+ "summary": {
104
+ "total_installed": len(formatted_addons),
105
+ "running": running_count,
106
+ "stopped": len(formatted_addons) - running_count,
107
+ "updates_available": update_count,
108
+ },
109
+ }
110
+
111
+ except Exception as e:
112
+ logger.error(f"Error listing add-ons: {e}")
113
+ return {
114
+ "success": False,
115
+ "error": f"Failed to list add-ons: {str(e)}",
116
+ "suggestion": "Check Home Assistant connection and Supervisor availability",
117
+ }
118
+ finally:
119
+ if ws_client:
120
+ try:
121
+ await ws_client.disconnect()
122
+ except Exception:
123
+ pass
124
+
125
+
126
+ async def list_available_addons(
127
+ client: HomeAssistantClient,
128
+ repository: str | None = None,
129
+ query: str | None = None,
130
+ ) -> dict[str, Any]:
131
+ """
132
+ List add-ons available in the add-on store.
133
+
134
+ Args:
135
+ client: Home Assistant REST client
136
+ repository: Filter by repository slug (e.g., "core", "community")
137
+ query: Search filter for add-on names/descriptions
138
+
139
+ Returns:
140
+ Dictionary with available add-ons and repositories.
141
+ """
142
+ ws_client = None
143
+
144
+ try:
145
+ # Connect to WebSocket
146
+ ws_client, error = await get_connected_ws_client(client.base_url, client.token)
147
+ if error or ws_client is None:
148
+ return error or {
149
+ "success": False,
150
+ "error": "Failed to establish WebSocket connection",
151
+ }
152
+
153
+ # Call Supervisor API to get store information
154
+ result = await ws_client.send_command(
155
+ "supervisor/api",
156
+ endpoint="/store",
157
+ method="GET",
158
+ )
159
+
160
+ if not result.get("success"):
161
+ # Check if this is a non-Supervisor installation
162
+ error_msg = str(result.get("error", ""))
163
+ if "not_found" in error_msg.lower() or "unknown" in error_msg.lower():
164
+ return {
165
+ "success": False,
166
+ "error": "Supervisor API not available",
167
+ "suggestion": "This feature requires Home Assistant OS or Supervised installation",
168
+ "details": result,
169
+ }
170
+ return {
171
+ "success": False,
172
+ "error": "Failed to retrieve add-on store",
173
+ "details": result,
174
+ }
175
+
176
+ # Response structure: result.addons/repositories (not result.data.*)
177
+ data = result.get("result", {})
178
+ repositories = data.get("repositories", [])
179
+ addons = data.get("addons", [])
180
+
181
+ # Format repository information
182
+ formatted_repos = []
183
+ for repo in repositories:
184
+ formatted_repos.append(
185
+ {
186
+ "slug": repo.get("slug"),
187
+ "name": repo.get("name"),
188
+ "source": repo.get("source"),
189
+ "maintainer": repo.get("maintainer"),
190
+ }
191
+ )
192
+
193
+ # Filter and format add-ons
194
+ formatted_addons = []
195
+ for addon in addons:
196
+ # Apply repository filter
197
+ if repository and addon.get("repository") != repository:
198
+ continue
199
+
200
+ # Apply search query filter
201
+ if query:
202
+ query_lower = query.lower()
203
+ name = (addon.get("name") or "").lower()
204
+ description = (addon.get("description") or "").lower()
205
+ if query_lower not in name and query_lower not in description:
206
+ continue
207
+
208
+ addon_info = {
209
+ "name": addon.get("name"),
210
+ "slug": addon.get("slug"),
211
+ "description": addon.get("description"),
212
+ "version": addon.get("version"),
213
+ "available": addon.get("available", True),
214
+ "installed": addon.get("installed", False),
215
+ "repository": addon.get("repository"),
216
+ "url": addon.get("url"),
217
+ "icon": addon.get("icon"),
218
+ "logo": addon.get("logo"),
219
+ }
220
+ formatted_addons.append(addon_info)
221
+
222
+ # Count statistics
223
+ installed_count = sum(1 for a in formatted_addons if a.get("installed"))
224
+
225
+ return {
226
+ "success": True,
227
+ "repositories": formatted_repos,
228
+ "addons": formatted_addons,
229
+ "summary": {
230
+ "total_available": len(formatted_addons),
231
+ "installed": installed_count,
232
+ "not_installed": len(formatted_addons) - installed_count,
233
+ "repository_count": len(formatted_repos),
234
+ },
235
+ "filters_applied": {
236
+ "repository": repository,
237
+ "query": query,
238
+ },
239
+ }
240
+
241
+ except Exception as e:
242
+ logger.error(f"Error listing available add-ons: {e}")
243
+ return {
244
+ "success": False,
245
+ "error": f"Failed to list available add-ons: {str(e)}",
246
+ "suggestion": "Check Home Assistant connection and Supervisor availability",
247
+ }
248
+ finally:
249
+ if ws_client:
250
+ try:
251
+ await ws_client.disconnect()
252
+ except Exception:
253
+ pass
254
+
255
+
256
+ def register_addon_tools(mcp: Any, client: HomeAssistantClient, **kwargs) -> None:
257
+ """
258
+ Register add-on management tools with the MCP server.
259
+
260
+ Args:
261
+ mcp: FastMCP server instance
262
+ client: Home Assistant REST client
263
+ **kwargs: Additional arguments (ignored, for auto-discovery compatibility)
264
+ """
265
+
266
+ @mcp.tool(annotations={"idempotentHint": True, "readOnlyHint": True, "tags": ["addon"], "title": "Get Add-ons"})
267
+ @log_tool_usage
268
+ async def ha_get_addon(
269
+ source: Annotated[
270
+ str | None,
271
+ Field(
272
+ description="Add-on source: 'installed' (default) for currently installed add-ons, "
273
+ "'available' for add-ons in the store that can be installed.",
274
+ default=None,
275
+ ),
276
+ ] = None,
277
+ include_stats: Annotated[
278
+ bool,
279
+ Field(
280
+ description="Include CPU/memory usage statistics (only for source='installed')",
281
+ default=False,
282
+ ),
283
+ ] = False,
284
+ repository: Annotated[
285
+ str | None,
286
+ Field(
287
+ description="Filter by repository slug, e.g., 'core', 'community' (only for source='available')",
288
+ default=None,
289
+ ),
290
+ ] = None,
291
+ query: Annotated[
292
+ str | None,
293
+ Field(
294
+ description="Search filter for add-on names/descriptions (only for source='available')",
295
+ default=None,
296
+ ),
297
+ ] = None,
298
+ ) -> dict[str, Any]:
299
+ """
300
+ Get Home Assistant add-ons - list installed or available from store.
301
+
302
+ This tool retrieves add-on information based on the source parameter:
303
+ - source='installed' (default): Lists currently installed add-ons
304
+ - source='available': Lists add-ons available in the add-on store
305
+
306
+ **Note:** This tool only works with Home Assistant OS or Supervised installations.
307
+
308
+ **INSTALLED ADD-ONS (source='installed'):**
309
+ Returns add-ons with version, state (started/stopped), and update availability.
310
+ - include_stats: Optionally include CPU/memory usage statistics
311
+
312
+ **AVAILABLE ADD-ONS (source='available'):**
313
+ Returns add-ons from official and custom repositories that can be installed.
314
+ - repository: Filter by repository slug (e.g., 'core', 'community')
315
+ - query: Search by name or description (case-insensitive)
316
+
317
+ **Example Usage:**
318
+ - List installed add-ons: ha_get_addon()
319
+ - List with resource usage: ha_get_addon(include_stats=True)
320
+ - List available add-ons: ha_get_addon(source="available")
321
+ - Search for MQTT: ha_get_addon(source="available", query="mqtt")
322
+ - List official add-ons: ha_get_addon(source="available", repository="core")
323
+
324
+ **Use Cases:**
325
+ - Check which add-ons are installed and running
326
+ - Monitor add-on health and resource usage
327
+ - Find add-ons with available updates
328
+ - Find add-ons to recommend for user's needs
329
+ - Check if a specific add-on is available
330
+ """
331
+ # Default to installed if not specified
332
+ effective_source = (source or "installed").lower()
333
+
334
+ if effective_source == "available":
335
+ return await list_available_addons(client, repository, query)
336
+ elif effective_source == "installed":
337
+ return await list_addons(client, include_stats)
338
+ else:
339
+ return {
340
+ "success": False,
341
+ "error": f"Invalid source: {source}. Must be 'installed' or 'available'.",
342
+ "valid_sources": ["installed", "available"],
343
+ }