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,787 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HACS (Home Assistant Community Store) integration tools for Home Assistant MCP server.
|
|
3
|
+
|
|
4
|
+
This module provides tools to interact with HACS via the WebSocket API, enabling AI agents
|
|
5
|
+
to discover custom integrations, Lovelace cards, themes, and more.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Annotated, Any, Literal
|
|
10
|
+
|
|
11
|
+
from pydantic import Field
|
|
12
|
+
|
|
13
|
+
from .helpers import exception_to_structured_error, log_tool_usage
|
|
14
|
+
from .util_helpers import add_timezone_metadata, coerce_int_param
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
# HACS uses different category names internally vs what users expect
|
|
19
|
+
# User-friendly name -> HACS internal name
|
|
20
|
+
CATEGORY_MAP = {
|
|
21
|
+
"lovelace": "plugin", # HACS calls Lovelace cards "plugin"
|
|
22
|
+
"integration": "integration",
|
|
23
|
+
"theme": "theme",
|
|
24
|
+
"appdaemon": "appdaemon",
|
|
25
|
+
"python_script": "python_script",
|
|
26
|
+
"template": "template",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
# Reverse mapping for display
|
|
30
|
+
CATEGORY_DISPLAY = {v: k for k, v in CATEGORY_MAP.items()}
|
|
31
|
+
CATEGORY_DISPLAY["plugin"] = "lovelace" # Display as lovelace for users
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async def _check_hacs_available(client) -> tuple[bool, str | None]:
|
|
35
|
+
"""
|
|
36
|
+
Check if HACS is installed and available via WebSocket.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Tuple of (is_available, error_message)
|
|
40
|
+
"""
|
|
41
|
+
try:
|
|
42
|
+
from ..client.websocket_client import get_websocket_client
|
|
43
|
+
ws_client = await get_websocket_client()
|
|
44
|
+
|
|
45
|
+
# Try to get HACS info to verify it's installed
|
|
46
|
+
response = await ws_client.send_command("hacs/info")
|
|
47
|
+
|
|
48
|
+
if response.get("success"):
|
|
49
|
+
return True, None
|
|
50
|
+
else:
|
|
51
|
+
return False, "HACS is installed but returned an error"
|
|
52
|
+
except Exception as e:
|
|
53
|
+
error_str = str(e).lower()
|
|
54
|
+
if "unknown command" in error_str or "not found" in error_str:
|
|
55
|
+
return False, "HACS is not installed or not loaded. Please install HACS from https://hacs.xyz/"
|
|
56
|
+
return False, f"Failed to connect to HACS: {str(e)}"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def register_hacs_tools(mcp, client, **kwargs):
|
|
60
|
+
"""Register HACS integration tools with the MCP server."""
|
|
61
|
+
|
|
62
|
+
@mcp.tool(annotations={"idempotentHint": True, "readOnlyHint": True, "tags": ["hacs", "info"], "title": "Get HACS Info"})
|
|
63
|
+
@log_tool_usage
|
|
64
|
+
async def ha_hacs_info() -> dict[str, Any]:
|
|
65
|
+
"""Get HACS status, version, and enabled categories.
|
|
66
|
+
|
|
67
|
+
Returns information about the HACS installation including:
|
|
68
|
+
- Version number
|
|
69
|
+
- Enabled categories (integration, lovelace, theme, etc.)
|
|
70
|
+
- Stage (running, startup, etc.)
|
|
71
|
+
- Lovelace mode
|
|
72
|
+
- Disabled reason (if any)
|
|
73
|
+
|
|
74
|
+
This is useful for validating that HACS is installed and operational
|
|
75
|
+
before using other HACS tools.
|
|
76
|
+
|
|
77
|
+
**HACS Installation:**
|
|
78
|
+
If HACS is not installed, visit https://hacs.xyz/ for installation instructions.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Dictionary with HACS status information or error if HACS is not available.
|
|
82
|
+
"""
|
|
83
|
+
try:
|
|
84
|
+
# Check if HACS is available
|
|
85
|
+
is_available, error_msg = await _check_hacs_available(client)
|
|
86
|
+
if not is_available:
|
|
87
|
+
return await add_timezone_metadata(client, {
|
|
88
|
+
"success": False,
|
|
89
|
+
"error": error_msg,
|
|
90
|
+
"error_code": "HACS_NOT_AVAILABLE",
|
|
91
|
+
"suggestions": [
|
|
92
|
+
"Install HACS from https://hacs.xyz/",
|
|
93
|
+
"Ensure Home Assistant has been restarted after HACS installation",
|
|
94
|
+
"Check Home Assistant logs for HACS errors",
|
|
95
|
+
],
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
# Get HACS info via WebSocket
|
|
99
|
+
from ..client.websocket_client import get_websocket_client
|
|
100
|
+
ws_client = await get_websocket_client()
|
|
101
|
+
response = await ws_client.send_command("hacs/info")
|
|
102
|
+
|
|
103
|
+
if not response.get("success"):
|
|
104
|
+
error_response = exception_to_structured_error(
|
|
105
|
+
Exception(f"HACS info request failed: {response}"),
|
|
106
|
+
context={"command": "hacs/info"},
|
|
107
|
+
)
|
|
108
|
+
return await add_timezone_metadata(client, error_response)
|
|
109
|
+
|
|
110
|
+
result = response.get("result", {})
|
|
111
|
+
|
|
112
|
+
return await add_timezone_metadata(client, {
|
|
113
|
+
"success": True,
|
|
114
|
+
"version": result.get("version"),
|
|
115
|
+
"categories": result.get("categories", []),
|
|
116
|
+
"stage": result.get("stage"),
|
|
117
|
+
"lovelace_mode": result.get("lovelace_mode"),
|
|
118
|
+
"disabled_reason": result.get("disabled_reason"),
|
|
119
|
+
"data": result,
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
except Exception as e:
|
|
123
|
+
error_response = exception_to_structured_error(
|
|
124
|
+
e,
|
|
125
|
+
context={"tool": "ha_hacs_info"},
|
|
126
|
+
)
|
|
127
|
+
if "error" in error_response and isinstance(error_response["error"], dict):
|
|
128
|
+
error_response["error"]["suggestions"] = [
|
|
129
|
+
"Verify HACS is installed: https://hacs.xyz/",
|
|
130
|
+
"Check Home Assistant connection",
|
|
131
|
+
"Restart Home Assistant if HACS was recently installed",
|
|
132
|
+
]
|
|
133
|
+
return await add_timezone_metadata(client, error_response)
|
|
134
|
+
|
|
135
|
+
@mcp.tool(annotations={"idempotentHint": True, "readOnlyHint": True, "tags": ["hacs", "search"], "title": "List HACS Installed"})
|
|
136
|
+
@log_tool_usage
|
|
137
|
+
async def ha_hacs_list_installed(
|
|
138
|
+
category: Annotated[
|
|
139
|
+
Literal["integration", "lovelace", "theme", "appdaemon", "python_script"] | None,
|
|
140
|
+
Field(
|
|
141
|
+
default=None,
|
|
142
|
+
description=(
|
|
143
|
+
"Filter by category: 'integration', 'lovelace', 'theme', "
|
|
144
|
+
"'appdaemon', or 'python_script'. Use None for all categories."
|
|
145
|
+
),
|
|
146
|
+
),
|
|
147
|
+
] = None,
|
|
148
|
+
) -> dict[str, Any]:
|
|
149
|
+
"""List installed HACS repositories with focused, small response.
|
|
150
|
+
|
|
151
|
+
**DASHBOARD TIP:** Use `category="lovelace"` to discover installed custom cards
|
|
152
|
+
for use with `ha_config_set_dashboard()`.
|
|
153
|
+
|
|
154
|
+
Returns a list of installed repositories with key information:
|
|
155
|
+
- name: Repository name
|
|
156
|
+
- full_name: Full GitHub repository name (owner/repo)
|
|
157
|
+
- category: Type of repository (integration, lovelace, theme, etc.)
|
|
158
|
+
- installed_version: Currently installed version
|
|
159
|
+
- available_version: Latest available version
|
|
160
|
+
- pending_update: Whether an update is available
|
|
161
|
+
- description: Repository description
|
|
162
|
+
|
|
163
|
+
**Categories:**
|
|
164
|
+
- `integration`: Custom integrations and components
|
|
165
|
+
- `lovelace`: Custom dashboard cards and panels
|
|
166
|
+
- `theme`: Custom themes for the UI
|
|
167
|
+
- `appdaemon`: AppDaemon apps
|
|
168
|
+
- `python_script`: Python scripts
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
category: Filter results by category (default: all categories)
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
List of installed HACS repositories or error if HACS is not available.
|
|
175
|
+
"""
|
|
176
|
+
try:
|
|
177
|
+
# Check if HACS is available
|
|
178
|
+
is_available, error_msg = await _check_hacs_available(client)
|
|
179
|
+
if not is_available:
|
|
180
|
+
return await add_timezone_metadata(client, {
|
|
181
|
+
"success": False,
|
|
182
|
+
"error": error_msg,
|
|
183
|
+
"error_code": "HACS_NOT_AVAILABLE",
|
|
184
|
+
"suggestions": [
|
|
185
|
+
"Install HACS from https://hacs.xyz/",
|
|
186
|
+
"Ensure Home Assistant has been restarted after HACS installation",
|
|
187
|
+
"Check Home Assistant logs for HACS errors",
|
|
188
|
+
],
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
# Get installed repositories via WebSocket
|
|
192
|
+
from ..client.websocket_client import get_websocket_client
|
|
193
|
+
ws_client = await get_websocket_client()
|
|
194
|
+
|
|
195
|
+
# Build command parameters - map user-friendly category to HACS internal name
|
|
196
|
+
kwargs_cmd: dict[str, Any] = {}
|
|
197
|
+
if category:
|
|
198
|
+
hacs_category = CATEGORY_MAP.get(category, category)
|
|
199
|
+
kwargs_cmd["categories"] = [hacs_category]
|
|
200
|
+
|
|
201
|
+
response = await ws_client.send_command("hacs/repositories/list", **kwargs_cmd)
|
|
202
|
+
|
|
203
|
+
if not response.get("success"):
|
|
204
|
+
error_response = exception_to_structured_error(
|
|
205
|
+
Exception(f"HACS repositories list request failed: {response}"),
|
|
206
|
+
context={"command": "hacs/repositories/list", "category": category},
|
|
207
|
+
)
|
|
208
|
+
return await add_timezone_metadata(client, error_response)
|
|
209
|
+
|
|
210
|
+
repositories = response.get("result", [])
|
|
211
|
+
|
|
212
|
+
# Filter to only installed repositories and extract key info
|
|
213
|
+
installed = []
|
|
214
|
+
for repo in repositories:
|
|
215
|
+
if repo.get("installed", False):
|
|
216
|
+
# Map HACS internal category back to user-friendly name
|
|
217
|
+
repo_category = repo.get("category", "")
|
|
218
|
+
display_category = CATEGORY_DISPLAY.get(repo_category, repo_category)
|
|
219
|
+
installed.append({
|
|
220
|
+
"name": repo.get("name"),
|
|
221
|
+
"full_name": repo.get("full_name"),
|
|
222
|
+
"category": display_category,
|
|
223
|
+
"id": repo.get("id"), # Include numeric ID for repository_info
|
|
224
|
+
"installed_version": repo.get("installed_version"),
|
|
225
|
+
"available_version": repo.get("available_version"),
|
|
226
|
+
"pending_update": repo.get("pending_upgrade", False),
|
|
227
|
+
"description": repo.get("description"),
|
|
228
|
+
"authors": repo.get("authors", []),
|
|
229
|
+
"domain": repo.get("domain"), # For integrations
|
|
230
|
+
"stars": repo.get("stars", 0),
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
return await add_timezone_metadata(client, {
|
|
234
|
+
"success": True,
|
|
235
|
+
"category_filter": category,
|
|
236
|
+
"total_installed": len(installed),
|
|
237
|
+
"repositories": installed,
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
except Exception as e:
|
|
241
|
+
error_response = exception_to_structured_error(
|
|
242
|
+
e,
|
|
243
|
+
context={"tool": "ha_hacs_list_installed", "category": category},
|
|
244
|
+
)
|
|
245
|
+
if "error" in error_response and isinstance(error_response["error"], dict):
|
|
246
|
+
error_response["error"]["suggestions"] = [
|
|
247
|
+
"Verify HACS is installed: https://hacs.xyz/",
|
|
248
|
+
"Check category name is valid: integration, lovelace, theme, appdaemon, python_script",
|
|
249
|
+
"Check Home Assistant connection",
|
|
250
|
+
]
|
|
251
|
+
return await add_timezone_metadata(client, error_response)
|
|
252
|
+
|
|
253
|
+
@mcp.tool(annotations={"idempotentHint": True, "readOnlyHint": True, "tags": ["hacs", "search"], "title": "Search HACS Store"})
|
|
254
|
+
@log_tool_usage
|
|
255
|
+
async def ha_hacs_search(
|
|
256
|
+
query: str,
|
|
257
|
+
category: Annotated[
|
|
258
|
+
Literal["integration", "lovelace", "theme", "appdaemon", "python_script"] | None,
|
|
259
|
+
Field(
|
|
260
|
+
default=None,
|
|
261
|
+
description="Filter by category (optional)",
|
|
262
|
+
),
|
|
263
|
+
] = None,
|
|
264
|
+
max_results: Annotated[
|
|
265
|
+
int | str,
|
|
266
|
+
Field(
|
|
267
|
+
default=10,
|
|
268
|
+
description="Maximum number of results to return (default: 10, max: 100)",
|
|
269
|
+
),
|
|
270
|
+
] = 10,
|
|
271
|
+
) -> dict[str, Any]:
|
|
272
|
+
"""Search HACS store for repositories by keyword with pagination.
|
|
273
|
+
|
|
274
|
+
Searches the HACS store for repositories matching the query string.
|
|
275
|
+
Returns repository information including stars, downloads, and descriptions.
|
|
276
|
+
|
|
277
|
+
**Use Cases:**
|
|
278
|
+
- Find custom cards: `ha_hacs_search("mushroom", category="lovelace")`
|
|
279
|
+
- Find integrations: `ha_hacs_search("nest", category="integration")`
|
|
280
|
+
- Browse themes: `ha_hacs_search("dark", category="theme")`
|
|
281
|
+
|
|
282
|
+
Results include:
|
|
283
|
+
- name: Repository name
|
|
284
|
+
- full_name: Full GitHub repository name
|
|
285
|
+
- description: Repository description
|
|
286
|
+
- category: Type of repository
|
|
287
|
+
- stars: GitHub stars count
|
|
288
|
+
- downloads: Number of HACS installations
|
|
289
|
+
- authors: Repository authors
|
|
290
|
+
- installed: Whether already installed
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
query: Search query (repository name, description, author)
|
|
294
|
+
category: Filter by category (optional)
|
|
295
|
+
max_results: Maximum results to return (default: 10, max: 100)
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
Search results from HACS store or error if HACS is not available.
|
|
299
|
+
"""
|
|
300
|
+
try:
|
|
301
|
+
# Coerce max_results to int
|
|
302
|
+
max_results_int = coerce_int_param(
|
|
303
|
+
max_results,
|
|
304
|
+
"max_results",
|
|
305
|
+
default=10,
|
|
306
|
+
min_value=1,
|
|
307
|
+
max_value=100,
|
|
308
|
+
) or 10
|
|
309
|
+
|
|
310
|
+
# Check if HACS is available
|
|
311
|
+
is_available, error_msg = await _check_hacs_available(client)
|
|
312
|
+
if not is_available:
|
|
313
|
+
return await add_timezone_metadata(client, {
|
|
314
|
+
"success": False,
|
|
315
|
+
"error": error_msg,
|
|
316
|
+
"error_code": "HACS_NOT_AVAILABLE",
|
|
317
|
+
"suggestions": [
|
|
318
|
+
"Install HACS from https://hacs.xyz/",
|
|
319
|
+
"Ensure Home Assistant has been restarted after HACS installation",
|
|
320
|
+
"Check Home Assistant logs for HACS errors",
|
|
321
|
+
],
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
# Get all repositories via WebSocket
|
|
325
|
+
from ..client.websocket_client import get_websocket_client
|
|
326
|
+
ws_client = await get_websocket_client()
|
|
327
|
+
|
|
328
|
+
# Build command parameters - map user-friendly category to HACS internal name
|
|
329
|
+
kwargs_cmd: dict[str, Any] = {}
|
|
330
|
+
if category:
|
|
331
|
+
hacs_category = CATEGORY_MAP.get(category, category)
|
|
332
|
+
kwargs_cmd["categories"] = [hacs_category]
|
|
333
|
+
|
|
334
|
+
response = await ws_client.send_command("hacs/repositories/list", **kwargs_cmd)
|
|
335
|
+
|
|
336
|
+
if not response.get("success"):
|
|
337
|
+
error_response = exception_to_structured_error(
|
|
338
|
+
Exception(f"HACS search request failed: {response}"),
|
|
339
|
+
context={"command": "hacs/repositories/list", "query": query, "category": category},
|
|
340
|
+
)
|
|
341
|
+
return await add_timezone_metadata(client, error_response)
|
|
342
|
+
|
|
343
|
+
all_repositories = response.get("result", [])
|
|
344
|
+
|
|
345
|
+
# Simple search: filter by query string in name, description, or authors
|
|
346
|
+
query_lower = query.lower().strip()
|
|
347
|
+
matches = []
|
|
348
|
+
|
|
349
|
+
for repo in all_repositories:
|
|
350
|
+
# Handle None values safely
|
|
351
|
+
name = (repo.get("name") or "").lower()
|
|
352
|
+
description = (repo.get("description") or "").lower()
|
|
353
|
+
full_name = (repo.get("full_name") or "").lower()
|
|
354
|
+
authors_list = repo.get("authors") or []
|
|
355
|
+
authors = " ".join(authors_list).lower()
|
|
356
|
+
|
|
357
|
+
# Calculate relevance score
|
|
358
|
+
score = 0
|
|
359
|
+
if query_lower in name:
|
|
360
|
+
score += 100
|
|
361
|
+
if query_lower in full_name:
|
|
362
|
+
score += 50
|
|
363
|
+
if query_lower in description:
|
|
364
|
+
score += 30
|
|
365
|
+
if query_lower in authors:
|
|
366
|
+
score += 20
|
|
367
|
+
|
|
368
|
+
if score > 0:
|
|
369
|
+
# Map HACS internal category back to user-friendly name
|
|
370
|
+
repo_category = repo.get("category", "")
|
|
371
|
+
display_category = CATEGORY_DISPLAY.get(repo_category, repo_category)
|
|
372
|
+
matches.append({
|
|
373
|
+
"name": repo.get("name"),
|
|
374
|
+
"full_name": repo.get("full_name"),
|
|
375
|
+
"description": repo.get("description"),
|
|
376
|
+
"category": display_category,
|
|
377
|
+
"id": repo.get("id"), # Include numeric ID for repository_info
|
|
378
|
+
"stars": repo.get("stars", 0),
|
|
379
|
+
"downloads": repo.get("downloads", 0),
|
|
380
|
+
"authors": authors_list,
|
|
381
|
+
"installed": repo.get("installed", False),
|
|
382
|
+
"installed_version": repo.get("installed_version") if repo.get("installed") else None,
|
|
383
|
+
"available_version": repo.get("available_version"),
|
|
384
|
+
"score": score,
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
# Sort by score (descending) and limit results
|
|
388
|
+
matches.sort(key=lambda x: x["score"], reverse=True)
|
|
389
|
+
limited_matches = matches[:max_results_int]
|
|
390
|
+
|
|
391
|
+
return await add_timezone_metadata(client, {
|
|
392
|
+
"success": True,
|
|
393
|
+
"query": query,
|
|
394
|
+
"category_filter": category,
|
|
395
|
+
"total_matches": len(matches),
|
|
396
|
+
"results_returned": len(limited_matches),
|
|
397
|
+
"results": limited_matches,
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
except Exception as e:
|
|
401
|
+
error_response = exception_to_structured_error(
|
|
402
|
+
e,
|
|
403
|
+
context={"tool": "ha_hacs_search", "query": query, "category": category},
|
|
404
|
+
)
|
|
405
|
+
if "error" in error_response and isinstance(error_response["error"], dict):
|
|
406
|
+
error_response["error"]["suggestions"] = [
|
|
407
|
+
"Verify HACS is installed: https://hacs.xyz/",
|
|
408
|
+
"Try a simpler search query",
|
|
409
|
+
"Check category name is valid: integration, lovelace, theme, appdaemon, python_script",
|
|
410
|
+
]
|
|
411
|
+
return await add_timezone_metadata(client, error_response)
|
|
412
|
+
|
|
413
|
+
@mcp.tool(annotations={"idempotentHint": True, "readOnlyHint": True, "tags": ["hacs", "info"], "title": "Get HACS Repository Info"})
|
|
414
|
+
@log_tool_usage
|
|
415
|
+
async def ha_hacs_repository_info(repository_id: str) -> dict[str, Any]:
|
|
416
|
+
"""Get detailed repository information including README and documentation.
|
|
417
|
+
|
|
418
|
+
Returns comprehensive information about a HACS repository:
|
|
419
|
+
- Basic info (name, description, category, authors)
|
|
420
|
+
- Installation status and versions
|
|
421
|
+
- README content (useful for configuration examples)
|
|
422
|
+
- Available releases and versions
|
|
423
|
+
- GitHub stats (stars, issues)
|
|
424
|
+
- Configuration examples (if available)
|
|
425
|
+
|
|
426
|
+
**Use Cases:**
|
|
427
|
+
- Get card configuration examples: `ha_hacs_repository_info("441028036")`
|
|
428
|
+
- Check integration setup instructions
|
|
429
|
+
- Find theme customization options
|
|
430
|
+
|
|
431
|
+
**Note:** The repository_id is the numeric ID from HACS, not the GitHub path.
|
|
432
|
+
Use `ha_hacs_list_installed()` or `ha_hacs_search()` to find the numeric ID.
|
|
433
|
+
|
|
434
|
+
Args:
|
|
435
|
+
repository_id: Repository numeric ID (e.g., "441028036") or GitHub path (e.g., "dvd-dev/hilo")
|
|
436
|
+
|
|
437
|
+
Returns:
|
|
438
|
+
Detailed repository information or error if not found.
|
|
439
|
+
"""
|
|
440
|
+
try:
|
|
441
|
+
# Check if HACS is available
|
|
442
|
+
is_available, error_msg = await _check_hacs_available(client)
|
|
443
|
+
if not is_available:
|
|
444
|
+
return await add_timezone_metadata(client, {
|
|
445
|
+
"success": False,
|
|
446
|
+
"error": error_msg,
|
|
447
|
+
"error_code": "HACS_NOT_AVAILABLE",
|
|
448
|
+
"suggestions": [
|
|
449
|
+
"Install HACS from https://hacs.xyz/",
|
|
450
|
+
"Ensure Home Assistant has been restarted after HACS installation",
|
|
451
|
+
"Check Home Assistant logs for HACS errors",
|
|
452
|
+
],
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
from ..client.websocket_client import get_websocket_client
|
|
456
|
+
ws_client = await get_websocket_client()
|
|
457
|
+
|
|
458
|
+
# If repository_id contains a slash, it's a GitHub path - need to look up numeric ID
|
|
459
|
+
actual_id = repository_id
|
|
460
|
+
if "/" in repository_id:
|
|
461
|
+
# Look up the numeric ID from the repository list
|
|
462
|
+
list_response = await ws_client.send_command("hacs/repositories/list")
|
|
463
|
+
if list_response.get("success"):
|
|
464
|
+
repos = list_response.get("result", [])
|
|
465
|
+
for repo in repos:
|
|
466
|
+
if repo.get("full_name", "").lower() == repository_id.lower():
|
|
467
|
+
actual_id = str(repo.get("id"))
|
|
468
|
+
break
|
|
469
|
+
else:
|
|
470
|
+
return await add_timezone_metadata(client, {
|
|
471
|
+
"success": False,
|
|
472
|
+
"error": f"Repository '{repository_id}' not found in HACS",
|
|
473
|
+
"error_code": "REPOSITORY_NOT_FOUND",
|
|
474
|
+
"suggestions": [
|
|
475
|
+
"Use ha_hacs_search() to find the repository",
|
|
476
|
+
"Check the repository name is correct (case-insensitive)",
|
|
477
|
+
"The repository may need to be added to HACS first",
|
|
478
|
+
],
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
# Get repository info via WebSocket using numeric ID
|
|
482
|
+
response = await ws_client.send_command("hacs/repository/info", repository_id=actual_id)
|
|
483
|
+
|
|
484
|
+
if not response.get("success"):
|
|
485
|
+
error_response = exception_to_structured_error(
|
|
486
|
+
Exception(f"HACS repository info request failed: {response}"),
|
|
487
|
+
context={"command": "hacs/repository/info", "repository_id": repository_id},
|
|
488
|
+
)
|
|
489
|
+
return await add_timezone_metadata(client, error_response)
|
|
490
|
+
|
|
491
|
+
result = response.get("result", {})
|
|
492
|
+
|
|
493
|
+
# Extract and structure the most useful information
|
|
494
|
+
return await add_timezone_metadata(client, {
|
|
495
|
+
"success": True,
|
|
496
|
+
"repository_id": repository_id,
|
|
497
|
+
"name": result.get("name"),
|
|
498
|
+
"full_name": result.get("full_name"),
|
|
499
|
+
"description": result.get("description"),
|
|
500
|
+
"category": result.get("category"),
|
|
501
|
+
"authors": result.get("authors", []),
|
|
502
|
+
"domain": result.get("domain"), # For integrations
|
|
503
|
+
"installed": result.get("installed", False),
|
|
504
|
+
"installed_version": result.get("installed_version"),
|
|
505
|
+
"available_version": result.get("available_version"),
|
|
506
|
+
"pending_update": result.get("pending_upgrade", False),
|
|
507
|
+
"stars": result.get("stars", 0),
|
|
508
|
+
"downloads": result.get("downloads", 0),
|
|
509
|
+
"topics": result.get("topics", []),
|
|
510
|
+
"releases": result.get("releases", []),
|
|
511
|
+
"default_branch": result.get("default_branch"),
|
|
512
|
+
"readme": result.get("readme"), # Full README content
|
|
513
|
+
"data": result, # Full response for advanced use
|
|
514
|
+
})
|
|
515
|
+
|
|
516
|
+
except Exception as e:
|
|
517
|
+
error_response = exception_to_structured_error(
|
|
518
|
+
e,
|
|
519
|
+
context={"tool": "ha_hacs_repository_info", "repository_id": repository_id},
|
|
520
|
+
)
|
|
521
|
+
if "error" in error_response and isinstance(error_response["error"], dict):
|
|
522
|
+
error_response["error"]["suggestions"] = [
|
|
523
|
+
"Verify HACS is installed: https://hacs.xyz/",
|
|
524
|
+
"Check repository ID format (e.g., 'hacs/integration' or 'owner/repo')",
|
|
525
|
+
"Use ha_hacs_search() to find the correct repository ID",
|
|
526
|
+
]
|
|
527
|
+
return await add_timezone_metadata(client, error_response)
|
|
528
|
+
|
|
529
|
+
@mcp.tool(annotations={"destructiveHint": True, "tags": ["hacs", "management"], "title": "Add HACS Repository"})
|
|
530
|
+
@log_tool_usage
|
|
531
|
+
async def ha_hacs_add_repository(
|
|
532
|
+
repository: str,
|
|
533
|
+
category: Annotated[
|
|
534
|
+
Literal["integration", "lovelace", "theme", "appdaemon", "python_script"],
|
|
535
|
+
Field(
|
|
536
|
+
description="Repository category (required)",
|
|
537
|
+
),
|
|
538
|
+
],
|
|
539
|
+
) -> dict[str, Any]:
|
|
540
|
+
"""Add a custom GitHub repository to HACS.
|
|
541
|
+
|
|
542
|
+
Allows adding custom repositories that are not in the default HACS store.
|
|
543
|
+
This is useful for:
|
|
544
|
+
- Adding custom integrations from GitHub
|
|
545
|
+
- Installing custom Lovelace cards
|
|
546
|
+
- Adding custom themes
|
|
547
|
+
- Installing beta/development versions
|
|
548
|
+
|
|
549
|
+
**Requirements:**
|
|
550
|
+
- Repository must be a valid GitHub repository
|
|
551
|
+
- Repository must follow HACS structure guidelines
|
|
552
|
+
- Category must match the repository type
|
|
553
|
+
|
|
554
|
+
**Examples:**
|
|
555
|
+
```python
|
|
556
|
+
# Add custom integration
|
|
557
|
+
ha_hacs_add_repository("owner/custom-integration", category="integration")
|
|
558
|
+
|
|
559
|
+
# Add custom card
|
|
560
|
+
ha_hacs_add_repository("owner/custom-card", category="lovelace")
|
|
561
|
+
|
|
562
|
+
# Add custom theme
|
|
563
|
+
ha_hacs_add_repository("owner/custom-theme", category="theme")
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
Args:
|
|
567
|
+
repository: GitHub repository in format "owner/repo"
|
|
568
|
+
category: Repository category (integration, lovelace, theme, appdaemon, python_script)
|
|
569
|
+
|
|
570
|
+
Returns:
|
|
571
|
+
Success status and repository ID if added successfully.
|
|
572
|
+
"""
|
|
573
|
+
try:
|
|
574
|
+
# Check if HACS is available
|
|
575
|
+
is_available, error_msg = await _check_hacs_available(client)
|
|
576
|
+
if not is_available:
|
|
577
|
+
return await add_timezone_metadata(client, {
|
|
578
|
+
"success": False,
|
|
579
|
+
"error": error_msg,
|
|
580
|
+
"error_code": "HACS_NOT_AVAILABLE",
|
|
581
|
+
"suggestions": [
|
|
582
|
+
"Install HACS from https://hacs.xyz/",
|
|
583
|
+
"Ensure Home Assistant has been restarted after HACS installation",
|
|
584
|
+
"Check Home Assistant logs for HACS errors",
|
|
585
|
+
],
|
|
586
|
+
})
|
|
587
|
+
|
|
588
|
+
# Validate repository format
|
|
589
|
+
if "/" not in repository:
|
|
590
|
+
return await add_timezone_metadata(client, {
|
|
591
|
+
"success": False,
|
|
592
|
+
"error": "Invalid repository format. Must be 'owner/repo'",
|
|
593
|
+
"error_code": "INVALID_REPOSITORY_FORMAT",
|
|
594
|
+
"suggestions": [
|
|
595
|
+
"Use format: 'owner/repo' (e.g., 'hacs/integration')",
|
|
596
|
+
"Check the repository exists on GitHub",
|
|
597
|
+
],
|
|
598
|
+
})
|
|
599
|
+
|
|
600
|
+
# Add repository via WebSocket
|
|
601
|
+
from ..client.websocket_client import get_websocket_client
|
|
602
|
+
ws_client = await get_websocket_client()
|
|
603
|
+
|
|
604
|
+
# Map user-friendly category to HACS internal name
|
|
605
|
+
hacs_category = CATEGORY_MAP.get(category, category)
|
|
606
|
+
|
|
607
|
+
response = await ws_client.send_command(
|
|
608
|
+
"hacs/repositories/add",
|
|
609
|
+
repository=repository,
|
|
610
|
+
category=hacs_category,
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
if not response.get("success"):
|
|
614
|
+
error_response = exception_to_structured_error(
|
|
615
|
+
Exception(f"HACS add repository request failed: {response}"),
|
|
616
|
+
context={
|
|
617
|
+
"command": "hacs/repositories/add",
|
|
618
|
+
"repository": repository,
|
|
619
|
+
"category": category,
|
|
620
|
+
},
|
|
621
|
+
)
|
|
622
|
+
return await add_timezone_metadata(client, error_response)
|
|
623
|
+
|
|
624
|
+
result = response.get("result", {})
|
|
625
|
+
|
|
626
|
+
return await add_timezone_metadata(client, {
|
|
627
|
+
"success": True,
|
|
628
|
+
"repository": repository,
|
|
629
|
+
"category": category,
|
|
630
|
+
"repository_id": result.get("id"),
|
|
631
|
+
"message": f"Successfully added {repository} to HACS",
|
|
632
|
+
"data": result,
|
|
633
|
+
})
|
|
634
|
+
|
|
635
|
+
except Exception as e:
|
|
636
|
+
error_response = exception_to_structured_error(
|
|
637
|
+
e,
|
|
638
|
+
context={
|
|
639
|
+
"tool": "ha_hacs_add_repository",
|
|
640
|
+
"repository": repository,
|
|
641
|
+
"category": category,
|
|
642
|
+
},
|
|
643
|
+
)
|
|
644
|
+
if "error" in error_response and isinstance(error_response["error"], dict):
|
|
645
|
+
error_response["error"]["suggestions"] = [
|
|
646
|
+
"Verify HACS is installed: https://hacs.xyz/",
|
|
647
|
+
"Check repository format: 'owner/repo'",
|
|
648
|
+
"Verify the repository exists on GitHub",
|
|
649
|
+
"Ensure category matches repository type",
|
|
650
|
+
"Check repository follows HACS guidelines: https://hacs.xyz/docs/publish/start",
|
|
651
|
+
]
|
|
652
|
+
return await add_timezone_metadata(client, error_response)
|
|
653
|
+
|
|
654
|
+
@mcp.tool(annotations={"destructiveHint": True, "tags": ["hacs", "management"], "title": "Download/Install HACS Repository"})
|
|
655
|
+
@log_tool_usage
|
|
656
|
+
async def ha_hacs_download(
|
|
657
|
+
repository_id: str,
|
|
658
|
+
version: Annotated[
|
|
659
|
+
str | None,
|
|
660
|
+
Field(
|
|
661
|
+
default=None,
|
|
662
|
+
description="Specific version to install (e.g., 'v1.2.3'). If not specified, installs the latest version.",
|
|
663
|
+
),
|
|
664
|
+
] = None,
|
|
665
|
+
) -> dict[str, Any]:
|
|
666
|
+
"""Download and install a HACS repository.
|
|
667
|
+
|
|
668
|
+
This installs a repository from HACS to your Home Assistant instance.
|
|
669
|
+
For integrations, a restart of Home Assistant may be required after installation.
|
|
670
|
+
|
|
671
|
+
**Prerequisites:**
|
|
672
|
+
- The repository must already be in HACS (either from the default store or added via `ha_hacs_add_repository`)
|
|
673
|
+
- Use `ha_hacs_search()` or `ha_hacs_list_installed()` to find the repository ID
|
|
674
|
+
|
|
675
|
+
**Examples:**
|
|
676
|
+
```python
|
|
677
|
+
# Install latest version of a repository
|
|
678
|
+
ha_hacs_download("441028036")
|
|
679
|
+
|
|
680
|
+
# Install specific version
|
|
681
|
+
ha_hacs_download("441028036", version="v2.0.0")
|
|
682
|
+
|
|
683
|
+
# Install by GitHub path (will look up the numeric ID)
|
|
684
|
+
ha_hacs_download("piitaya/lovelace-mushroom", version="v4.0.0")
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
**Note:** For integrations, you may need to restart Home Assistant after installation.
|
|
688
|
+
For Lovelace cards, clear your browser cache to see the new card.
|
|
689
|
+
|
|
690
|
+
Args:
|
|
691
|
+
repository_id: Repository numeric ID or GitHub path (e.g., "441028036" or "owner/repo")
|
|
692
|
+
version: Specific version to install (optional, defaults to latest)
|
|
693
|
+
|
|
694
|
+
Returns:
|
|
695
|
+
Success status and installation details.
|
|
696
|
+
"""
|
|
697
|
+
try:
|
|
698
|
+
# Check if HACS is available
|
|
699
|
+
is_available, error_msg = await _check_hacs_available(client)
|
|
700
|
+
if not is_available:
|
|
701
|
+
return await add_timezone_metadata(client, {
|
|
702
|
+
"success": False,
|
|
703
|
+
"error": error_msg,
|
|
704
|
+
"error_code": "HACS_NOT_AVAILABLE",
|
|
705
|
+
"suggestions": [
|
|
706
|
+
"Install HACS from https://hacs.xyz/",
|
|
707
|
+
"Ensure Home Assistant has been restarted after HACS installation",
|
|
708
|
+
"Check Home Assistant logs for HACS errors",
|
|
709
|
+
],
|
|
710
|
+
})
|
|
711
|
+
|
|
712
|
+
from ..client.websocket_client import get_websocket_client
|
|
713
|
+
ws_client = await get_websocket_client()
|
|
714
|
+
|
|
715
|
+
# If repository_id contains a slash, it's a GitHub path - need to look up numeric ID
|
|
716
|
+
actual_id = repository_id
|
|
717
|
+
repo_name = repository_id
|
|
718
|
+
if "/" in repository_id:
|
|
719
|
+
# Look up the numeric ID from the repository list
|
|
720
|
+
list_response = await ws_client.send_command("hacs/repositories/list")
|
|
721
|
+
if list_response.get("success"):
|
|
722
|
+
repos = list_response.get("result", [])
|
|
723
|
+
for repo in repos:
|
|
724
|
+
if repo.get("full_name", "").lower() == repository_id.lower():
|
|
725
|
+
actual_id = str(repo.get("id"))
|
|
726
|
+
repo_name = repo.get("name") or repository_id
|
|
727
|
+
break
|
|
728
|
+
else:
|
|
729
|
+
return await add_timezone_metadata(client, {
|
|
730
|
+
"success": False,
|
|
731
|
+
"error": f"Repository '{repository_id}' not found in HACS",
|
|
732
|
+
"error_code": "REPOSITORY_NOT_FOUND",
|
|
733
|
+
"suggestions": [
|
|
734
|
+
"Use ha_hacs_add_repository() to add the repository first",
|
|
735
|
+
"Use ha_hacs_search() to find available repositories",
|
|
736
|
+
"Check the repository name is correct (case-insensitive)",
|
|
737
|
+
],
|
|
738
|
+
})
|
|
739
|
+
|
|
740
|
+
# Build download command parameters
|
|
741
|
+
download_kwargs: dict[str, Any] = {"repository": actual_id}
|
|
742
|
+
if version:
|
|
743
|
+
download_kwargs["version"] = version
|
|
744
|
+
|
|
745
|
+
# Download/install the repository
|
|
746
|
+
response = await ws_client.send_command("hacs/repository/download", **download_kwargs)
|
|
747
|
+
|
|
748
|
+
if not response.get("success"):
|
|
749
|
+
error_response = exception_to_structured_error(
|
|
750
|
+
Exception(f"HACS download request failed: {response}"),
|
|
751
|
+
context={
|
|
752
|
+
"command": "hacs/repository/download",
|
|
753
|
+
"repository_id": repository_id,
|
|
754
|
+
"version": version,
|
|
755
|
+
},
|
|
756
|
+
)
|
|
757
|
+
return await add_timezone_metadata(client, error_response)
|
|
758
|
+
|
|
759
|
+
result = response.get("result", {})
|
|
760
|
+
|
|
761
|
+
return await add_timezone_metadata(client, {
|
|
762
|
+
"success": True,
|
|
763
|
+
"repository_id": actual_id,
|
|
764
|
+
"repository": repo_name,
|
|
765
|
+
"version": version or "latest",
|
|
766
|
+
"message": f"Successfully installed {repo_name}" + (f" version {version}" if version else ""),
|
|
767
|
+
"note": "For integrations, restart Home Assistant to activate. For Lovelace cards, clear browser cache.",
|
|
768
|
+
"data": result,
|
|
769
|
+
})
|
|
770
|
+
|
|
771
|
+
except Exception as e:
|
|
772
|
+
error_response = exception_to_structured_error(
|
|
773
|
+
e,
|
|
774
|
+
context={
|
|
775
|
+
"tool": "ha_hacs_download",
|
|
776
|
+
"repository_id": repository_id,
|
|
777
|
+
"version": version,
|
|
778
|
+
},
|
|
779
|
+
)
|
|
780
|
+
if "error" in error_response and isinstance(error_response["error"], dict):
|
|
781
|
+
error_response["error"]["suggestions"] = [
|
|
782
|
+
"Verify HACS is installed: https://hacs.xyz/",
|
|
783
|
+
"Check repository ID is valid (use ha_hacs_search() to find it)",
|
|
784
|
+
"Ensure the repository is in HACS (use ha_hacs_add_repository() if needed)",
|
|
785
|
+
"Check version format (e.g., 'v1.2.3' or '1.2.3')",
|
|
786
|
+
]
|
|
787
|
+
return await add_timezone_metadata(client, error_response)
|