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,306 @@
1
+ """
2
+ Entity group management tools for Home Assistant.
3
+
4
+ This module provides tools for listing, creating/updating, and removing
5
+ Home Assistant entity groups (old-style groups created via group.set service).
6
+ """
7
+
8
+ import logging
9
+ from typing import Annotated, Any
10
+
11
+ from pydantic import Field
12
+
13
+ from .helpers import log_tool_usage
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ def register_group_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
19
+ """Register Home Assistant entity group management tools."""
20
+
21
+ @mcp.tool(annotations={"idempotentHint": True, "readOnlyHint": True, "tags": ["group"], "title": "List Groups"})
22
+ @log_tool_usage
23
+ async def ha_config_list_groups() -> dict[str, Any]:
24
+ """
25
+ List all Home Assistant entity groups with their member entities.
26
+
27
+ Returns all groups created via group.set service or YAML configuration,
28
+ including:
29
+ - Entity ID (group.xxx)
30
+ - Friendly name
31
+ - State (on/off based on member states)
32
+ - Member entities
33
+ - Icon (if set)
34
+ - All mode (if all entities must be on)
35
+
36
+ EXAMPLES:
37
+ - List all groups: ha_config_list_groups()
38
+
39
+ **NOTE:** This returns old-style groups (created via group.set or YAML).
40
+ Platform-specific groups (light groups, cover groups) are separate entities.
41
+ """
42
+ try:
43
+ # Get all entity states and filter for groups
44
+ states = await client.get_states()
45
+
46
+ groups = []
47
+ for state in states:
48
+ entity_id = state.get("entity_id", "")
49
+ if entity_id.startswith("group."):
50
+ attributes = state.get("attributes", {})
51
+ groups.append(
52
+ {
53
+ "entity_id": entity_id,
54
+ "object_id": entity_id.removeprefix("group."),
55
+ "state": state.get("state"),
56
+ "friendly_name": attributes.get("friendly_name"),
57
+ "icon": attributes.get("icon"),
58
+ "entity_ids": attributes.get("entity_id", []),
59
+ "all": attributes.get("all", False),
60
+ "order": attributes.get("order"),
61
+ }
62
+ )
63
+
64
+ # Sort by friendly name or entity_id
65
+ groups.sort(
66
+ key=lambda g: (g.get("friendly_name") or g.get("entity_id", "")).lower()
67
+ )
68
+
69
+ return {
70
+ "success": True,
71
+ "count": len(groups),
72
+ "groups": groups,
73
+ "message": f"Found {len(groups)} group(s)",
74
+ }
75
+
76
+ except Exception as e:
77
+ logger.error(f"Error listing groups: {e}")
78
+ return {
79
+ "success": False,
80
+ "error": f"Failed to list groups: {str(e)}",
81
+ "suggestions": [
82
+ "Check Home Assistant connection",
83
+ "Verify REST API is accessible",
84
+ ],
85
+ }
86
+
87
+ @mcp.tool(annotations={"destructiveHint": True, "tags": ["group"], "title": "Create or Update Group"})
88
+ @log_tool_usage
89
+ async def ha_config_set_group(
90
+ object_id: Annotated[
91
+ str,
92
+ Field(
93
+ description="Group identifier without 'group.' prefix (e.g., 'living_room_lights')"
94
+ ),
95
+ ],
96
+ entities: Annotated[
97
+ list[str] | None,
98
+ Field(
99
+ description="List of entity IDs for the group. Required when creating new group. When updating, replaces all entities (mutually exclusive with add_entities/remove_entities).",
100
+ default=None,
101
+ ),
102
+ ] = None,
103
+ name: Annotated[
104
+ str | None,
105
+ Field(
106
+ description="Friendly display name for the group",
107
+ default=None,
108
+ ),
109
+ ] = None,
110
+ icon: Annotated[
111
+ str | None,
112
+ Field(
113
+ description="Material Design Icon (e.g., 'mdi:lightbulb-group')",
114
+ default=None,
115
+ ),
116
+ ] = None,
117
+ all_on: Annotated[
118
+ bool | None,
119
+ Field(
120
+ description="If True, all entities must be on for group to be on (default: False)",
121
+ default=None,
122
+ ),
123
+ ] = None,
124
+ add_entities: Annotated[
125
+ list[str] | None,
126
+ Field(
127
+ description="Add these entities to an existing group (mutually exclusive with entities)",
128
+ default=None,
129
+ ),
130
+ ] = None,
131
+ remove_entities: Annotated[
132
+ list[str] | None,
133
+ Field(
134
+ description="Remove these entities from an existing group (mutually exclusive with entities)",
135
+ default=None,
136
+ ),
137
+ ] = None,
138
+ ) -> dict[str, Any]:
139
+ """
140
+ Create or update a Home Assistant entity group.
141
+
142
+ Uses the group.set service to create a new group or update an existing one.
143
+ Groups are useful for organizing entities and controlling them together.
144
+
145
+ **For NEW groups:** Provide object_id and entities (required).
146
+ **For EXISTING groups:** Provide object_id and any fields to update.
147
+
148
+ EXAMPLES:
149
+ - Create group: ha_config_set_group("bedroom_lights", entities=["light.lamp", "light.ceiling"])
150
+ - Create with name: ha_config_set_group("sensors", entities=["sensor.temp"], name="All Sensors")
151
+ - Update name: ha_config_set_group("lights", name="Living Room Lights")
152
+ - Add entities: ha_config_set_group("lights", add_entities=["light.extra"])
153
+ - Remove entities: ha_config_set_group("lights", remove_entities=["light.old"])
154
+ - Replace all entities: ha_config_set_group("lights", entities=["light.new1", "light.new2"])
155
+
156
+ **NOTE:** entities, add_entities, and remove_entities are mutually exclusive.
157
+ """
158
+ try:
159
+ # Validate object_id doesn't contain invalid characters
160
+ if "." in object_id:
161
+ return {
162
+ "success": False,
163
+ "error": f"Invalid object_id: '{object_id}'. Do not include 'group.' prefix or dots.",
164
+ "object_id": object_id,
165
+ }
166
+
167
+ # Check mutual exclusivity of entity operations
168
+ entity_ops = [
169
+ ("entities", entities),
170
+ ("add_entities", add_entities),
171
+ ("remove_entities", remove_entities),
172
+ ]
173
+ provided_ops = [
174
+ (op_name, val) for op_name, val in entity_ops if val is not None
175
+ ]
176
+
177
+ if len(provided_ops) > 1:
178
+ op_names = [op_name for op_name, _ in provided_ops]
179
+ return {
180
+ "success": False,
181
+ "error": f"Only one of entities, add_entities, or remove_entities can be provided. Got: {op_names}",
182
+ "object_id": object_id,
183
+ }
184
+
185
+ # Validate non-empty lists
186
+ if entities is not None and not entities:
187
+ return {
188
+ "success": False,
189
+ "error": "Entities list cannot be empty",
190
+ "object_id": object_id,
191
+ }
192
+ if add_entities is not None and not add_entities:
193
+ return {
194
+ "success": False,
195
+ "error": "add_entities list cannot be empty",
196
+ "object_id": object_id,
197
+ }
198
+
199
+ # Build service data
200
+ service_data: dict[str, Any] = {
201
+ "object_id": object_id,
202
+ }
203
+
204
+ if name is not None:
205
+ service_data["name"] = name
206
+ if icon is not None:
207
+ service_data["icon"] = icon
208
+ if all_on is not None:
209
+ service_data["all"] = all_on
210
+ if entities is not None:
211
+ service_data["entities"] = entities
212
+ if add_entities is not None:
213
+ service_data["add_entities"] = add_entities
214
+ if remove_entities is not None:
215
+ service_data["remove_entities"] = remove_entities
216
+
217
+ # Call group.set service
218
+ await client.call_service("group", "set", service_data)
219
+
220
+ entity_id = f"group.{object_id}"
221
+ updated_fields = [k for k in service_data.keys() if k != "object_id"]
222
+
223
+ # Determine if this was a create or update based on fields provided
224
+ is_create = entities is not None and name is None and add_entities is None and remove_entities is None
225
+
226
+ return {
227
+ "success": True,
228
+ "entity_id": entity_id,
229
+ "object_id": object_id,
230
+ "updated_fields": updated_fields,
231
+ "message": f"Successfully {'created' if is_create else 'updated'} group: {entity_id}",
232
+ }
233
+
234
+ except Exception as e:
235
+ logger.error(f"Error setting group: {e}")
236
+ return {
237
+ "success": False,
238
+ "error": f"Failed to set group: {str(e)}",
239
+ "object_id": object_id,
240
+ "suggestions": [
241
+ "Check Home Assistant connection",
242
+ "Verify all entity IDs in the entities list exist",
243
+ "Ensure object_id is valid (no dots, no 'group.' prefix)",
244
+ "Use ha_config_list_groups() to see existing groups",
245
+ ],
246
+ }
247
+
248
+ @mcp.tool(annotations={"destructiveHint": True, "idempotentHint": True, "tags": ["group"], "title": "Remove Group"})
249
+ @log_tool_usage
250
+ async def ha_config_remove_group(
251
+ object_id: Annotated[
252
+ str,
253
+ Field(
254
+ description="Group identifier without 'group.' prefix (e.g., 'living_room_lights')"
255
+ ),
256
+ ],
257
+ ) -> dict[str, Any]:
258
+ """
259
+ Remove a Home Assistant entity group.
260
+
261
+ Uses the group.remove service to delete the group.
262
+
263
+ EXAMPLES:
264
+ - Remove group: ha_config_remove_group("living_room_lights")
265
+
266
+ Use ha_config_list_groups() to find existing groups.
267
+
268
+ **WARNING:**
269
+ - Removing a group used in automations may cause those automations to fail.
270
+ - Groups defined in YAML can be removed at runtime but will reappear after restart.
271
+ - This only removes old-style groups, not platform-specific groups.
272
+ """
273
+ try:
274
+ # Validate object_id
275
+ if "." in object_id:
276
+ return {
277
+ "success": False,
278
+ "error": f"Invalid object_id: '{object_id}'. Do not include 'group.' prefix.",
279
+ "object_id": object_id,
280
+ }
281
+
282
+ # Call group.remove service
283
+ service_data = {"object_id": object_id}
284
+ await client.call_service("group", "remove", service_data)
285
+
286
+ entity_id = f"group.{object_id}"
287
+
288
+ return {
289
+ "success": True,
290
+ "entity_id": entity_id,
291
+ "object_id": object_id,
292
+ "message": f"Successfully removed group: {entity_id}",
293
+ }
294
+
295
+ except Exception as e:
296
+ logger.error(f"Error removing group: {e}")
297
+ return {
298
+ "success": False,
299
+ "error": f"Failed to remove group: {str(e)}",
300
+ "object_id": object_id,
301
+ "suggestions": [
302
+ "Check Home Assistant connection",
303
+ "Verify the group exists using ha_config_list_groups()",
304
+ "Groups defined in YAML cannot be permanently removed",
305
+ ],
306
+ }