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,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
|
+
}
|