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,530 @@
1
+ """
2
+ Todo/Shopping List management tools for Home Assistant MCP server.
3
+
4
+ This module provides tools for managing Home Assistant todo lists including:
5
+ - Listing all todo list entities
6
+ - Getting items from a todo list
7
+ - Adding items to a todo list
8
+ - Updating/completing todo items
9
+ - Removing items from a todo list
10
+ """
11
+
12
+ import logging
13
+ from typing import Annotated, Any, Literal
14
+
15
+ from pydantic import Field
16
+
17
+ from .helpers import log_tool_usage
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ def register_todo_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
23
+ """Register Home Assistant todo list management tools."""
24
+
25
+ @mcp.tool(annotations={"idempotentHint": True, "readOnlyHint": True, "tags": ["todo"], "title": "Get Todo"})
26
+ @log_tool_usage
27
+ async def ha_get_todo(
28
+ entity_id: Annotated[
29
+ str | None,
30
+ Field(
31
+ description="Todo list entity ID (e.g., 'todo.shopping_list'). "
32
+ "If omitted, lists all todo list entities.",
33
+ default=None,
34
+ ),
35
+ ] = None,
36
+ status: Annotated[
37
+ Literal["needs_action", "completed"] | None,
38
+ Field(
39
+ description="Filter items by status: 'needs_action' for incomplete, 'completed' for done. "
40
+ "Only applies when entity_id is provided.",
41
+ default=None,
42
+ ),
43
+ ] = None,
44
+ ) -> dict[str, Any]:
45
+ """
46
+ Get todo lists or items - list all todo lists or get items from a specific list.
47
+
48
+ Without an entity_id: Lists all todo list entities in Home Assistant.
49
+ With an entity_id: Gets items from that specific todo list, optionally filtered by status.
50
+
51
+ **LISTING TODO LISTS (entity_id omitted):**
52
+ Returns all entities in the 'todo' domain, including shopping lists
53
+ and any other todo-type integrations.
54
+
55
+ Each todo list includes:
56
+ - entity_id: The unique identifier (e.g., 'todo.shopping_list')
57
+ - friendly_name: Human-readable name
58
+ - state: Number of incomplete items or current status
59
+
60
+ **GETTING TODO ITEMS (entity_id provided):**
61
+ Retrieves items from the specified todo list.
62
+
63
+ Status filter values:
64
+ - needs_action: Items that still need to be done
65
+ - completed: Items that have been marked as done
66
+ - None (default): Returns all items regardless of status
67
+
68
+ Item properties:
69
+ - uid: Unique identifier for the item
70
+ - summary: The item text/description
71
+ - status: Current status (needs_action or completed)
72
+ - description: Optional detailed description
73
+ - due: Optional due date (if supported)
74
+
75
+ EXAMPLES:
76
+ - List all todo lists: ha_get_todo()
77
+ - Get all items: ha_get_todo("todo.shopping_list")
78
+ - Get incomplete items: ha_get_todo("todo.shopping_list", status="needs_action")
79
+ - Get completed items: ha_get_todo("todo.shopping_list", status="completed")
80
+
81
+ USE CASES:
82
+ - "What todo lists do I have?"
83
+ - "Show me my shopping list"
84
+ - "What's on my todo list?"
85
+ - "Show completed items"
86
+ """
87
+ try:
88
+ # List mode - no entity_id provided
89
+ if entity_id is None:
90
+ # Get all states and filter by todo domain
91
+ states = await client.get_states()
92
+
93
+ todo_lists = []
94
+ for state in states:
95
+ eid = state.get("entity_id", "")
96
+ if eid.startswith("todo."):
97
+ todo_lists.append({
98
+ "entity_id": eid,
99
+ "friendly_name": state.get("attributes", {}).get(
100
+ "friendly_name", eid
101
+ ),
102
+ "state": state.get("state"),
103
+ "icon": state.get("attributes", {}).get("icon"),
104
+ "supported_features": state.get("attributes", {}).get(
105
+ "supported_features"
106
+ ),
107
+ })
108
+
109
+ return {
110
+ "success": True,
111
+ "count": len(todo_lists),
112
+ "todo_lists": todo_lists,
113
+ "message": f"Found {len(todo_lists)} todo list(s)",
114
+ }
115
+
116
+ # Get items mode - entity_id provided
117
+ # Validate entity_id format
118
+ if not entity_id.startswith("todo."):
119
+ return {
120
+ "success": False,
121
+ "error": f"Invalid entity_id: {entity_id}. Must start with 'todo.'",
122
+ "suggestions": ["Use ha_get_todo() without entity_id to find valid todo list entity IDs"],
123
+ }
124
+
125
+ # Use WebSocket to get todo items
126
+ message: dict[str, Any] = {
127
+ "type": "todo/item/list",
128
+ "entity_id": entity_id,
129
+ }
130
+
131
+ result = await client.send_websocket_message(message)
132
+
133
+ if result.get("success"):
134
+ items = result.get("result", {}).get("items", [])
135
+
136
+ # Filter by status if specified
137
+ if status:
138
+ items = [item for item in items if item.get("status") == status]
139
+
140
+ return {
141
+ "success": True,
142
+ "entity_id": entity_id,
143
+ "status_filter": status,
144
+ "count": len(items),
145
+ "items": items,
146
+ "message": f"Found {len(items)} item(s) in {entity_id}",
147
+ }
148
+ else:
149
+ error = result.get("error", "Unknown error")
150
+ return {
151
+ "success": False,
152
+ "error": f"Failed to get todo items: {error}",
153
+ "entity_id": entity_id,
154
+ "suggestions": [
155
+ "Verify the entity_id exists using ha_get_todo()",
156
+ "Check Home Assistant WebSocket connection",
157
+ ],
158
+ }
159
+
160
+ except Exception as e:
161
+ logger.error(f"Error in ha_get_todo: {e}")
162
+ if entity_id:
163
+ return {
164
+ "success": False,
165
+ "error": f"Failed to get todo items: {str(e)}",
166
+ "entity_id": entity_id,
167
+ "suggestions": [
168
+ "Check Home Assistant connection",
169
+ "Verify entity_id is correct",
170
+ "Use ha_get_todo() to find valid todo lists",
171
+ ],
172
+ }
173
+ else:
174
+ return {
175
+ "success": False,
176
+ "error": f"Failed to list todo lists: {str(e)}",
177
+ "suggestions": [
178
+ "Check Home Assistant connection",
179
+ "Verify todo integration is enabled",
180
+ ],
181
+ }
182
+
183
+ @mcp.tool(annotations={"destructiveHint": True, "tags": ["todo"], "title": "Add Todo Item"})
184
+ @log_tool_usage
185
+ async def ha_add_todo_item(
186
+ entity_id: Annotated[
187
+ str,
188
+ Field(
189
+ description="Todo list entity ID (e.g., 'todo.shopping_list')"
190
+ ),
191
+ ],
192
+ summary: Annotated[
193
+ str,
194
+ Field(description="Item text/name to add (e.g., 'Buy milk')"),
195
+ ],
196
+ description: Annotated[
197
+ str | None,
198
+ Field(
199
+ description="Optional detailed description for the item",
200
+ default=None,
201
+ ),
202
+ ] = None,
203
+ due_date: Annotated[
204
+ str | None,
205
+ Field(
206
+ description="Optional due date in YYYY-MM-DD format (e.g., '2024-12-25')",
207
+ default=None,
208
+ ),
209
+ ] = None,
210
+ due_datetime: Annotated[
211
+ str | None,
212
+ Field(
213
+ description="Optional due datetime in ISO format (e.g., '2024-12-25T14:00:00')",
214
+ default=None,
215
+ ),
216
+ ] = None,
217
+ ) -> dict[str, Any]:
218
+ """
219
+ Add an item to a Home Assistant todo list.
220
+
221
+ Creates a new item in the specified todo list with optional description and due date.
222
+
223
+ PARAMETERS:
224
+ - entity_id: The todo list to add to (e.g., 'todo.shopping_list')
225
+ - summary: The item text (required)
226
+ - description: Optional detailed description
227
+ - due_date: Optional due date (YYYY-MM-DD format)
228
+ - due_datetime: Optional due datetime (ISO format, overrides due_date if both provided)
229
+
230
+ EXAMPLES:
231
+ - Add simple item: ha_add_todo_item("todo.shopping_list", "Buy milk")
232
+ - Add with description: ha_add_todo_item("todo.shopping_list", "Buy milk", description="2% organic")
233
+ - Add with due date: ha_add_todo_item("todo.tasks", "Pay bills", due_date="2024-12-31")
234
+
235
+ USE CASES:
236
+ - "Add milk to my shopping list"
237
+ - "Add 'call mom' to my todo list"
238
+ - "Remind me to pay rent by the 1st"
239
+
240
+ NOTE: Not all todo integrations support all features (description, due dates).
241
+ The Shopping List integration only supports summary.
242
+ """
243
+ try:
244
+ # Validate entity_id format
245
+ if not entity_id.startswith("todo."):
246
+ return {
247
+ "success": False,
248
+ "error": f"Invalid entity_id: {entity_id}. Must start with 'todo.'",
249
+ "suggestions": ["Use ha_get_todo() to find valid todo list entity IDs"],
250
+ }
251
+
252
+ # Build service data
253
+ service_data: dict[str, Any] = {
254
+ "entity_id": entity_id,
255
+ "item": summary,
256
+ }
257
+
258
+ # Add optional fields if provided
259
+ if description:
260
+ service_data["description"] = description
261
+ if due_datetime:
262
+ service_data["due_datetime"] = due_datetime
263
+ elif due_date:
264
+ service_data["due_date"] = due_date
265
+
266
+ # Call the service
267
+ result = await client.call_service("todo", "add_item", service_data)
268
+
269
+ return {
270
+ "success": True,
271
+ "entity_id": entity_id,
272
+ "item": summary,
273
+ "description": description,
274
+ "due_date": due_date,
275
+ "due_datetime": due_datetime,
276
+ "result": result,
277
+ "message": f"Successfully added '{summary}' to {entity_id}",
278
+ }
279
+
280
+ except Exception as e:
281
+ logger.error(f"Error adding todo item: {e}")
282
+ return {
283
+ "success": False,
284
+ "error": f"Failed to add todo item: {str(e)}",
285
+ "entity_id": entity_id,
286
+ "item": summary,
287
+ "suggestions": [
288
+ "Verify the entity_id exists using ha_get_todo()",
289
+ "Check if the todo list supports adding items",
290
+ "Some todo lists may not support description or due dates",
291
+ ],
292
+ }
293
+
294
+ @mcp.tool(annotations={"destructiveHint": True, "tags": ["todo"], "title": "Update Todo Item"})
295
+ @log_tool_usage
296
+ async def ha_update_todo_item(
297
+ entity_id: Annotated[
298
+ str,
299
+ Field(
300
+ description="Todo list entity ID (e.g., 'todo.shopping_list')"
301
+ ),
302
+ ],
303
+ item: Annotated[
304
+ str,
305
+ Field(
306
+ description="Item to update - can be the item UID or the exact item summary/name"
307
+ ),
308
+ ],
309
+ rename: Annotated[
310
+ str | None,
311
+ Field(
312
+ description="New name/summary for the item",
313
+ default=None,
314
+ ),
315
+ ] = None,
316
+ status: Annotated[
317
+ Literal["needs_action", "completed"] | None,
318
+ Field(
319
+ description="New status: 'completed' to mark done, 'needs_action' to mark incomplete",
320
+ default=None,
321
+ ),
322
+ ] = None,
323
+ description: Annotated[
324
+ str | None,
325
+ Field(
326
+ description="New description for the item",
327
+ default=None,
328
+ ),
329
+ ] = None,
330
+ due_date: Annotated[
331
+ str | None,
332
+ Field(
333
+ description="New due date in YYYY-MM-DD format",
334
+ default=None,
335
+ ),
336
+ ] = None,
337
+ due_datetime: Annotated[
338
+ str | None,
339
+ Field(
340
+ description="New due datetime in ISO format",
341
+ default=None,
342
+ ),
343
+ ] = None,
344
+ ) -> dict[str, Any]:
345
+ """
346
+ Update or complete a todo item in Home Assistant.
347
+
348
+ Modifies an existing item in the specified todo list. Can be used to:
349
+ - Mark items as completed or incomplete
350
+ - Rename items
351
+ - Update descriptions and due dates
352
+
353
+ IDENTIFYING ITEMS:
354
+ - Use the item's UID (from ha_get_todo)
355
+ - Or use the exact item summary/name text
356
+
357
+ STATUS VALUES:
358
+ - completed: Mark the item as done
359
+ - needs_action: Mark the item as not done (reopen)
360
+
361
+ EXAMPLES:
362
+ - Complete item: ha_update_todo_item("todo.shopping_list", "Buy milk", status="completed")
363
+ - Rename item: ha_update_todo_item("todo.tasks", "Old task", rename="New task name")
364
+ - Update due date: ha_update_todo_item("todo.tasks", "Pay bills", due_date="2024-12-31")
365
+ - Reopen item: ha_update_todo_item("todo.tasks", "Task to redo", status="needs_action")
366
+
367
+ USE CASES:
368
+ - "Mark milk as bought"
369
+ - "Complete the eggs on my shopping list"
370
+ - "Change the due date for my task"
371
+ - "Rename 'call mom' to 'video call with mom'"
372
+
373
+ NOTE: At least one update field (rename, status, description, due_date, due_datetime) must be provided.
374
+ """
375
+ try:
376
+ # Validate entity_id format
377
+ if not entity_id.startswith("todo."):
378
+ return {
379
+ "success": False,
380
+ "error": f"Invalid entity_id: {entity_id}. Must start with 'todo.'",
381
+ "suggestions": ["Use ha_get_todo() to find valid todo list entity IDs"],
382
+ }
383
+
384
+ # Validate at least one update field is provided
385
+ if not any([rename, status, description, due_date, due_datetime]):
386
+ return {
387
+ "success": False,
388
+ "error": "At least one update field must be provided (rename, status, description, due_date, or due_datetime)",
389
+ "suggestions": ["Specify what to update, e.g., status='completed' to mark item done"],
390
+ }
391
+
392
+ # Build service data
393
+ service_data: dict[str, Any] = {
394
+ "entity_id": entity_id,
395
+ "item": item,
396
+ }
397
+
398
+ # Add update fields if provided
399
+ if rename:
400
+ service_data["rename"] = rename
401
+ if status:
402
+ service_data["status"] = status
403
+ if description:
404
+ service_data["description"] = description
405
+ if due_datetime:
406
+ service_data["due_datetime"] = due_datetime
407
+ elif due_date:
408
+ service_data["due_date"] = due_date
409
+
410
+ # Call the service
411
+ result = await client.call_service("todo", "update_item", service_data)
412
+
413
+ # Build response message
414
+ updates = []
415
+ if rename:
416
+ updates.append(f"renamed to '{rename}'")
417
+ if status:
418
+ updates.append(f"status set to '{status}'")
419
+ if description:
420
+ updates.append("description updated")
421
+ if due_date or due_datetime:
422
+ updates.append("due date updated")
423
+
424
+ update_msg = ", ".join(updates) if updates else "updated"
425
+
426
+ return {
427
+ "success": True,
428
+ "entity_id": entity_id,
429
+ "item": item,
430
+ "updates": {
431
+ "rename": rename,
432
+ "status": status,
433
+ "description": description,
434
+ "due_date": due_date,
435
+ "due_datetime": due_datetime,
436
+ },
437
+ "result": result,
438
+ "message": f"Successfully updated '{item}' in {entity_id}: {update_msg}",
439
+ }
440
+
441
+ except Exception as e:
442
+ logger.error(f"Error updating todo item: {e}")
443
+ return {
444
+ "success": False,
445
+ "error": f"Failed to update todo item: {str(e)}",
446
+ "entity_id": entity_id,
447
+ "item": item,
448
+ "suggestions": [
449
+ "Verify the item exists using ha_get_todo()",
450
+ "Check if you're using the correct item name or UID",
451
+ "Some todo lists may not support all update operations",
452
+ ],
453
+ }
454
+
455
+ @mcp.tool(annotations={"destructiveHint": True, "idempotentHint": True, "tags": ["todo"], "title": "Remove Todo Item"})
456
+ @log_tool_usage
457
+ async def ha_remove_todo_item(
458
+ entity_id: Annotated[
459
+ str,
460
+ Field(
461
+ description="Todo list entity ID (e.g., 'todo.shopping_list')"
462
+ ),
463
+ ],
464
+ item: Annotated[
465
+ str,
466
+ Field(
467
+ description="Item to remove - can be the item UID or the exact item summary/name"
468
+ ),
469
+ ],
470
+ ) -> dict[str, Any]:
471
+ """
472
+ Remove an item from a Home Assistant todo list.
473
+
474
+ Permanently deletes an item from the specified todo list.
475
+
476
+ IDENTIFYING ITEMS:
477
+ - Use the item's UID (from ha_get_todo)
478
+ - Or use the exact item summary/name text
479
+
480
+ EXAMPLES:
481
+ - Remove by name: ha_remove_todo_item("todo.shopping_list", "Buy milk")
482
+ - Remove by UID: ha_remove_todo_item("todo.shopping_list", "abc123-uid")
483
+
484
+ USE CASES:
485
+ - "Remove milk from my shopping list"
486
+ - "Delete the eggs item"
487
+ - "Clear 'call mom' from my todo"
488
+
489
+ WARNING: This permanently removes the item. To mark as completed instead,
490
+ use ha_update_todo_item() with status="completed".
491
+ """
492
+ try:
493
+ # Validate entity_id format
494
+ if not entity_id.startswith("todo."):
495
+ return {
496
+ "success": False,
497
+ "error": f"Invalid entity_id: {entity_id}. Must start with 'todo.'",
498
+ "suggestions": ["Use ha_get_todo() to find valid todo list entity IDs"],
499
+ }
500
+
501
+ # Build service data
502
+ service_data: dict[str, Any] = {
503
+ "entity_id": entity_id,
504
+ "item": item,
505
+ }
506
+
507
+ # Call the service
508
+ result = await client.call_service("todo", "remove_item", service_data)
509
+
510
+ return {
511
+ "success": True,
512
+ "entity_id": entity_id,
513
+ "item": item,
514
+ "result": result,
515
+ "message": f"Successfully removed '{item}' from {entity_id}",
516
+ }
517
+
518
+ except Exception as e:
519
+ logger.error(f"Error removing todo item: {e}")
520
+ return {
521
+ "success": False,
522
+ "error": f"Failed to remove todo item: {str(e)}",
523
+ "entity_id": entity_id,
524
+ "item": item,
525
+ "suggestions": [
526
+ "Verify the item exists using ha_get_todo()",
527
+ "Check if you're using the correct item name or UID",
528
+ "Make sure the item hasn't already been removed",
529
+ ],
530
+ }