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
ha_mcp/errors.py
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Structured error handling for Home Assistant MCP Server.
|
|
3
|
+
|
|
4
|
+
This module provides standardized error codes, error response models, and helper
|
|
5
|
+
functions for creating consistent, informative error responses across all MCP tools.
|
|
6
|
+
|
|
7
|
+
The structured error format enables AI agents to:
|
|
8
|
+
- Diagnose issues programmatically using error codes
|
|
9
|
+
- Provide helpful suggestions to users
|
|
10
|
+
- Understand the context and details of failures
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from enum import Enum
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ErrorCode(str, Enum):
|
|
18
|
+
"""
|
|
19
|
+
Standard error codes for Home Assistant MCP operations.
|
|
20
|
+
|
|
21
|
+
Error codes are grouped by category:
|
|
22
|
+
- CONNECTION_*: Network and connectivity issues
|
|
23
|
+
- AUTH_*: Authentication and authorization issues
|
|
24
|
+
- ENTITY_*: Entity-related issues
|
|
25
|
+
- SERVICE_*: Service call issues
|
|
26
|
+
- CONFIG_*: Configuration issues
|
|
27
|
+
- VALIDATION_*: Input validation issues
|
|
28
|
+
- TIMEOUT_*: Timeout issues
|
|
29
|
+
- INTERNAL_*: Internal server errors
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
# Connection errors
|
|
33
|
+
CONNECTION_FAILED = "CONNECTION_FAILED"
|
|
34
|
+
CONNECTION_TIMEOUT = "CONNECTION_TIMEOUT"
|
|
35
|
+
WEBSOCKET_DISCONNECTED = "WEBSOCKET_DISCONNECTED"
|
|
36
|
+
WEBSOCKET_NOT_AUTHENTICATED = "WEBSOCKET_NOT_AUTHENTICATED"
|
|
37
|
+
|
|
38
|
+
# Authentication errors
|
|
39
|
+
AUTH_INVALID_TOKEN = "AUTH_INVALID_TOKEN"
|
|
40
|
+
AUTH_EXPIRED = "AUTH_EXPIRED"
|
|
41
|
+
AUTH_INSUFFICIENT_PERMISSIONS = "AUTH_INSUFFICIENT_PERMISSIONS"
|
|
42
|
+
|
|
43
|
+
# Entity errors
|
|
44
|
+
ENTITY_NOT_FOUND = "ENTITY_NOT_FOUND"
|
|
45
|
+
ENTITY_UNAVAILABLE = "ENTITY_UNAVAILABLE"
|
|
46
|
+
ENTITY_INVALID_ID = "ENTITY_INVALID_ID"
|
|
47
|
+
ENTITY_DOMAIN_MISMATCH = "ENTITY_DOMAIN_MISMATCH"
|
|
48
|
+
|
|
49
|
+
# Service errors
|
|
50
|
+
SERVICE_NOT_FOUND = "SERVICE_NOT_FOUND"
|
|
51
|
+
SERVICE_INVALID_DOMAIN = "SERVICE_INVALID_DOMAIN"
|
|
52
|
+
SERVICE_INVALID_ACTION = "SERVICE_INVALID_ACTION"
|
|
53
|
+
SERVICE_CALL_FAILED = "SERVICE_CALL_FAILED"
|
|
54
|
+
|
|
55
|
+
# Configuration errors
|
|
56
|
+
CONFIG_NOT_FOUND = "CONFIG_NOT_FOUND"
|
|
57
|
+
CONFIG_INVALID = "CONFIG_INVALID"
|
|
58
|
+
CONFIG_MISSING_REQUIRED_FIELDS = "CONFIG_MISSING_REQUIRED_FIELDS"
|
|
59
|
+
CONFIG_VALIDATION_FAILED = "CONFIG_VALIDATION_FAILED"
|
|
60
|
+
|
|
61
|
+
# Validation errors
|
|
62
|
+
VALIDATION_FAILED = "VALIDATION_FAILED"
|
|
63
|
+
VALIDATION_INVALID_JSON = "VALIDATION_INVALID_JSON"
|
|
64
|
+
VALIDATION_INVALID_PARAMETER = "VALIDATION_INVALID_PARAMETER"
|
|
65
|
+
VALIDATION_MISSING_PARAMETER = "VALIDATION_MISSING_PARAMETER"
|
|
66
|
+
|
|
67
|
+
# Timeout errors
|
|
68
|
+
TIMEOUT_OPERATION = "TIMEOUT_OPERATION"
|
|
69
|
+
TIMEOUT_WEBSOCKET = "TIMEOUT_WEBSOCKET"
|
|
70
|
+
TIMEOUT_API_REQUEST = "TIMEOUT_API_REQUEST"
|
|
71
|
+
|
|
72
|
+
# Internal errors
|
|
73
|
+
INTERNAL_ERROR = "INTERNAL_ERROR"
|
|
74
|
+
INTERNAL_UNEXPECTED = "INTERNAL_UNEXPECTED"
|
|
75
|
+
|
|
76
|
+
# Resource errors
|
|
77
|
+
RESOURCE_NOT_FOUND = "RESOURCE_NOT_FOUND"
|
|
78
|
+
RESOURCE_ALREADY_EXISTS = "RESOURCE_ALREADY_EXISTS"
|
|
79
|
+
RESOURCE_LOCKED = "RESOURCE_LOCKED"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# Default suggestions for common error codes
|
|
83
|
+
DEFAULT_SUGGESTIONS: dict[ErrorCode, list[str]] = {
|
|
84
|
+
ErrorCode.CONNECTION_FAILED: [
|
|
85
|
+
"Check if Home Assistant is running and accessible",
|
|
86
|
+
"Verify the HOMEASSISTANT_URL environment variable is correct",
|
|
87
|
+
"Check network connectivity to Home Assistant",
|
|
88
|
+
],
|
|
89
|
+
ErrorCode.CONNECTION_TIMEOUT: [
|
|
90
|
+
"Home Assistant may be overloaded or slow to respond",
|
|
91
|
+
"Check network latency to Home Assistant",
|
|
92
|
+
"Try increasing the timeout value if available",
|
|
93
|
+
],
|
|
94
|
+
ErrorCode.WEBSOCKET_DISCONNECTED: [
|
|
95
|
+
"WebSocket connection was lost",
|
|
96
|
+
"Check Home Assistant logs for errors",
|
|
97
|
+
"The operation may need to be retried",
|
|
98
|
+
],
|
|
99
|
+
ErrorCode.WEBSOCKET_NOT_AUTHENTICATED: [
|
|
100
|
+
"WebSocket connection is not authenticated",
|
|
101
|
+
"Verify the access token is valid",
|
|
102
|
+
"Try reconnecting to Home Assistant",
|
|
103
|
+
],
|
|
104
|
+
ErrorCode.AUTH_INVALID_TOKEN: [
|
|
105
|
+
"Verify the HOMEASSISTANT_TOKEN is correct",
|
|
106
|
+
"Generate a new long-lived access token in Home Assistant",
|
|
107
|
+
"Check token has not been revoked",
|
|
108
|
+
],
|
|
109
|
+
ErrorCode.AUTH_EXPIRED: [
|
|
110
|
+
"The access token has expired",
|
|
111
|
+
"Generate a new long-lived access token in Home Assistant",
|
|
112
|
+
],
|
|
113
|
+
ErrorCode.AUTH_INSUFFICIENT_PERMISSIONS: [
|
|
114
|
+
"The access token does not have sufficient permissions",
|
|
115
|
+
"Check token permissions in Home Assistant",
|
|
116
|
+
"Create a new token with required permissions",
|
|
117
|
+
],
|
|
118
|
+
ErrorCode.ENTITY_NOT_FOUND: [
|
|
119
|
+
"Use ha_search_entities() to find correct entity ID",
|
|
120
|
+
"Verify the entity exists in Home Assistant",
|
|
121
|
+
"Check for typos in the entity ID",
|
|
122
|
+
],
|
|
123
|
+
ErrorCode.ENTITY_UNAVAILABLE: [
|
|
124
|
+
"The entity exists but is currently unavailable",
|
|
125
|
+
"Check if the device is powered on and connected",
|
|
126
|
+
"Check Home Assistant integration status",
|
|
127
|
+
],
|
|
128
|
+
ErrorCode.ENTITY_INVALID_ID: [
|
|
129
|
+
"Entity ID must be in format: domain.name",
|
|
130
|
+
"Use ha_search_entities() to find valid entity IDs",
|
|
131
|
+
],
|
|
132
|
+
ErrorCode.ENTITY_DOMAIN_MISMATCH: [
|
|
133
|
+
"Cannot change entity to a different domain",
|
|
134
|
+
"Entity domain must match the original domain",
|
|
135
|
+
],
|
|
136
|
+
ErrorCode.SERVICE_NOT_FOUND: [
|
|
137
|
+
"Use ha_get_domain_docs() to see available services",
|
|
138
|
+
"Check the service name spelling",
|
|
139
|
+
"Verify the domain supports this service",
|
|
140
|
+
],
|
|
141
|
+
ErrorCode.SERVICE_INVALID_DOMAIN: [
|
|
142
|
+
"Use ha_get_overview() to see available domains",
|
|
143
|
+
"Check the domain name spelling",
|
|
144
|
+
],
|
|
145
|
+
ErrorCode.SERVICE_INVALID_ACTION: [
|
|
146
|
+
"Check available actions for this domain",
|
|
147
|
+
"Common actions: turn_on, turn_off, toggle",
|
|
148
|
+
"Use ha_get_domain_docs() for service documentation",
|
|
149
|
+
],
|
|
150
|
+
ErrorCode.SERVICE_CALL_FAILED: [
|
|
151
|
+
"Check the service parameters are correct",
|
|
152
|
+
"Verify the target entity supports this service",
|
|
153
|
+
"Check Home Assistant logs for detailed error",
|
|
154
|
+
],
|
|
155
|
+
ErrorCode.CONFIG_NOT_FOUND: [
|
|
156
|
+
"Verify the resource exists",
|
|
157
|
+
"Use search tools to find the correct identifier",
|
|
158
|
+
],
|
|
159
|
+
ErrorCode.CONFIG_INVALID: [
|
|
160
|
+
"Review the configuration format",
|
|
161
|
+
"Use ha_get_domain_docs() for configuration help",
|
|
162
|
+
],
|
|
163
|
+
ErrorCode.CONFIG_MISSING_REQUIRED_FIELDS: [
|
|
164
|
+
"Check documentation for required fields",
|
|
165
|
+
"Ensure all required parameters are provided",
|
|
166
|
+
],
|
|
167
|
+
ErrorCode.VALIDATION_INVALID_JSON: [
|
|
168
|
+
"Ensure the parameter is valid JSON",
|
|
169
|
+
"Check for syntax errors in JSON",
|
|
170
|
+
"Use a JSON validator to verify the format",
|
|
171
|
+
],
|
|
172
|
+
ErrorCode.VALIDATION_INVALID_PARAMETER: [
|
|
173
|
+
"Check the parameter type and format",
|
|
174
|
+
"Review the tool documentation for expected values",
|
|
175
|
+
],
|
|
176
|
+
ErrorCode.TIMEOUT_OPERATION: [
|
|
177
|
+
"The operation took too long to complete",
|
|
178
|
+
"Home Assistant may be under heavy load",
|
|
179
|
+
"Try the operation again",
|
|
180
|
+
],
|
|
181
|
+
ErrorCode.INTERNAL_ERROR: [
|
|
182
|
+
"An internal error occurred",
|
|
183
|
+
"Check Home Assistant MCP server logs",
|
|
184
|
+
"Report this issue if it persists",
|
|
185
|
+
],
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def create_error_response(
|
|
190
|
+
code: ErrorCode,
|
|
191
|
+
message: str,
|
|
192
|
+
details: str | None = None,
|
|
193
|
+
suggestions: list[str] | None = None,
|
|
194
|
+
context: dict[str, Any] | None = None,
|
|
195
|
+
) -> dict[str, Any]:
|
|
196
|
+
"""
|
|
197
|
+
Create a structured error response.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
code: Error code from ErrorCode enum
|
|
201
|
+
message: Human-readable error message
|
|
202
|
+
details: Additional details about the error (optional)
|
|
203
|
+
suggestions: List of suggestions to resolve the error (optional)
|
|
204
|
+
context: Additional context data (e.g., entity_id, domain) (optional)
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Structured error response dictionary with success=False
|
|
208
|
+
|
|
209
|
+
Example:
|
|
210
|
+
>>> create_error_response(
|
|
211
|
+
... ErrorCode.ENTITY_NOT_FOUND,
|
|
212
|
+
... "Entity light.nonexistent not found",
|
|
213
|
+
... details="No entity with this ID exists in Home Assistant",
|
|
214
|
+
... context={"entity_id": "light.nonexistent"}
|
|
215
|
+
... )
|
|
216
|
+
{
|
|
217
|
+
"success": False,
|
|
218
|
+
"error": {
|
|
219
|
+
"code": "ENTITY_NOT_FOUND",
|
|
220
|
+
"message": "Entity light.nonexistent not found",
|
|
221
|
+
"details": "No entity with this ID exists in Home Assistant",
|
|
222
|
+
"suggestion": "Use ha_search_entities() to find correct entity ID"
|
|
223
|
+
},
|
|
224
|
+
"entity_id": "light.nonexistent"
|
|
225
|
+
}
|
|
226
|
+
"""
|
|
227
|
+
# Use provided suggestions or fall back to defaults
|
|
228
|
+
error_suggestions = suggestions if suggestions else DEFAULT_SUGGESTIONS.get(code, [])
|
|
229
|
+
|
|
230
|
+
error_dict: dict[str, Any] = {
|
|
231
|
+
"code": code.value,
|
|
232
|
+
"message": message,
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if details:
|
|
236
|
+
error_dict["details"] = details
|
|
237
|
+
|
|
238
|
+
if error_suggestions:
|
|
239
|
+
# Include first suggestion as primary, all suggestions in list
|
|
240
|
+
error_dict["suggestion"] = error_suggestions[0]
|
|
241
|
+
if len(error_suggestions) > 1:
|
|
242
|
+
error_dict["suggestions"] = error_suggestions
|
|
243
|
+
|
|
244
|
+
response: dict[str, Any] = {
|
|
245
|
+
"success": False,
|
|
246
|
+
"error": error_dict,
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
# Add context fields at top level for easy access
|
|
250
|
+
if context:
|
|
251
|
+
response.update(context)
|
|
252
|
+
|
|
253
|
+
return response
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def create_connection_error(
|
|
257
|
+
message: str,
|
|
258
|
+
details: str | None = None,
|
|
259
|
+
timeout: bool = False,
|
|
260
|
+
) -> dict[str, Any]:
|
|
261
|
+
"""Create a connection error response."""
|
|
262
|
+
code = ErrorCode.CONNECTION_TIMEOUT if timeout else ErrorCode.CONNECTION_FAILED
|
|
263
|
+
return create_error_response(code, message, details)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def create_auth_error(
|
|
267
|
+
message: str,
|
|
268
|
+
details: str | None = None,
|
|
269
|
+
expired: bool = False,
|
|
270
|
+
) -> dict[str, Any]:
|
|
271
|
+
"""Create an authentication error response."""
|
|
272
|
+
code = ErrorCode.AUTH_EXPIRED if expired else ErrorCode.AUTH_INVALID_TOKEN
|
|
273
|
+
return create_error_response(code, message, details)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def create_entity_not_found_error(
|
|
277
|
+
entity_id: str,
|
|
278
|
+
details: str | None = None,
|
|
279
|
+
) -> dict[str, Any]:
|
|
280
|
+
"""Create an entity not found error response."""
|
|
281
|
+
return create_error_response(
|
|
282
|
+
ErrorCode.ENTITY_NOT_FOUND,
|
|
283
|
+
f"Entity '{entity_id}' not found",
|
|
284
|
+
details=details or f"No entity with ID '{entity_id}' exists in Home Assistant",
|
|
285
|
+
context={"entity_id": entity_id},
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def create_service_error(
|
|
290
|
+
domain: str,
|
|
291
|
+
service: str,
|
|
292
|
+
message: str,
|
|
293
|
+
details: str | None = None,
|
|
294
|
+
entity_id: str | None = None,
|
|
295
|
+
) -> dict[str, Any]:
|
|
296
|
+
"""Create a service call error response."""
|
|
297
|
+
context: dict[str, Any] = {"domain": domain, "service": service}
|
|
298
|
+
if entity_id:
|
|
299
|
+
context["entity_id"] = entity_id
|
|
300
|
+
|
|
301
|
+
return create_error_response(
|
|
302
|
+
ErrorCode.SERVICE_CALL_FAILED,
|
|
303
|
+
message,
|
|
304
|
+
details=details,
|
|
305
|
+
context=context,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def create_validation_error(
|
|
310
|
+
message: str,
|
|
311
|
+
parameter: str | None = None,
|
|
312
|
+
details: str | None = None,
|
|
313
|
+
invalid_json: bool = False,
|
|
314
|
+
context: dict[str, Any] | None = None,
|
|
315
|
+
) -> dict[str, Any]:
|
|
316
|
+
"""Create a validation error response."""
|
|
317
|
+
code = ErrorCode.VALIDATION_INVALID_JSON if invalid_json else ErrorCode.VALIDATION_FAILED
|
|
318
|
+
# Build context, prioritizing explicit context but adding parameter if provided
|
|
319
|
+
final_context: dict[str, Any] = {}
|
|
320
|
+
if context:
|
|
321
|
+
final_context.update(context)
|
|
322
|
+
if parameter:
|
|
323
|
+
final_context["parameter"] = parameter
|
|
324
|
+
return create_error_response(code, message, details, context=final_context if final_context else None)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def create_config_error(
|
|
328
|
+
message: str,
|
|
329
|
+
identifier: str | None = None,
|
|
330
|
+
missing_fields: list[str] | None = None,
|
|
331
|
+
details: str | None = None,
|
|
332
|
+
) -> dict[str, Any]:
|
|
333
|
+
"""Create a configuration error response."""
|
|
334
|
+
if missing_fields:
|
|
335
|
+
code = ErrorCode.CONFIG_MISSING_REQUIRED_FIELDS
|
|
336
|
+
details = details or f"Missing required fields: {', '.join(missing_fields)}"
|
|
337
|
+
else:
|
|
338
|
+
code = ErrorCode.CONFIG_INVALID
|
|
339
|
+
|
|
340
|
+
context: dict[str, Any] = {}
|
|
341
|
+
if identifier:
|
|
342
|
+
context["identifier"] = identifier
|
|
343
|
+
if missing_fields:
|
|
344
|
+
context["missing_fields"] = missing_fields
|
|
345
|
+
|
|
346
|
+
return create_error_response(code, message, details, context=context or None)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def create_timeout_error(
|
|
350
|
+
operation: str,
|
|
351
|
+
timeout_seconds: float,
|
|
352
|
+
details: str | None = None,
|
|
353
|
+
) -> dict[str, Any]:
|
|
354
|
+
"""Create a timeout error response."""
|
|
355
|
+
return create_error_response(
|
|
356
|
+
ErrorCode.TIMEOUT_OPERATION,
|
|
357
|
+
f"Operation '{operation}' timed out after {timeout_seconds}s",
|
|
358
|
+
details=details,
|
|
359
|
+
context={"operation": operation, "timeout_seconds": timeout_seconds},
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def create_resource_not_found_error(
|
|
364
|
+
resource_type: str,
|
|
365
|
+
identifier: str,
|
|
366
|
+
details: str | None = None,
|
|
367
|
+
) -> dict[str, Any]:
|
|
368
|
+
"""Create a resource not found error response."""
|
|
369
|
+
return create_error_response(
|
|
370
|
+
ErrorCode.RESOURCE_NOT_FOUND,
|
|
371
|
+
f"{resource_type} '{identifier}' not found",
|
|
372
|
+
details=details,
|
|
373
|
+
context={"resource_type": resource_type, "identifier": identifier},
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def is_error_response(response: dict[str, Any]) -> bool:
|
|
378
|
+
"""Check if a response is an error response."""
|
|
379
|
+
return response.get("success") is False and "error" in response
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def get_error_code(response: dict[str, Any]) -> str | None:
|
|
383
|
+
"""Extract the error code from an error response."""
|
|
384
|
+
if is_error_response(response):
|
|
385
|
+
error = response.get("error", {})
|
|
386
|
+
if isinstance(error, dict):
|
|
387
|
+
return error.get("code")
|
|
388
|
+
return None
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def get_error_message(response: dict[str, Any]) -> str | None:
|
|
392
|
+
"""Extract the error message from an error response."""
|
|
393
|
+
if is_error_response(response):
|
|
394
|
+
error = response.get("error", {})
|
|
395
|
+
if isinstance(error, dict):
|
|
396
|
+
return error.get("message")
|
|
397
|
+
if isinstance(error, str):
|
|
398
|
+
return error
|
|
399
|
+
return None
|
ha_mcp/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"card_types": [
|
|
3
|
+
"alarm-panel",
|
|
4
|
+
"area",
|
|
5
|
+
"button",
|
|
6
|
+
"calendar",
|
|
7
|
+
"clock",
|
|
8
|
+
"conditional",
|
|
9
|
+
"energy",
|
|
10
|
+
"entities",
|
|
11
|
+
"entity-filter",
|
|
12
|
+
"entity",
|
|
13
|
+
"gauge",
|
|
14
|
+
"glance",
|
|
15
|
+
"grid",
|
|
16
|
+
"heading",
|
|
17
|
+
"history-graph",
|
|
18
|
+
"horizontal-stack",
|
|
19
|
+
"humidifier",
|
|
20
|
+
"iframe",
|
|
21
|
+
"light",
|
|
22
|
+
"logbook",
|
|
23
|
+
"map",
|
|
24
|
+
"markdown",
|
|
25
|
+
"masonry",
|
|
26
|
+
"media-control",
|
|
27
|
+
"panel",
|
|
28
|
+
"picture-elements",
|
|
29
|
+
"picture-entity",
|
|
30
|
+
"picture-glance",
|
|
31
|
+
"picture",
|
|
32
|
+
"plant-status",
|
|
33
|
+
"sections",
|
|
34
|
+
"sensor",
|
|
35
|
+
"shopping-list",
|
|
36
|
+
"sidebar",
|
|
37
|
+
"statistic",
|
|
38
|
+
"statistics-graph",
|
|
39
|
+
"thermostat",
|
|
40
|
+
"tile",
|
|
41
|
+
"todo-list",
|
|
42
|
+
"vertical-stack",
|
|
43
|
+
"weather-forecast"
|
|
44
|
+
],
|
|
45
|
+
"total_count": 41,
|
|
46
|
+
"documentation_base_url": "https://raw.githubusercontent.com/home-assistant/home-assistant.io/refs/heads/current/source/_dashboards",
|
|
47
|
+
"usage": "Use ha-dashboard://card-docs/{card-type} to get documentation for specific card type"
|
|
48
|
+
}
|