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,391 @@
1
+ """
2
+ Calendar event management tools for Home Assistant MCP server.
3
+
4
+ This module provides tools for managing calendar events in Home Assistant,
5
+ including retrieving events, creating events, and deleting events.
6
+
7
+ Use ha_search_entities(query='calendar', domain_filter='calendar') to find calendar entities.
8
+ """
9
+
10
+ import logging
11
+ from datetime import datetime, timedelta
12
+ from typing import Annotated, Any
13
+
14
+ from pydantic import Field
15
+
16
+ from .helpers import log_tool_usage
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ def register_calendar_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
22
+ """Register calendar management tools with the MCP server."""
23
+
24
+ @mcp.tool(annotations={"idempotentHint": True, "readOnlyHint": True, "tags": ["calendar"], "title": "Get Calendar Events"})
25
+ @log_tool_usage
26
+ async def ha_config_get_calendar_events(
27
+ entity_id: Annotated[
28
+ str, Field(description="Calendar entity ID (e.g., 'calendar.family')")
29
+ ],
30
+ start: Annotated[
31
+ str | None,
32
+ Field(
33
+ description="Start datetime in ISO format (default: now)", default=None
34
+ ),
35
+ ] = None,
36
+ end: Annotated[
37
+ str | None,
38
+ Field(
39
+ description="End datetime in ISO format (default: 7 days from start)",
40
+ default=None,
41
+ ),
42
+ ] = None,
43
+ max_results: Annotated[
44
+ int,
45
+ Field(description="Maximum number of events to return", default=20),
46
+ ] = 20,
47
+ ) -> dict[str, Any]:
48
+ """
49
+ Retrieve calendar events from a calendar entity.
50
+
51
+ Retrieves calendar events within a specified time range.
52
+
53
+ **Parameters:**
54
+ - entity_id: Calendar entity ID (e.g., 'calendar.family')
55
+ - start: Start datetime in ISO format (default: now)
56
+ - end: End datetime in ISO format (default: 7 days from start)
57
+ - max_results: Maximum number of events to return (default: 20)
58
+
59
+ **Example Usage:**
60
+ ```python
61
+ # Get events for the next week
62
+ events = ha_config_get_calendar_events("calendar.family")
63
+
64
+ # Get events for a specific date range
65
+ events = ha_config_get_calendar_events(
66
+ "calendar.work",
67
+ start="2024-01-01T00:00:00",
68
+ end="2024-01-31T23:59:59"
69
+ )
70
+ ```
71
+
72
+ **Note:** To find calendar entities, use ha_search_entities(query='calendar', domain_filter='calendar')
73
+
74
+ **Returns:**
75
+ - List of calendar events with summary, start, end, description, location
76
+ """
77
+ try:
78
+ # Validate entity_id
79
+ if not entity_id.startswith("calendar."):
80
+ return {
81
+ "success": False,
82
+ "error": f"Invalid calendar entity ID: {entity_id}. Must start with 'calendar.'",
83
+ "entity_id": entity_id,
84
+ "suggestions": [
85
+ "Use ha_search_entities(query='calendar', domain_filter='calendar') to find calendar entities",
86
+ "Calendar entity IDs start with 'calendar.' prefix",
87
+ ],
88
+ }
89
+
90
+ # Set default time range if not provided
91
+ now = datetime.now()
92
+ if start is None:
93
+ start = now.isoformat()
94
+ if end is None:
95
+ end_date = now + timedelta(days=7)
96
+ end = end_date.isoformat()
97
+
98
+ # Build the API endpoint for calendar events
99
+ # Home Assistant uses: GET /api/calendars/{entity_id}?start=...&end=...
100
+ params = {"start": start, "end": end}
101
+
102
+ # Use the REST client to fetch calendar events
103
+ # The endpoint is /calendars/{entity_id} (note: without /api prefix as client adds it)
104
+ response = await client._request(
105
+ "GET", f"/calendars/{entity_id}", params=params
106
+ )
107
+
108
+ # Response is a list of events
109
+ events = response if isinstance(response, list) else []
110
+
111
+ # Limit results
112
+ limited_events = events[:max_results]
113
+
114
+ return {
115
+ "success": True,
116
+ "entity_id": entity_id,
117
+ "events": limited_events,
118
+ "count": len(limited_events),
119
+ "total_available": len(events),
120
+ "time_range": {
121
+ "start": start,
122
+ "end": end,
123
+ },
124
+ "message": f"Retrieved {len(limited_events)} event(s) from {entity_id}",
125
+ }
126
+
127
+ except Exception as error:
128
+ error_str = str(error)
129
+ logger.error(f"Failed to get calendar events for {entity_id}: {error}")
130
+
131
+ # Provide helpful error messages
132
+ suggestions = [
133
+ f"Verify calendar entity '{entity_id}' exists using ha_search_entities(query='calendar', domain_filter='calendar')",
134
+ "Check start/end datetime format (ISO 8601)",
135
+ "Ensure calendar integration supports event retrieval",
136
+ ]
137
+
138
+ if "404" in error_str or "not found" in error_str.lower():
139
+ suggestions.insert(0, f"Calendar entity '{entity_id}' not found")
140
+
141
+ return {
142
+ "success": False,
143
+ "error": error_str,
144
+ "entity_id": entity_id,
145
+ "events": [],
146
+ "suggestions": suggestions,
147
+ }
148
+
149
+ @mcp.tool(annotations={"destructiveHint": True, "tags": ["calendar"], "title": "Create or Update Calendar Event"})
150
+ @log_tool_usage
151
+ async def ha_config_set_calendar_event(
152
+ entity_id: Annotated[
153
+ str, Field(description="Calendar entity ID (e.g., 'calendar.family')")
154
+ ],
155
+ summary: Annotated[str, Field(description="Event title/summary")],
156
+ start: Annotated[
157
+ str, Field(description="Event start datetime in ISO format")
158
+ ],
159
+ end: Annotated[str, Field(description="Event end datetime in ISO format")],
160
+ description: Annotated[
161
+ str | None,
162
+ Field(description="Optional event description", default=None),
163
+ ] = None,
164
+ location: Annotated[
165
+ str | None, Field(description="Optional event location", default=None)
166
+ ] = None,
167
+ ) -> dict[str, Any]:
168
+ """
169
+ Create a new event in a calendar.
170
+
171
+ Creates a calendar event using the calendar.create_event service.
172
+
173
+ **Parameters:**
174
+ - entity_id: Calendar entity ID (e.g., 'calendar.family')
175
+ - summary: Event title/summary
176
+ - start: Event start datetime in ISO format
177
+ - end: Event end datetime in ISO format
178
+ - description: Optional event description
179
+ - location: Optional event location
180
+
181
+ **Example Usage:**
182
+ ```python
183
+ # Create a simple event
184
+ result = ha_config_set_calendar_event(
185
+ "calendar.family",
186
+ summary="Doctor appointment",
187
+ start="2024-01-15T14:00:00",
188
+ end="2024-01-15T15:00:00"
189
+ )
190
+
191
+ # Create an event with details
192
+ result = ha_config_set_calendar_event(
193
+ "calendar.work",
194
+ summary="Team meeting",
195
+ start="2024-01-16T10:00:00",
196
+ end="2024-01-16T11:00:00",
197
+ description="Weekly sync meeting",
198
+ location="Conference Room A"
199
+ )
200
+ ```
201
+
202
+ **Returns:**
203
+ - Success status and event details
204
+ """
205
+ try:
206
+ # Validate entity_id
207
+ if not entity_id.startswith("calendar."):
208
+ return {
209
+ "success": False,
210
+ "error": f"Invalid calendar entity ID: {entity_id}. Must start with 'calendar.'",
211
+ "entity_id": entity_id,
212
+ "suggestions": [
213
+ "Use ha_search_entities(query='calendar', domain_filter='calendar') to find calendar entities",
214
+ "Calendar entity IDs start with 'calendar.' prefix",
215
+ ],
216
+ }
217
+
218
+ # Build service data
219
+ service_data: dict[str, Any] = {
220
+ "entity_id": entity_id,
221
+ "summary": summary,
222
+ "start_date_time": start,
223
+ "end_date_time": end,
224
+ }
225
+
226
+ if description:
227
+ service_data["description"] = description
228
+ if location:
229
+ service_data["location"] = location
230
+
231
+ # Call the calendar.create_event service
232
+ result = await client.call_service("calendar", "create_event", service_data)
233
+
234
+ return {
235
+ "success": True,
236
+ "entity_id": entity_id,
237
+ "event": {
238
+ "summary": summary,
239
+ "start": start,
240
+ "end": end,
241
+ "description": description,
242
+ "location": location,
243
+ },
244
+ "result": result,
245
+ "message": f"Successfully created event '{summary}' in {entity_id}",
246
+ }
247
+
248
+ except Exception as error:
249
+ error_str = str(error)
250
+ logger.error(f"Failed to create calendar event in {entity_id}: {error}")
251
+
252
+ suggestions = [
253
+ f"Verify calendar entity '{entity_id}' exists and supports event creation",
254
+ "Check datetime format (ISO 8601)",
255
+ "Ensure end time is after start time",
256
+ "Some calendar integrations may be read-only",
257
+ ]
258
+
259
+ if "404" in error_str or "not found" in error_str.lower():
260
+ suggestions.insert(0, f"Calendar entity '{entity_id}' not found")
261
+ if "not supported" in error_str.lower():
262
+ suggestions.insert(0, "This calendar does not support event creation")
263
+
264
+ return {
265
+ "success": False,
266
+ "error": error_str,
267
+ "entity_id": entity_id,
268
+ "event": {
269
+ "summary": summary,
270
+ "start": start,
271
+ "end": end,
272
+ },
273
+ "suggestions": suggestions,
274
+ }
275
+
276
+ @mcp.tool(annotations={"destructiveHint": True, "idempotentHint": True, "tags": ["calendar"], "title": "Remove Calendar Event"})
277
+ @log_tool_usage
278
+ async def ha_config_remove_calendar_event(
279
+ entity_id: Annotated[
280
+ str, Field(description="Calendar entity ID (e.g., 'calendar.family')")
281
+ ],
282
+ uid: Annotated[str, Field(description="Unique identifier of the event to delete")],
283
+ recurrence_id: Annotated[
284
+ str | None,
285
+ Field(description="Optional recurrence ID for recurring events", default=None),
286
+ ] = None,
287
+ recurrence_range: Annotated[
288
+ str | None,
289
+ Field(
290
+ description="Optional recurrence range ('THIS_AND_FUTURE' to delete this and future occurrences)",
291
+ default=None,
292
+ ),
293
+ ] = None,
294
+ ) -> dict[str, Any]:
295
+ """
296
+ Delete an event from a calendar.
297
+
298
+ Deletes a calendar event using the calendar.delete_event service.
299
+
300
+ **Parameters:**
301
+ - entity_id: Calendar entity ID (e.g., 'calendar.family')
302
+ - uid: Unique identifier of the event to delete
303
+ - recurrence_id: Optional recurrence ID for recurring events
304
+ - recurrence_range: Optional recurrence range ('THIS_AND_FUTURE' to delete this and future occurrences)
305
+
306
+ **Example Usage:**
307
+ ```python
308
+ # Delete a single event
309
+ result = ha_config_remove_calendar_event(
310
+ "calendar.family",
311
+ uid="event-12345"
312
+ )
313
+
314
+ # Delete a recurring event instance and future occurrences
315
+ result = ha_config_remove_calendar_event(
316
+ "calendar.work",
317
+ uid="recurring-event-67890",
318
+ recurrence_id="20240115T100000",
319
+ recurrence_range="THIS_AND_FUTURE"
320
+ )
321
+ ```
322
+
323
+ **Note:**
324
+ To get the event UID, first use ha_config_get_calendar_events() to list events.
325
+ The UID is returned in each event's data.
326
+
327
+ **Returns:**
328
+ - Success status and deletion confirmation
329
+ """
330
+ try:
331
+ # Validate entity_id
332
+ if not entity_id.startswith("calendar."):
333
+ return {
334
+ "success": False,
335
+ "error": f"Invalid calendar entity ID: {entity_id}. Must start with 'calendar.'",
336
+ "entity_id": entity_id,
337
+ "suggestions": [
338
+ "Use ha_search_entities(query='calendar', domain_filter='calendar') to find calendar entities",
339
+ "Calendar entity IDs start with 'calendar.' prefix",
340
+ ],
341
+ }
342
+
343
+ # Build service data
344
+ service_data: dict[str, Any] = {
345
+ "entity_id": entity_id,
346
+ "uid": uid,
347
+ }
348
+
349
+ if recurrence_id:
350
+ service_data["recurrence_id"] = recurrence_id
351
+ if recurrence_range:
352
+ service_data["recurrence_range"] = recurrence_range
353
+
354
+ # Call the calendar.delete_event service
355
+ result = await client.call_service("calendar", "delete_event", service_data)
356
+
357
+ return {
358
+ "success": True,
359
+ "entity_id": entity_id,
360
+ "uid": uid,
361
+ "recurrence_id": recurrence_id,
362
+ "recurrence_range": recurrence_range,
363
+ "result": result,
364
+ "message": f"Successfully deleted event '{uid}' from {entity_id}",
365
+ }
366
+
367
+ except Exception as error:
368
+ error_str = str(error)
369
+ logger.error(f"Failed to delete calendar event from {entity_id}: {error}")
370
+
371
+ suggestions = [
372
+ f"Verify calendar entity '{entity_id}' exists",
373
+ f"Verify event with UID '{uid}' exists in the calendar",
374
+ "Use ha_config_get_calendar_events() to find the correct event UID",
375
+ "Some calendar integrations may not support event deletion",
376
+ ]
377
+
378
+ if "404" in error_str or "not found" in error_str.lower():
379
+ suggestions.insert(
380
+ 0, f"Calendar entity '{entity_id}' or event '{uid}' not found"
381
+ )
382
+ if "not supported" in error_str.lower():
383
+ suggestions.insert(0, "This calendar does not support event deletion")
384
+
385
+ return {
386
+ "success": False,
387
+ "error": error_str,
388
+ "entity_id": entity_id,
389
+ "uid": uid,
390
+ "suggestions": suggestions,
391
+ }
@@ -0,0 +1,149 @@
1
+ """
2
+ Camera tools for Home Assistant MCP server.
3
+
4
+ This module provides camera-related tools including snapshot retrieval
5
+ that returns images directly to the LLM for visual analysis.
6
+ """
7
+
8
+ import logging
9
+ from typing import Any
10
+
11
+ from fastmcp.utilities.types import Image
12
+
13
+ from .helpers import log_tool_usage
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ def register_camera_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
19
+ """Register Home Assistant camera tools."""
20
+
21
+ @mcp.tool(annotations={"idempotentHint": True, "readOnlyHint": True, "tags": ["camera"], "title": "Get Camera Image"})
22
+ @log_tool_usage
23
+ async def ha_get_camera_image(
24
+ entity_id: str,
25
+ width: int | None = None,
26
+ height: int | None = None,
27
+ ) -> Image:
28
+ """
29
+ Retrieve a snapshot image from a Home Assistant camera entity.
30
+
31
+ This tool fetches the current camera image and returns it directly for visual
32
+ analysis. Use this when you need to see what a camera is currently viewing.
33
+
34
+ **Parameters:**
35
+ - entity_id: Camera entity ID (e.g., 'camera.front_door', 'camera.living_room')
36
+ - width: Optional width to resize the image (reduces token usage for large images)
37
+ - height: Optional height to resize the image
38
+
39
+ **Use Cases:**
40
+ - Security checks: "Is someone at the front door?"
41
+ - Pet monitoring: "Is my dog still on the couch?"
42
+ - Delivery verification: "Did my package get delivered?"
43
+ - Visual confirmation: "Did the garage door actually close?"
44
+ - Incident investigation: "What triggered the motion sensor?"
45
+
46
+ **Example Usage:**
47
+ ```python
48
+ # Get current snapshot from front door camera
49
+ ha_get_camera_image(entity_id="camera.front_door")
50
+
51
+ # Get resized image to reduce token usage
52
+ ha_get_camera_image(entity_id="camera.backyard", width=640, height=480)
53
+ ```
54
+
55
+ **Notes:**
56
+ - Only cameras exposed to Home Assistant are accessible
57
+ - The existing HA authentication/authorization applies
58
+ - Images are returned in their native format (JPEG, PNG, or GIF)
59
+ - Use width/height parameters for large high-resolution cameras to reduce
60
+ token usage when full resolution is not needed
61
+
62
+ **Related Services:**
63
+ - camera.snapshot: Save snapshot to file on HA server
64
+ - camera.turn_on/turn_off: Control camera power
65
+ - camera.enable_motion_detection: Enable motion detection
66
+ """
67
+ # Validate entity_id format
68
+ if not entity_id or "." not in entity_id:
69
+ raise ValueError(
70
+ f"Invalid entity_id format: {entity_id}. "
71
+ "Expected format: camera.entity_name"
72
+ )
73
+
74
+ # Validate domain is camera
75
+ domain = entity_id.split(".")[0]
76
+ if domain != "camera":
77
+ raise ValueError(
78
+ f"Entity {entity_id} is not a camera entity. "
79
+ f"Domain is '{domain}', expected 'camera'."
80
+ )
81
+
82
+ # Build the camera proxy URL with optional size parameters
83
+ # Home Assistant camera proxy API: /api/camera_proxy/<entity_id>
84
+ endpoint = f"/camera_proxy/{entity_id}"
85
+
86
+ params = {}
87
+ if width is not None:
88
+ params["width"] = str(width)
89
+ if height is not None:
90
+ params["height"] = str(height)
91
+
92
+ try:
93
+ # Use the client's httpx_client directly for binary image data
94
+ response = await client.httpx_client.get(endpoint, params=params or None)
95
+
96
+ # Handle authentication errors
97
+ if response.status_code == 401:
98
+ raise PermissionError("Invalid authentication token for camera access")
99
+
100
+ # Handle not found errors
101
+ if response.status_code == 404:
102
+ raise ValueError(
103
+ f"Camera entity not found: {entity_id}. "
104
+ "Use ha_search_entities() to find available cameras."
105
+ )
106
+
107
+ # Handle other HTTP errors
108
+ if response.status_code >= 400:
109
+ raise RuntimeError(
110
+ f"Failed to retrieve camera image: HTTP {response.status_code}"
111
+ )
112
+
113
+ # Get the image bytes
114
+ image_data = response.content
115
+
116
+ if not image_data:
117
+ raise RuntimeError(
118
+ f"Camera {entity_id} returned empty image data. "
119
+ "The camera may be offline or unavailable."
120
+ )
121
+
122
+ # Determine MIME type from response headers or default to JPEG
123
+ content_type = response.headers.get("content-type", "image/jpeg")
124
+ if "jpeg" in content_type or "jpg" in content_type:
125
+ image_format = "jpeg"
126
+ elif "png" in content_type:
127
+ image_format = "png"
128
+ elif "gif" in content_type:
129
+ image_format = "gif"
130
+ else:
131
+ # Default to JPEG as Home Assistant camera proxy typically returns JPEG
132
+ image_format = "jpeg"
133
+
134
+ logger.info(
135
+ f"Retrieved camera image from {entity_id} "
136
+ f"({len(image_data)} bytes, format={image_format})"
137
+ )
138
+
139
+ # Return FastMCP Image object which automatically converts to MCP ImageContent
140
+ return Image(data=image_data, format=image_format)
141
+
142
+ except (PermissionError, ValueError, RuntimeError):
143
+ raise
144
+ except Exception as e:
145
+ logger.error(f"Error retrieving camera image from {entity_id}: {e}")
146
+ raise RuntimeError(
147
+ f"Failed to retrieve camera image from {entity_id}: {str(e)}. "
148
+ "Ensure the camera is online and accessible."
149
+ )