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,519 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility tools for Home Assistant MCP server.
|
|
3
|
+
|
|
4
|
+
This module provides general-purpose utility tools including logbook access,
|
|
5
|
+
template evaluation, and domain documentation retrieval.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import logging
|
|
10
|
+
from datetime import UTC, datetime, timedelta
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
|
|
15
|
+
from .helpers import log_tool_usage
|
|
16
|
+
from .util_helpers import add_timezone_metadata, coerce_bool_param, coerce_int_param
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def register_utility_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
22
|
+
"""Register Home Assistant utility tools."""
|
|
23
|
+
|
|
24
|
+
# Default and maximum limits for logbook entries
|
|
25
|
+
DEFAULT_LOGBOOK_LIMIT = 50
|
|
26
|
+
MAX_LOGBOOK_LIMIT = 500
|
|
27
|
+
|
|
28
|
+
@mcp.tool(
|
|
29
|
+
annotations={
|
|
30
|
+
"idempotentHint": True,
|
|
31
|
+
"readOnlyHint": True,
|
|
32
|
+
"tags": ["history"],
|
|
33
|
+
"title": "Get Logbook Entries",
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
@log_tool_usage
|
|
37
|
+
async def ha_get_logbook(
|
|
38
|
+
hours_back: int | str = 1,
|
|
39
|
+
entity_id: str | None = None,
|
|
40
|
+
end_time: str | None = None,
|
|
41
|
+
limit: int | str | None = None,
|
|
42
|
+
offset: int | str = 0,
|
|
43
|
+
) -> dict[str, Any]:
|
|
44
|
+
"""
|
|
45
|
+
Get Home Assistant logbook entries for the specified time period.
|
|
46
|
+
|
|
47
|
+
Returns paginated logbook entries to prevent excessively large responses.
|
|
48
|
+
|
|
49
|
+
**Parameters:**
|
|
50
|
+
- hours_back: Number of hours to look back (default: 1)
|
|
51
|
+
- entity_id: Optional entity ID to filter entries
|
|
52
|
+
- end_time: Optional end time in ISO format (defaults to now)
|
|
53
|
+
- limit: Maximum number of entries to return (default: 50, max: 500)
|
|
54
|
+
- offset: Number of entries to skip for pagination (default: 0)
|
|
55
|
+
|
|
56
|
+
**Pagination:**
|
|
57
|
+
When the logbook has more entries than the limit, use offset to get
|
|
58
|
+
additional pages. The response includes `has_more` to indicate if
|
|
59
|
+
more entries are available.
|
|
60
|
+
|
|
61
|
+
**IMPORTANT - Pagination Stability:**
|
|
62
|
+
Pagination is performed client-side on the full result set returned
|
|
63
|
+
by Home Assistant. If new logbook entries are created between page
|
|
64
|
+
requests, results may shift and items could be missed or duplicated
|
|
65
|
+
across pages. For best results, use consistent time ranges (start/end)
|
|
66
|
+
and retrieve pages in quick succession.
|
|
67
|
+
|
|
68
|
+
**Example:**
|
|
69
|
+
- First page: ha_get_logbook(hours_back=24, limit=50, offset=0)
|
|
70
|
+
- Second page: ha_get_logbook(hours_back=24, limit=50, offset=50)
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
# Coerce parameters with string handling for AI tools
|
|
74
|
+
try:
|
|
75
|
+
hours_back_int = coerce_int_param(
|
|
76
|
+
hours_back,
|
|
77
|
+
param_name="hours_back",
|
|
78
|
+
default=1,
|
|
79
|
+
min_value=1,
|
|
80
|
+
)
|
|
81
|
+
if hours_back_int is None:
|
|
82
|
+
hours_back_int = 1
|
|
83
|
+
except ValueError as e:
|
|
84
|
+
return {
|
|
85
|
+
"success": False,
|
|
86
|
+
"error": str(e),
|
|
87
|
+
"suggestions": ["Provide hours_back as an integer (e.g., 24)"],
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
effective_limit = coerce_int_param(
|
|
92
|
+
limit,
|
|
93
|
+
param_name="limit",
|
|
94
|
+
default=DEFAULT_LOGBOOK_LIMIT,
|
|
95
|
+
min_value=1,
|
|
96
|
+
max_value=MAX_LOGBOOK_LIMIT,
|
|
97
|
+
)
|
|
98
|
+
if effective_limit is None:
|
|
99
|
+
effective_limit = DEFAULT_LOGBOOK_LIMIT
|
|
100
|
+
except ValueError as e:
|
|
101
|
+
return {
|
|
102
|
+
"success": False,
|
|
103
|
+
"error": str(e),
|
|
104
|
+
"suggestions": ["Provide limit as an integer (e.g., 50)"],
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
offset_int = coerce_int_param(
|
|
109
|
+
offset,
|
|
110
|
+
param_name="offset",
|
|
111
|
+
default=0,
|
|
112
|
+
min_value=0,
|
|
113
|
+
)
|
|
114
|
+
if offset_int is None:
|
|
115
|
+
offset_int = 0
|
|
116
|
+
except ValueError as e:
|
|
117
|
+
return {
|
|
118
|
+
"success": False,
|
|
119
|
+
"error": str(e),
|
|
120
|
+
"suggestions": ["Provide offset as an integer (e.g., 0)"],
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# Calculate start time
|
|
124
|
+
if end_time:
|
|
125
|
+
end_dt = datetime.fromisoformat(end_time.replace("Z", "+00:00"))
|
|
126
|
+
else:
|
|
127
|
+
end_dt = datetime.now(UTC)
|
|
128
|
+
|
|
129
|
+
start_dt = end_dt - timedelta(hours=hours_back_int)
|
|
130
|
+
start_timestamp = start_dt.isoformat()
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
response = await client.get_logbook(
|
|
134
|
+
entity_id=entity_id, start_time=start_timestamp, end_time=end_time
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
if not response:
|
|
138
|
+
no_entries_data = {
|
|
139
|
+
"success": False,
|
|
140
|
+
"error": "No logbook entries found",
|
|
141
|
+
"period": f"{hours_back_int} hours back from {end_dt.isoformat()}",
|
|
142
|
+
"entity_filter": entity_id,
|
|
143
|
+
"total_entries": 0,
|
|
144
|
+
"returned_entries": 0,
|
|
145
|
+
"limit": effective_limit,
|
|
146
|
+
"offset": offset_int,
|
|
147
|
+
"has_more": False,
|
|
148
|
+
}
|
|
149
|
+
return await add_timezone_metadata(client, no_entries_data)
|
|
150
|
+
|
|
151
|
+
# Get total count before pagination
|
|
152
|
+
total_entries = len(response) if isinstance(response, list) else 1
|
|
153
|
+
|
|
154
|
+
# Apply pagination
|
|
155
|
+
if isinstance(response, list):
|
|
156
|
+
paginated_entries = response[offset_int : offset_int + effective_limit]
|
|
157
|
+
has_more = (offset_int + effective_limit) < total_entries
|
|
158
|
+
else:
|
|
159
|
+
paginated_entries = response
|
|
160
|
+
has_more = False
|
|
161
|
+
|
|
162
|
+
logbook_data = {
|
|
163
|
+
"success": True,
|
|
164
|
+
"entries": paginated_entries,
|
|
165
|
+
"period": f"{hours_back_int} hours back from {end_dt.isoformat()}",
|
|
166
|
+
"start_time": start_timestamp,
|
|
167
|
+
"end_time": end_dt.isoformat(),
|
|
168
|
+
"entity_filter": entity_id,
|
|
169
|
+
"total_entries": total_entries,
|
|
170
|
+
"returned_entries": len(paginated_entries)
|
|
171
|
+
if isinstance(paginated_entries, list)
|
|
172
|
+
else 1,
|
|
173
|
+
"limit": effective_limit,
|
|
174
|
+
"offset": offset_int,
|
|
175
|
+
"has_more": has_more,
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
# Add helpful message when results are truncated
|
|
179
|
+
if has_more:
|
|
180
|
+
next_offset = offset_int + effective_limit
|
|
181
|
+
# Build complete parameter string for reproducible pagination
|
|
182
|
+
param_parts = [
|
|
183
|
+
f"hours_back={hours_back_int}",
|
|
184
|
+
f"limit={effective_limit}",
|
|
185
|
+
f"offset={next_offset}",
|
|
186
|
+
]
|
|
187
|
+
if entity_id:
|
|
188
|
+
param_parts.append(f"entity_id={entity_id}")
|
|
189
|
+
if end_time:
|
|
190
|
+
param_parts.append(f"end_time={end_time}")
|
|
191
|
+
|
|
192
|
+
param_str = ", ".join(param_parts)
|
|
193
|
+
logbook_data["pagination_hint"] = (
|
|
194
|
+
f"Showing entries {offset_int + 1}-{offset_int + len(paginated_entries)} of {total_entries}. "
|
|
195
|
+
f"To get the next page, use: ha_get_logbook({param_str})"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
return await add_timezone_metadata(client, logbook_data)
|
|
199
|
+
|
|
200
|
+
except Exception as e:
|
|
201
|
+
error_str = str(e)
|
|
202
|
+
suggestions = []
|
|
203
|
+
|
|
204
|
+
# Detect 500 errors (server crash from heavy query)
|
|
205
|
+
if "500" in error_str:
|
|
206
|
+
suggestions = [
|
|
207
|
+
"The query returned too many results causing a server error (500).",
|
|
208
|
+
"This often happens with very active entities or long time periods.",
|
|
209
|
+
"Try reducing 'hours_back' parameter (e.g., from 24 to 1 hour)",
|
|
210
|
+
"Add a specific 'entity_id' filter to narrow down results",
|
|
211
|
+
"If debugging an automation, filter by that automation's entity_id",
|
|
212
|
+
"Use ha_bug_report tool to check Home Assistant logs for crash details",
|
|
213
|
+
]
|
|
214
|
+
|
|
215
|
+
error_data = {
|
|
216
|
+
"success": False,
|
|
217
|
+
"error": f"Failed to retrieve logbook: {error_str}",
|
|
218
|
+
"period": f"{hours_back_int} hours back from {end_dt.isoformat()}",
|
|
219
|
+
"suggestions": suggestions if suggestions else None,
|
|
220
|
+
}
|
|
221
|
+
return await add_timezone_metadata(client, error_data)
|
|
222
|
+
|
|
223
|
+
@mcp.tool(
|
|
224
|
+
annotations={
|
|
225
|
+
"idempotentHint": True,
|
|
226
|
+
"readOnlyHint": True,
|
|
227
|
+
"tags": ["docs"],
|
|
228
|
+
"title": "Evaluate Template",
|
|
229
|
+
}
|
|
230
|
+
)
|
|
231
|
+
@log_tool_usage
|
|
232
|
+
async def ha_eval_template(
|
|
233
|
+
template: str, timeout: int = 3, report_errors: bool | str = True
|
|
234
|
+
) -> dict[str, Any]:
|
|
235
|
+
"""
|
|
236
|
+
Evaluate Jinja2 templates using Home Assistant's template engine.
|
|
237
|
+
|
|
238
|
+
This tool allows testing and debugging of Jinja2 template expressions that are commonly used in
|
|
239
|
+
Home Assistant automations, scripts, and configurations. It provides real-time evaluation with
|
|
240
|
+
access to all Home Assistant states, functions, and template variables.
|
|
241
|
+
|
|
242
|
+
**Parameters:**
|
|
243
|
+
- template: The Jinja2 template string to evaluate
|
|
244
|
+
- timeout: Maximum evaluation time in seconds (default: 3)
|
|
245
|
+
- report_errors: Whether to return detailed error information (default: True)
|
|
246
|
+
|
|
247
|
+
**Common Template Functions:**
|
|
248
|
+
|
|
249
|
+
**State Access:**
|
|
250
|
+
```jinja2
|
|
251
|
+
{{ states('sensor.temperature') }} # Get entity state value
|
|
252
|
+
{{ states.sensor.temperature.state }} # Alternative syntax
|
|
253
|
+
{{ state_attr('light.bedroom', 'brightness') }} # Get entity attribute
|
|
254
|
+
{{ is_state('light.living_room', 'on') }} # Check if entity has specific state
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
**Numeric Operations:**
|
|
258
|
+
```jinja2
|
|
259
|
+
{{ states('sensor.temperature') | float(0) }} # Convert to float with default
|
|
260
|
+
{{ states('sensor.humidity') | int }} # Convert to integer
|
|
261
|
+
{{ (states('sensor.temp') | float + 5) | round(1) }} # Math operations
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
**Time and Date:**
|
|
265
|
+
```jinja2
|
|
266
|
+
{{ now() }} # Current datetime
|
|
267
|
+
{{ now().strftime('%H:%M:%S') }} # Format current time
|
|
268
|
+
{{ as_timestamp(now()) }} # Convert to Unix timestamp
|
|
269
|
+
{{ now().hour }} # Current hour (0-23)
|
|
270
|
+
{{ now().weekday() }} # Day of week (0=Monday)
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
**Conditional Logic:**
|
|
274
|
+
```jinja2
|
|
275
|
+
{{ 'Day' if now().hour < 18 else 'Night' }} # Ternary operator
|
|
276
|
+
{% if is_state('sun.sun', 'above_horizon') %}
|
|
277
|
+
It's daytime
|
|
278
|
+
{% else %}
|
|
279
|
+
It's nighttime
|
|
280
|
+
{% endif %}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
**Lists and Loops:**
|
|
284
|
+
```jinja2
|
|
285
|
+
{% for entity in states.light %}
|
|
286
|
+
{{ entity.entity_id }}: {{ entity.state }}
|
|
287
|
+
{% endfor %}
|
|
288
|
+
|
|
289
|
+
{{ states.light | selectattr('state', 'eq', 'on') | list | count }} # Count on lights
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
**String Operations:**
|
|
293
|
+
```jinja2
|
|
294
|
+
{{ states('sensor.weather') | title }} # Title case
|
|
295
|
+
{{ 'Hello ' + states('input_text.name') }} # String concatenation
|
|
296
|
+
{{ states('sensor.data') | regex_replace('pattern', 'replacement') }}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
**Device and Area Functions:**
|
|
300
|
+
```jinja2
|
|
301
|
+
{{ device_entities('device_id_here') }} # Get entities for device
|
|
302
|
+
{{ area_entities('living_room') }} # Get entities in area
|
|
303
|
+
{{ device_id('light.bedroom') }} # Get device ID for entity
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
**Common Use Cases:**
|
|
307
|
+
|
|
308
|
+
**Automation Conditions:**
|
|
309
|
+
```jinja2
|
|
310
|
+
# Check if it's a workday and after 7 AM
|
|
311
|
+
{{ is_state('binary_sensor.workday', 'on') and now().hour >= 7 }}
|
|
312
|
+
|
|
313
|
+
# Temperature-based condition
|
|
314
|
+
{{ states('sensor.outdoor_temp') | float < 0 }}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
**Dynamic Service Data:**
|
|
318
|
+
```jinja2
|
|
319
|
+
# Dynamic brightness based on time
|
|
320
|
+
{{ 255 if now().hour < 22 else 50 }}
|
|
321
|
+
|
|
322
|
+
# Message with current values
|
|
323
|
+
"Temperature is {{ states('sensor.temp') }}°C, humidity {{ states('sensor.humidity') }}%"
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
**Examples:**
|
|
327
|
+
|
|
328
|
+
**Test basic state access:**
|
|
329
|
+
```python
|
|
330
|
+
ha_eval_template("{{ states('light.living_room') }}")
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
**Test conditional logic:**
|
|
334
|
+
```python
|
|
335
|
+
ha_eval_template("{{ 'Day' if now().hour < 18 else 'Night' }}")
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
**Test mathematical operations:**
|
|
339
|
+
```python
|
|
340
|
+
ha_eval_template("{{ (states('sensor.temperature') | float + 5) | round(1) }}")
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
**Test complex automation condition:**
|
|
344
|
+
```python
|
|
345
|
+
ha_eval_template("{{ is_state('binary_sensor.workday', 'on') and now().hour >= 7 and states('sensor.temperature') | float > 20 }}")
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
**Test entity counting:**
|
|
349
|
+
```python
|
|
350
|
+
ha_eval_template("{{ states.light | selectattr('state', 'eq', 'on') | list | count }}")
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
**IMPORTANT NOTES:**
|
|
354
|
+
- Templates have access to all current Home Assistant states and attributes
|
|
355
|
+
- Use this tool to test templates before using them in automations or scripts
|
|
356
|
+
- Template evaluation respects Home Assistant's security model and timeouts
|
|
357
|
+
- Complex templates may affect Home Assistant performance - keep them efficient
|
|
358
|
+
- Use default values (e.g., `| float(0)`) to handle missing or invalid states
|
|
359
|
+
|
|
360
|
+
**For template documentation:** https://www.home-assistant.io/docs/configuration/templating/
|
|
361
|
+
"""
|
|
362
|
+
# Coerce boolean parameter that may come as string from XML-style calls
|
|
363
|
+
report_errors_bool = coerce_bool_param(
|
|
364
|
+
report_errors, "report_errors", default=True
|
|
365
|
+
)
|
|
366
|
+
assert report_errors_bool is not None # default=True guarantees non-None
|
|
367
|
+
|
|
368
|
+
try:
|
|
369
|
+
# Generate unique ID for the template evaluation request
|
|
370
|
+
import time
|
|
371
|
+
|
|
372
|
+
request_id = int(time.time() * 1000) % 1000000 # Simple unique ID
|
|
373
|
+
|
|
374
|
+
# Construct WebSocket message following the protocol
|
|
375
|
+
message: dict[str, Any] = {
|
|
376
|
+
"type": "render_template",
|
|
377
|
+
"template": template,
|
|
378
|
+
"timeout": timeout,
|
|
379
|
+
"report_errors": report_errors_bool,
|
|
380
|
+
"id": request_id,
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
# Send WebSocket message and get response
|
|
384
|
+
result = await client.send_websocket_message(message)
|
|
385
|
+
|
|
386
|
+
if result.get("success"):
|
|
387
|
+
# Check if we have an event-type response with the actual result
|
|
388
|
+
if "event" in result and "result" in result["event"]:
|
|
389
|
+
template_result = result["event"]["result"]
|
|
390
|
+
listeners = result["event"].get("listeners", {})
|
|
391
|
+
|
|
392
|
+
return {
|
|
393
|
+
"success": True,
|
|
394
|
+
"template": template,
|
|
395
|
+
"result": template_result,
|
|
396
|
+
"listeners": listeners,
|
|
397
|
+
"request_id": request_id,
|
|
398
|
+
"evaluation_time": timeout,
|
|
399
|
+
}
|
|
400
|
+
else:
|
|
401
|
+
# Handle direct result response
|
|
402
|
+
return {
|
|
403
|
+
"success": True,
|
|
404
|
+
"template": template,
|
|
405
|
+
"result": result.get("result"),
|
|
406
|
+
"request_id": request_id,
|
|
407
|
+
"evaluation_time": timeout,
|
|
408
|
+
}
|
|
409
|
+
else:
|
|
410
|
+
error_info = result.get("error", "Unknown error occurred")
|
|
411
|
+
return {
|
|
412
|
+
"success": False,
|
|
413
|
+
"template": template,
|
|
414
|
+
"error": error_info,
|
|
415
|
+
"request_id": request_id,
|
|
416
|
+
"suggestions": [
|
|
417
|
+
"Check template syntax - ensure proper Jinja2 formatting",
|
|
418
|
+
"Verify entity_ids exist using ha_get_state()",
|
|
419
|
+
"Use default values: {{ states('sensor.temp') | float(0) }}",
|
|
420
|
+
"Check for typos in function names and entity references",
|
|
421
|
+
"Test simpler templates first to isolate issues",
|
|
422
|
+
],
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
except Exception as e:
|
|
426
|
+
error_str = str(e)
|
|
427
|
+
suggestions = [
|
|
428
|
+
"Check Home Assistant WebSocket connection",
|
|
429
|
+
"Verify template syntax is valid Jinja2",
|
|
430
|
+
"Try a simpler template to test basic functionality",
|
|
431
|
+
"Check if referenced entities exist",
|
|
432
|
+
"Ensure template doesn't exceed timeout limit",
|
|
433
|
+
]
|
|
434
|
+
|
|
435
|
+
# Add specific suggestions for 403 errors
|
|
436
|
+
if "403" in error_str and "Forbidden" in error_str:
|
|
437
|
+
suggestions = [
|
|
438
|
+
"The request was blocked (403 Forbidden) - this may be caused by:",
|
|
439
|
+
" • Reverse proxy security rules (Apache, Nginx, Traefik)",
|
|
440
|
+
" • Rate limiting from multiple simultaneous requests",
|
|
441
|
+
" • Complex template triggering security filters",
|
|
442
|
+
"Try simplifying the template (remove newlines, reduce complexity)",
|
|
443
|
+
"Break complex templates into multiple simpler calls",
|
|
444
|
+
"Use ha_bug_report tool to check Home Assistant logs for details",
|
|
445
|
+
] + suggestions
|
|
446
|
+
|
|
447
|
+
return {
|
|
448
|
+
"success": False,
|
|
449
|
+
"template": template,
|
|
450
|
+
"error": f"Template evaluation failed: {error_str}",
|
|
451
|
+
"suggestions": suggestions,
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
@mcp.tool(annotations={"readOnlyHint": True, "title": "Get Domain Docs"})
|
|
455
|
+
async def ha_get_domain_docs(domain: str) -> dict[str, Any]:
|
|
456
|
+
"""Get comprehensive documentation for Home Assistant entity domains."""
|
|
457
|
+
domain = domain.lower().strip()
|
|
458
|
+
|
|
459
|
+
# GitHub URL for Home Assistant integration documentation
|
|
460
|
+
github_url = f"https://raw.githubusercontent.com/home-assistant/home-assistant.io/refs/heads/current/source/_integrations/{domain}.markdown"
|
|
461
|
+
|
|
462
|
+
try:
|
|
463
|
+
# Fetch documentation from GitHub
|
|
464
|
+
async with httpx.AsyncClient(timeout=30.0) as client_http:
|
|
465
|
+
response = await client_http.get(github_url)
|
|
466
|
+
|
|
467
|
+
if response.status_code == 200:
|
|
468
|
+
# Successfully fetched documentation
|
|
469
|
+
doc_content = response.text
|
|
470
|
+
|
|
471
|
+
# Extract title from the first line if available
|
|
472
|
+
lines = doc_content.split("\n")
|
|
473
|
+
title = lines[0] if lines else f"{domain.title()} Integration"
|
|
474
|
+
|
|
475
|
+
return {
|
|
476
|
+
"domain": domain,
|
|
477
|
+
"source": "Home Assistant Official Documentation",
|
|
478
|
+
"url": github_url,
|
|
479
|
+
"documentation": doc_content,
|
|
480
|
+
"title": title.strip("# "),
|
|
481
|
+
"fetched_at": asyncio.get_event_loop().time(),
|
|
482
|
+
"status": "success",
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
elif response.status_code == 404:
|
|
486
|
+
# Domain documentation not found
|
|
487
|
+
return {
|
|
488
|
+
"error": f"No official documentation found for domain '{domain}'",
|
|
489
|
+
"domain": domain,
|
|
490
|
+
"status": "not_found",
|
|
491
|
+
"suggestion": "Check if the domain name is correct. Common domains include: light, climate, switch, lock, sensor, automation, media_player, cover, fan, binary_sensor, camera, alarm_control_panel, etc.",
|
|
492
|
+
"github_url": github_url,
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
else:
|
|
496
|
+
# Other HTTP errors
|
|
497
|
+
return {
|
|
498
|
+
"error": f"Failed to fetch documentation for '{domain}' (HTTP {response.status_code})",
|
|
499
|
+
"domain": domain,
|
|
500
|
+
"status": "fetch_error",
|
|
501
|
+
"github_url": github_url,
|
|
502
|
+
"suggestion": "Try again later or check the domain name",
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
except httpx.TimeoutException:
|
|
506
|
+
return {
|
|
507
|
+
"error": f"Timeout while fetching documentation for '{domain}'",
|
|
508
|
+
"domain": domain,
|
|
509
|
+
"status": "timeout",
|
|
510
|
+
"suggestion": "Try again later - GitHub may be temporarily unavailable",
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
except Exception as e:
|
|
514
|
+
return {
|
|
515
|
+
"error": f"Unexpected error fetching documentation for '{domain}': {str(e)}",
|
|
516
|
+
"domain": domain,
|
|
517
|
+
"status": "error",
|
|
518
|
+
"suggestion": "Check your internet connection and try again",
|
|
519
|
+
}
|