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,478 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Area and floor management tools for Home Assistant.
|
|
3
|
+
|
|
4
|
+
This module provides tools for listing, creating, updating, and deleting
|
|
5
|
+
Home Assistant areas and floors - essential organizational features for smart homes.
|
|
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
|
+
from .util_helpers import parse_string_list_param
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def register_area_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
20
|
+
"""Register Home Assistant area and floor management tools."""
|
|
21
|
+
|
|
22
|
+
# ============================================================
|
|
23
|
+
# AREA TOOLS
|
|
24
|
+
# ============================================================
|
|
25
|
+
|
|
26
|
+
@mcp.tool(annotations={"idempotentHint": True, "readOnlyHint": True, "tags": ["area"], "title": "List Areas"})
|
|
27
|
+
@log_tool_usage
|
|
28
|
+
async def ha_config_list_areas() -> dict[str, Any]:
|
|
29
|
+
"""
|
|
30
|
+
List all Home Assistant areas (rooms).
|
|
31
|
+
|
|
32
|
+
Returns area ID, name, icon, floor assignment, aliases, and picture URL.
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
message: dict[str, Any] = {
|
|
36
|
+
"type": "config/area_registry/list",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
result = await client.send_websocket_message(message)
|
|
40
|
+
|
|
41
|
+
if result.get("success"):
|
|
42
|
+
areas = result.get("result", [])
|
|
43
|
+
return {
|
|
44
|
+
"success": True,
|
|
45
|
+
"count": len(areas),
|
|
46
|
+
"areas": areas,
|
|
47
|
+
"message": f"Found {len(areas)} area(s)",
|
|
48
|
+
}
|
|
49
|
+
else:
|
|
50
|
+
return {
|
|
51
|
+
"success": False,
|
|
52
|
+
"error": f"Failed to list areas: {result.get('error', 'Unknown error')}",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
except Exception as e:
|
|
56
|
+
logger.error(f"Error listing areas: {e}")
|
|
57
|
+
return {
|
|
58
|
+
"success": False,
|
|
59
|
+
"error": f"Failed to list areas: {str(e)}",
|
|
60
|
+
"suggestions": [
|
|
61
|
+
"Check Home Assistant connection",
|
|
62
|
+
"Verify WebSocket connection is active",
|
|
63
|
+
],
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@mcp.tool(annotations={"destructiveHint": True, "tags": ["area"], "title": "Create or Update Area"})
|
|
67
|
+
@log_tool_usage
|
|
68
|
+
async def ha_config_set_area(
|
|
69
|
+
name: Annotated[
|
|
70
|
+
str | None,
|
|
71
|
+
Field(
|
|
72
|
+
description="Name for the area (required for create, optional for update, e.g., 'Living Room', 'Kitchen')",
|
|
73
|
+
default=None,
|
|
74
|
+
),
|
|
75
|
+
] = None,
|
|
76
|
+
area_id: Annotated[
|
|
77
|
+
str | None,
|
|
78
|
+
Field(
|
|
79
|
+
description="Area ID to update (omit to create new area, use ha_config_list_areas to find IDs)",
|
|
80
|
+
default=None,
|
|
81
|
+
),
|
|
82
|
+
] = None,
|
|
83
|
+
floor_id: Annotated[
|
|
84
|
+
str | None,
|
|
85
|
+
Field(
|
|
86
|
+
description="Floor ID to assign this area to (use ha_config_list_floors to find IDs, empty string to remove)",
|
|
87
|
+
default=None,
|
|
88
|
+
),
|
|
89
|
+
] = None,
|
|
90
|
+
icon: Annotated[
|
|
91
|
+
str | None,
|
|
92
|
+
Field(
|
|
93
|
+
description="Material Design Icon (e.g., 'mdi:sofa', 'mdi:bed', empty string to remove)",
|
|
94
|
+
default=None,
|
|
95
|
+
),
|
|
96
|
+
] = None,
|
|
97
|
+
aliases: Annotated[
|
|
98
|
+
str | list[str] | None,
|
|
99
|
+
Field(
|
|
100
|
+
description="Alternative names for voice assistant recognition (e.g., ['lounge', 'family room'], empty list to clear)",
|
|
101
|
+
default=None,
|
|
102
|
+
),
|
|
103
|
+
] = None,
|
|
104
|
+
picture: Annotated[
|
|
105
|
+
str | None,
|
|
106
|
+
Field(
|
|
107
|
+
description="URL to a picture representing the area (empty string to remove)",
|
|
108
|
+
default=None,
|
|
109
|
+
),
|
|
110
|
+
] = None,
|
|
111
|
+
) -> dict[str, Any]:
|
|
112
|
+
"""
|
|
113
|
+
Create or update a Home Assistant area (room).
|
|
114
|
+
|
|
115
|
+
Provide name only to create a new area. Provide area_id to update existing.
|
|
116
|
+
Areas organize entities by physical location for room-based control.
|
|
117
|
+
"""
|
|
118
|
+
try:
|
|
119
|
+
# Parse aliases if provided as string
|
|
120
|
+
try:
|
|
121
|
+
parsed_aliases = parse_string_list_param(aliases, "aliases")
|
|
122
|
+
except ValueError as e:
|
|
123
|
+
return {"success": False, "error": f"Invalid aliases parameter: {e}"}
|
|
124
|
+
|
|
125
|
+
# Determine if this is a create or update operation
|
|
126
|
+
if area_id:
|
|
127
|
+
# UPDATE operation
|
|
128
|
+
message: dict[str, Any] = {
|
|
129
|
+
"type": "config/area_registry/update",
|
|
130
|
+
"area_id": area_id,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
# Only add fields that were explicitly provided
|
|
134
|
+
if name is not None:
|
|
135
|
+
message["name"] = name
|
|
136
|
+
if floor_id is not None:
|
|
137
|
+
message["floor_id"] = floor_id if floor_id else None
|
|
138
|
+
if icon is not None:
|
|
139
|
+
message["icon"] = icon if icon else None
|
|
140
|
+
if parsed_aliases is not None:
|
|
141
|
+
message["aliases"] = parsed_aliases
|
|
142
|
+
if picture is not None:
|
|
143
|
+
message["picture"] = picture if picture else None
|
|
144
|
+
|
|
145
|
+
operation = "update"
|
|
146
|
+
else:
|
|
147
|
+
# CREATE operation - name is required
|
|
148
|
+
if not name:
|
|
149
|
+
return {
|
|
150
|
+
"success": False,
|
|
151
|
+
"error": "name is required when creating a new area",
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
message: dict[str, Any] = {
|
|
155
|
+
"type": "config/area_registry/create",
|
|
156
|
+
"name": name,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if floor_id:
|
|
160
|
+
message["floor_id"] = floor_id
|
|
161
|
+
if icon:
|
|
162
|
+
message["icon"] = icon
|
|
163
|
+
if parsed_aliases:
|
|
164
|
+
message["aliases"] = parsed_aliases
|
|
165
|
+
if picture:
|
|
166
|
+
message["picture"] = picture
|
|
167
|
+
|
|
168
|
+
operation = "create"
|
|
169
|
+
|
|
170
|
+
result = await client.send_websocket_message(message)
|
|
171
|
+
|
|
172
|
+
if result.get("success"):
|
|
173
|
+
area_data = result.get("result", {})
|
|
174
|
+
area_name = name or area_data.get("name", area_id)
|
|
175
|
+
return {
|
|
176
|
+
"success": True,
|
|
177
|
+
"area": area_data,
|
|
178
|
+
"area_id": area_data.get("area_id", area_id),
|
|
179
|
+
"message": f"Successfully {operation}d area: {area_name}",
|
|
180
|
+
}
|
|
181
|
+
else:
|
|
182
|
+
error = result.get("error", {})
|
|
183
|
+
error_msg = error.get("message", str(error)) if isinstance(error, dict) else str(error)
|
|
184
|
+
error_response = {
|
|
185
|
+
"success": False,
|
|
186
|
+
"error": f"Failed to {operation} area: {error_msg}",
|
|
187
|
+
}
|
|
188
|
+
if name:
|
|
189
|
+
error_response["name"] = name
|
|
190
|
+
return error_response
|
|
191
|
+
|
|
192
|
+
except Exception as e:
|
|
193
|
+
logger.error(f"Error in ha_config_set_area: {e}")
|
|
194
|
+
error_response = {
|
|
195
|
+
"success": False,
|
|
196
|
+
"error": f"Failed to set area: {str(e)}",
|
|
197
|
+
"suggestions": [
|
|
198
|
+
"Check Home Assistant connection",
|
|
199
|
+
"For create: Verify the name is unique",
|
|
200
|
+
"For update: Verify the area_id exists using ha_config_list_areas()",
|
|
201
|
+
"If assigning to a floor, verify floor_id exists",
|
|
202
|
+
],
|
|
203
|
+
}
|
|
204
|
+
if name:
|
|
205
|
+
error_response["name"] = name
|
|
206
|
+
return error_response
|
|
207
|
+
|
|
208
|
+
@mcp.tool(annotations={"destructiveHint": True, "idempotentHint": True, "tags": ["area"], "title": "Remove Area"})
|
|
209
|
+
@log_tool_usage
|
|
210
|
+
async def ha_config_remove_area(
|
|
211
|
+
area_id: Annotated[
|
|
212
|
+
str,
|
|
213
|
+
Field(description="Area ID to delete (use ha_config_list_areas to find IDs)"),
|
|
214
|
+
],
|
|
215
|
+
) -> dict[str, Any]:
|
|
216
|
+
"""
|
|
217
|
+
Delete a Home Assistant area.
|
|
218
|
+
|
|
219
|
+
Entities and devices in the area are not deleted, just unassigned.
|
|
220
|
+
May break automations referencing this area.
|
|
221
|
+
"""
|
|
222
|
+
try:
|
|
223
|
+
message: dict[str, Any] = {
|
|
224
|
+
"type": "config/area_registry/delete",
|
|
225
|
+
"area_id": area_id,
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
result = await client.send_websocket_message(message)
|
|
229
|
+
|
|
230
|
+
if result.get("success"):
|
|
231
|
+
return {
|
|
232
|
+
"success": True,
|
|
233
|
+
"area_id": area_id,
|
|
234
|
+
"message": f"Successfully deleted area: {area_id}",
|
|
235
|
+
}
|
|
236
|
+
else:
|
|
237
|
+
error = result.get("error", {})
|
|
238
|
+
error_msg = error.get("message", str(error)) if isinstance(error, dict) else str(error)
|
|
239
|
+
return {
|
|
240
|
+
"success": False,
|
|
241
|
+
"error": f"Failed to delete area: {error_msg}",
|
|
242
|
+
"area_id": area_id,
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
except Exception as e:
|
|
246
|
+
logger.error(f"Error deleting area: {e}")
|
|
247
|
+
return {
|
|
248
|
+
"success": False,
|
|
249
|
+
"error": f"Failed to delete area: {str(e)}",
|
|
250
|
+
"area_id": area_id,
|
|
251
|
+
"suggestions": [
|
|
252
|
+
"Check Home Assistant connection",
|
|
253
|
+
"Verify the area_id exists using ha_config_list_areas()",
|
|
254
|
+
],
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
# ============================================================
|
|
258
|
+
# FLOOR TOOLS
|
|
259
|
+
# ============================================================
|
|
260
|
+
|
|
261
|
+
@mcp.tool(annotations={"idempotentHint": True, "readOnlyHint": True, "tags": ["floor"], "title": "List Floors"})
|
|
262
|
+
@log_tool_usage
|
|
263
|
+
async def ha_config_list_floors() -> dict[str, Any]:
|
|
264
|
+
"""
|
|
265
|
+
List all Home Assistant floors.
|
|
266
|
+
|
|
267
|
+
Returns floor ID, name, icon, level (0=ground, 1=first, -1=basement), and aliases.
|
|
268
|
+
"""
|
|
269
|
+
try:
|
|
270
|
+
message: dict[str, Any] = {
|
|
271
|
+
"type": "config/floor_registry/list",
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
result = await client.send_websocket_message(message)
|
|
275
|
+
|
|
276
|
+
if result.get("success"):
|
|
277
|
+
floors = result.get("result", [])
|
|
278
|
+
return {
|
|
279
|
+
"success": True,
|
|
280
|
+
"count": len(floors),
|
|
281
|
+
"floors": floors,
|
|
282
|
+
"message": f"Found {len(floors)} floor(s)",
|
|
283
|
+
}
|
|
284
|
+
else:
|
|
285
|
+
return {
|
|
286
|
+
"success": False,
|
|
287
|
+
"error": f"Failed to list floors: {result.get('error', 'Unknown error')}",
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
except Exception as e:
|
|
291
|
+
logger.error(f"Error listing floors: {e}")
|
|
292
|
+
return {
|
|
293
|
+
"success": False,
|
|
294
|
+
"error": f"Failed to list floors: {str(e)}",
|
|
295
|
+
"suggestions": [
|
|
296
|
+
"Check Home Assistant connection",
|
|
297
|
+
"Verify WebSocket connection is active",
|
|
298
|
+
],
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
@mcp.tool(annotations={"destructiveHint": True, "tags": ["floor"], "title": "Create or Update Floor"})
|
|
302
|
+
@log_tool_usage
|
|
303
|
+
async def ha_config_set_floor(
|
|
304
|
+
name: Annotated[
|
|
305
|
+
str | None,
|
|
306
|
+
Field(
|
|
307
|
+
description="Name for the floor (required for create, optional for update, e.g., 'Ground Floor', 'Basement')",
|
|
308
|
+
default=None,
|
|
309
|
+
),
|
|
310
|
+
] = None,
|
|
311
|
+
floor_id: Annotated[
|
|
312
|
+
str | None,
|
|
313
|
+
Field(
|
|
314
|
+
description="Floor ID to update (omit to create new floor, use ha_config_list_floors to find IDs)",
|
|
315
|
+
default=None,
|
|
316
|
+
),
|
|
317
|
+
] = None,
|
|
318
|
+
level: Annotated[
|
|
319
|
+
int | None,
|
|
320
|
+
Field(
|
|
321
|
+
description="Numeric level for ordering (0=ground, 1=first, -1=basement, etc.)",
|
|
322
|
+
default=None,
|
|
323
|
+
),
|
|
324
|
+
] = None,
|
|
325
|
+
icon: Annotated[
|
|
326
|
+
str | None,
|
|
327
|
+
Field(
|
|
328
|
+
description="Material Design Icon (e.g., 'mdi:home-floor-1', 'mdi:home-floor-b', empty string to remove)",
|
|
329
|
+
default=None,
|
|
330
|
+
),
|
|
331
|
+
] = None,
|
|
332
|
+
aliases: Annotated[
|
|
333
|
+
str | list[str] | None,
|
|
334
|
+
Field(
|
|
335
|
+
description="Alternative names for voice assistant recognition (e.g., ['downstairs', 'main level'], empty list to clear)",
|
|
336
|
+
default=None,
|
|
337
|
+
),
|
|
338
|
+
] = None,
|
|
339
|
+
) -> dict[str, Any]:
|
|
340
|
+
"""
|
|
341
|
+
Create or update a Home Assistant floor.
|
|
342
|
+
|
|
343
|
+
Provide name only to create a new floor. Provide floor_id to update existing.
|
|
344
|
+
Floors organize areas into vertical levels for building-wide control.
|
|
345
|
+
"""
|
|
346
|
+
try:
|
|
347
|
+
# Parse aliases if provided as string
|
|
348
|
+
try:
|
|
349
|
+
parsed_aliases = parse_string_list_param(aliases, "aliases")
|
|
350
|
+
except ValueError as e:
|
|
351
|
+
return {"success": False, "error": f"Invalid aliases parameter: {e}"}
|
|
352
|
+
|
|
353
|
+
# Determine if this is a create or update operation
|
|
354
|
+
if floor_id:
|
|
355
|
+
# UPDATE operation
|
|
356
|
+
message: dict[str, Any] = {
|
|
357
|
+
"type": "config/floor_registry/update",
|
|
358
|
+
"floor_id": floor_id,
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
# Only add fields that were explicitly provided
|
|
362
|
+
if name is not None:
|
|
363
|
+
message["name"] = name
|
|
364
|
+
if level is not None:
|
|
365
|
+
message["level"] = level
|
|
366
|
+
if icon is not None:
|
|
367
|
+
message["icon"] = icon if icon else None
|
|
368
|
+
if parsed_aliases is not None:
|
|
369
|
+
message["aliases"] = parsed_aliases
|
|
370
|
+
|
|
371
|
+
operation = "update"
|
|
372
|
+
else:
|
|
373
|
+
# CREATE operation - name is required
|
|
374
|
+
if not name:
|
|
375
|
+
return {
|
|
376
|
+
"success": False,
|
|
377
|
+
"error": "name is required when creating a new floor",
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
message: dict[str, Any] = {
|
|
381
|
+
"type": "config/floor_registry/create",
|
|
382
|
+
"name": name,
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if level is not None:
|
|
386
|
+
message["level"] = level
|
|
387
|
+
if icon:
|
|
388
|
+
message["icon"] = icon
|
|
389
|
+
if parsed_aliases:
|
|
390
|
+
message["aliases"] = parsed_aliases
|
|
391
|
+
|
|
392
|
+
operation = "create"
|
|
393
|
+
|
|
394
|
+
result = await client.send_websocket_message(message)
|
|
395
|
+
|
|
396
|
+
if result.get("success"):
|
|
397
|
+
floor_data = result.get("result", {})
|
|
398
|
+
floor_name = name or floor_data.get("name", floor_id)
|
|
399
|
+
return {
|
|
400
|
+
"success": True,
|
|
401
|
+
"floor": floor_data,
|
|
402
|
+
"floor_id": floor_data.get("floor_id", floor_id),
|
|
403
|
+
"message": f"Successfully {operation}d floor: {floor_name}",
|
|
404
|
+
}
|
|
405
|
+
else:
|
|
406
|
+
error = result.get("error", {})
|
|
407
|
+
error_msg = error.get("message", str(error)) if isinstance(error, dict) else str(error)
|
|
408
|
+
error_response = {
|
|
409
|
+
"success": False,
|
|
410
|
+
"error": f"Failed to {operation} floor: {error_msg}",
|
|
411
|
+
}
|
|
412
|
+
if name:
|
|
413
|
+
error_response["name"] = name
|
|
414
|
+
return error_response
|
|
415
|
+
|
|
416
|
+
except Exception as e:
|
|
417
|
+
logger.error(f"Error in ha_config_set_floor: {e}")
|
|
418
|
+
error_response = {
|
|
419
|
+
"success": False,
|
|
420
|
+
"error": f"Failed to set floor: {str(e)}",
|
|
421
|
+
"suggestions": [
|
|
422
|
+
"Check Home Assistant connection",
|
|
423
|
+
"For create: Verify the name is unique",
|
|
424
|
+
"For update: Verify the floor_id exists using ha_config_list_floors()",
|
|
425
|
+
],
|
|
426
|
+
}
|
|
427
|
+
if name:
|
|
428
|
+
error_response["name"] = name
|
|
429
|
+
return error_response
|
|
430
|
+
|
|
431
|
+
@mcp.tool(annotations={"destructiveHint": True, "idempotentHint": True, "tags": ["floor"], "title": "Remove Floor"})
|
|
432
|
+
@log_tool_usage
|
|
433
|
+
async def ha_config_remove_floor(
|
|
434
|
+
floor_id: Annotated[
|
|
435
|
+
str,
|
|
436
|
+
Field(description="Floor ID to delete (use ha_config_list_floors to find IDs)"),
|
|
437
|
+
],
|
|
438
|
+
) -> dict[str, Any]:
|
|
439
|
+
"""
|
|
440
|
+
Delete a Home Assistant floor.
|
|
441
|
+
|
|
442
|
+
Areas on this floor are not deleted, just unassigned.
|
|
443
|
+
May break automations referencing this floor.
|
|
444
|
+
"""
|
|
445
|
+
try:
|
|
446
|
+
message: dict[str, Any] = {
|
|
447
|
+
"type": "config/floor_registry/delete",
|
|
448
|
+
"floor_id": floor_id,
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
result = await client.send_websocket_message(message)
|
|
452
|
+
|
|
453
|
+
if result.get("success"):
|
|
454
|
+
return {
|
|
455
|
+
"success": True,
|
|
456
|
+
"floor_id": floor_id,
|
|
457
|
+
"message": f"Successfully deleted floor: {floor_id}",
|
|
458
|
+
}
|
|
459
|
+
else:
|
|
460
|
+
error = result.get("error", {})
|
|
461
|
+
error_msg = error.get("message", str(error)) if isinstance(error, dict) else str(error)
|
|
462
|
+
return {
|
|
463
|
+
"success": False,
|
|
464
|
+
"error": f"Failed to delete floor: {error_msg}",
|
|
465
|
+
"floor_id": floor_id,
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
except Exception as e:
|
|
469
|
+
logger.error(f"Error deleting floor: {e}")
|
|
470
|
+
return {
|
|
471
|
+
"success": False,
|
|
472
|
+
"error": f"Failed to delete floor: {str(e)}",
|
|
473
|
+
"floor_id": floor_id,
|
|
474
|
+
"suggestions": [
|
|
475
|
+
"Check Home Assistant connection",
|
|
476
|
+
"Verify the floor_id exists using ha_config_list_floors()",
|
|
477
|
+
],
|
|
478
|
+
}
|