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,925 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration management tools for Home Assistant helpers.
|
|
3
|
+
|
|
4
|
+
This module provides tools for listing, creating, updating, and removing
|
|
5
|
+
Home Assistant helper entities (input_button, input_boolean, input_select,
|
|
6
|
+
input_number, input_text, input_datetime, counter, timer, schedule).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import logging
|
|
11
|
+
from typing import Annotated, Any, Literal
|
|
12
|
+
|
|
13
|
+
from pydantic import Field
|
|
14
|
+
|
|
15
|
+
from .helpers import log_tool_usage
|
|
16
|
+
from .util_helpers import parse_string_list_param
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def register_config_helper_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
22
|
+
"""Register Home Assistant helper configuration tools."""
|
|
23
|
+
|
|
24
|
+
@mcp.tool(
|
|
25
|
+
annotations={
|
|
26
|
+
"idempotentHint": True,
|
|
27
|
+
"readOnlyHint": True,
|
|
28
|
+
"tags": ["helper"],
|
|
29
|
+
"title": "List Helpers",
|
|
30
|
+
}
|
|
31
|
+
)
|
|
32
|
+
@log_tool_usage
|
|
33
|
+
async def ha_config_list_helpers(
|
|
34
|
+
helper_type: Annotated[
|
|
35
|
+
Literal[
|
|
36
|
+
"input_button",
|
|
37
|
+
"input_boolean",
|
|
38
|
+
"input_select",
|
|
39
|
+
"input_number",
|
|
40
|
+
"input_text",
|
|
41
|
+
"input_datetime",
|
|
42
|
+
"counter",
|
|
43
|
+
"timer",
|
|
44
|
+
"schedule",
|
|
45
|
+
"zone",
|
|
46
|
+
"person",
|
|
47
|
+
"tag",
|
|
48
|
+
],
|
|
49
|
+
Field(description="Type of helper entity to list"),
|
|
50
|
+
],
|
|
51
|
+
) -> dict[str, Any]:
|
|
52
|
+
"""
|
|
53
|
+
List all Home Assistant helpers of a specific type with their configurations.
|
|
54
|
+
|
|
55
|
+
Returns complete configuration for all helpers of the specified type including:
|
|
56
|
+
- ID, name, icon
|
|
57
|
+
- Type-specific settings (min/max for input_number, options for input_select, etc.)
|
|
58
|
+
- Area and label assignments
|
|
59
|
+
|
|
60
|
+
SUPPORTED HELPER TYPES:
|
|
61
|
+
- input_button: Virtual buttons for triggering automations
|
|
62
|
+
- input_boolean: Toggle switches/checkboxes
|
|
63
|
+
- input_select: Dropdown selection lists
|
|
64
|
+
- input_number: Numeric sliders/input boxes
|
|
65
|
+
- input_text: Text input fields
|
|
66
|
+
- input_datetime: Date/time pickers
|
|
67
|
+
- counter: Counters with increment/decrement/reset
|
|
68
|
+
- timer: Countdown timers with start/pause/cancel
|
|
69
|
+
- schedule: Weekly schedules with time ranges (on/off per day)
|
|
70
|
+
- zone: Geographical zones for presence detection
|
|
71
|
+
- person: Person entities linked to device trackers
|
|
72
|
+
- tag: NFC/QR tags for automation triggers
|
|
73
|
+
|
|
74
|
+
EXAMPLES:
|
|
75
|
+
- List all number helpers: ha_config_list_helpers("input_number")
|
|
76
|
+
- List all counters: ha_config_list_helpers("counter")
|
|
77
|
+
- List all zones: ha_config_list_helpers("zone")
|
|
78
|
+
- List all persons: ha_config_list_helpers("person")
|
|
79
|
+
- List all tags: ha_config_list_helpers("tag")
|
|
80
|
+
|
|
81
|
+
**NOTE:** This only returns storage-based helpers (created via UI/API), not YAML-defined helpers.
|
|
82
|
+
|
|
83
|
+
For detailed helper documentation, use: ha_get_domain_docs("input_number"), etc.
|
|
84
|
+
"""
|
|
85
|
+
try:
|
|
86
|
+
# Use the websocket list endpoint for the helper type
|
|
87
|
+
message: dict[str, Any] = {
|
|
88
|
+
"type": f"{helper_type}/list",
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
result = await client.send_websocket_message(message)
|
|
92
|
+
|
|
93
|
+
if result.get("success"):
|
|
94
|
+
items = result.get("result", [])
|
|
95
|
+
return {
|
|
96
|
+
"success": True,
|
|
97
|
+
"helper_type": helper_type,
|
|
98
|
+
"count": len(items),
|
|
99
|
+
"helpers": items,
|
|
100
|
+
"message": f"Found {len(items)} {helper_type} helper(s)",
|
|
101
|
+
}
|
|
102
|
+
else:
|
|
103
|
+
return {
|
|
104
|
+
"success": False,
|
|
105
|
+
"error": f"Failed to list helpers: {result.get('error', 'Unknown error')}",
|
|
106
|
+
"helper_type": helper_type,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
except Exception as e:
|
|
110
|
+
logger.error(f"Error listing helpers: {e}")
|
|
111
|
+
return {
|
|
112
|
+
"success": False,
|
|
113
|
+
"error": f"Failed to list {helper_type} helpers: {str(e)}",
|
|
114
|
+
"helper_type": helper_type,
|
|
115
|
+
"suggestions": [
|
|
116
|
+
"Check Home Assistant connection",
|
|
117
|
+
"Verify WebSocket connection is active",
|
|
118
|
+
"Use ha_search_entities(domain_filter='input_*') as alternative",
|
|
119
|
+
],
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@mcp.tool(
|
|
123
|
+
annotations={
|
|
124
|
+
"destructiveHint": True,
|
|
125
|
+
"tags": ["helper"],
|
|
126
|
+
"title": "Create or Update Helper",
|
|
127
|
+
}
|
|
128
|
+
)
|
|
129
|
+
@log_tool_usage
|
|
130
|
+
async def ha_config_set_helper(
|
|
131
|
+
helper_type: Annotated[
|
|
132
|
+
Literal[
|
|
133
|
+
"input_button",
|
|
134
|
+
"input_boolean",
|
|
135
|
+
"input_select",
|
|
136
|
+
"input_number",
|
|
137
|
+
"input_text",
|
|
138
|
+
"input_datetime",
|
|
139
|
+
"counter",
|
|
140
|
+
"timer",
|
|
141
|
+
"schedule",
|
|
142
|
+
"zone",
|
|
143
|
+
"person",
|
|
144
|
+
"tag",
|
|
145
|
+
],
|
|
146
|
+
Field(description="Type of helper entity to create or update"),
|
|
147
|
+
],
|
|
148
|
+
name: Annotated[str, Field(description="Display name for the helper")],
|
|
149
|
+
helper_id: Annotated[
|
|
150
|
+
str | None,
|
|
151
|
+
Field(
|
|
152
|
+
description="Helper ID for updates (e.g., 'my_button' or 'input_button.my_button'). If not provided, creates a new helper.",
|
|
153
|
+
default=None,
|
|
154
|
+
),
|
|
155
|
+
] = None,
|
|
156
|
+
icon: Annotated[
|
|
157
|
+
str | None,
|
|
158
|
+
Field(
|
|
159
|
+
description="Material Design Icon (e.g., 'mdi:bell', 'mdi:toggle-switch')",
|
|
160
|
+
default=None,
|
|
161
|
+
),
|
|
162
|
+
] = None,
|
|
163
|
+
area_id: Annotated[
|
|
164
|
+
str | None,
|
|
165
|
+
Field(description="Area/room ID to assign the helper to", default=None),
|
|
166
|
+
] = None,
|
|
167
|
+
labels: Annotated[
|
|
168
|
+
str | list[str] | None,
|
|
169
|
+
Field(description="Labels to categorize the helper", default=None),
|
|
170
|
+
] = None,
|
|
171
|
+
min_value: Annotated[
|
|
172
|
+
float | None,
|
|
173
|
+
Field(
|
|
174
|
+
description="Minimum value (input_number/counter) or minimum length (input_text)",
|
|
175
|
+
default=None,
|
|
176
|
+
),
|
|
177
|
+
] = None,
|
|
178
|
+
max_value: Annotated[
|
|
179
|
+
float | None,
|
|
180
|
+
Field(
|
|
181
|
+
description="Maximum value (input_number/counter) or maximum length (input_text)",
|
|
182
|
+
default=None,
|
|
183
|
+
),
|
|
184
|
+
] = None,
|
|
185
|
+
step: Annotated[
|
|
186
|
+
float | None,
|
|
187
|
+
Field(
|
|
188
|
+
description="Step/increment value for input_number or counter",
|
|
189
|
+
default=None,
|
|
190
|
+
),
|
|
191
|
+
] = None,
|
|
192
|
+
unit_of_measurement: Annotated[
|
|
193
|
+
str | None,
|
|
194
|
+
Field(
|
|
195
|
+
description="Unit of measurement for input_number (e.g., '°C', '%', 'W')",
|
|
196
|
+
default=None,
|
|
197
|
+
),
|
|
198
|
+
] = None,
|
|
199
|
+
options: Annotated[
|
|
200
|
+
str | list[str] | None,
|
|
201
|
+
Field(
|
|
202
|
+
description="List of options for input_select (required for input_select)",
|
|
203
|
+
default=None,
|
|
204
|
+
),
|
|
205
|
+
] = None,
|
|
206
|
+
initial: Annotated[
|
|
207
|
+
str | int | None,
|
|
208
|
+
Field(
|
|
209
|
+
description="Initial value for the helper (input_select, input_text, input_boolean, input_datetime, counter)",
|
|
210
|
+
default=None,
|
|
211
|
+
),
|
|
212
|
+
] = None,
|
|
213
|
+
mode: Annotated[
|
|
214
|
+
str | None,
|
|
215
|
+
Field(
|
|
216
|
+
description="Display mode: 'box'/'slider' for input_number, 'text'/'password' for input_text",
|
|
217
|
+
default=None,
|
|
218
|
+
),
|
|
219
|
+
] = None,
|
|
220
|
+
has_date: Annotated[
|
|
221
|
+
bool | None,
|
|
222
|
+
Field(
|
|
223
|
+
description="Include date component for input_datetime", default=None
|
|
224
|
+
),
|
|
225
|
+
] = None,
|
|
226
|
+
has_time: Annotated[
|
|
227
|
+
bool | None,
|
|
228
|
+
Field(
|
|
229
|
+
description="Include time component for input_datetime", default=None
|
|
230
|
+
),
|
|
231
|
+
] = None,
|
|
232
|
+
restore: Annotated[
|
|
233
|
+
bool | None,
|
|
234
|
+
Field(
|
|
235
|
+
description="Restore state after restart (counter, timer). Defaults to True for counter, False for timer",
|
|
236
|
+
default=None,
|
|
237
|
+
),
|
|
238
|
+
] = None,
|
|
239
|
+
duration: Annotated[
|
|
240
|
+
str | None,
|
|
241
|
+
Field(
|
|
242
|
+
description="Default duration for timer in format 'HH:MM:SS' or seconds (e.g., '0:05:00' for 5 minutes)",
|
|
243
|
+
default=None,
|
|
244
|
+
),
|
|
245
|
+
] = None,
|
|
246
|
+
monday: Annotated[
|
|
247
|
+
list[dict[str, str]] | None,
|
|
248
|
+
Field(
|
|
249
|
+
description="Schedule time ranges for Monday. List of {'from': 'HH:MM', 'to': 'HH:MM'} dicts",
|
|
250
|
+
default=None,
|
|
251
|
+
),
|
|
252
|
+
] = None,
|
|
253
|
+
tuesday: Annotated[
|
|
254
|
+
list[dict[str, str]] | None,
|
|
255
|
+
Field(
|
|
256
|
+
description="Schedule time ranges for Tuesday. List of {'from': 'HH:MM', 'to': 'HH:MM'} dicts",
|
|
257
|
+
default=None,
|
|
258
|
+
),
|
|
259
|
+
] = None,
|
|
260
|
+
wednesday: Annotated[
|
|
261
|
+
list[dict[str, str]] | None,
|
|
262
|
+
Field(
|
|
263
|
+
description="Schedule time ranges for Wednesday. List of {'from': 'HH:MM', 'to': 'HH:MM'} dicts",
|
|
264
|
+
default=None,
|
|
265
|
+
),
|
|
266
|
+
] = None,
|
|
267
|
+
thursday: Annotated[
|
|
268
|
+
list[dict[str, str]] | None,
|
|
269
|
+
Field(
|
|
270
|
+
description="Schedule time ranges for Thursday. List of {'from': 'HH:MM', 'to': 'HH:MM'} dicts",
|
|
271
|
+
default=None,
|
|
272
|
+
),
|
|
273
|
+
] = None,
|
|
274
|
+
friday: Annotated[
|
|
275
|
+
list[dict[str, str]] | None,
|
|
276
|
+
Field(
|
|
277
|
+
description="Schedule time ranges for Friday. List of {'from': 'HH:MM', 'to': 'HH:MM'} dicts",
|
|
278
|
+
default=None,
|
|
279
|
+
),
|
|
280
|
+
] = None,
|
|
281
|
+
saturday: Annotated[
|
|
282
|
+
list[dict[str, str]] | None,
|
|
283
|
+
Field(
|
|
284
|
+
description="Schedule time ranges for Saturday. List of {'from': 'HH:MM', 'to': 'HH:MM'} dicts",
|
|
285
|
+
default=None,
|
|
286
|
+
),
|
|
287
|
+
] = None,
|
|
288
|
+
sunday: Annotated[
|
|
289
|
+
list[dict[str, str]] | None,
|
|
290
|
+
Field(
|
|
291
|
+
description="Schedule time ranges for Sunday. List of {'from': 'HH:MM', 'to': 'HH:MM'} dicts",
|
|
292
|
+
default=None,
|
|
293
|
+
),
|
|
294
|
+
] = None,
|
|
295
|
+
latitude: Annotated[
|
|
296
|
+
float | None,
|
|
297
|
+
Field(
|
|
298
|
+
description="Latitude for zone (required for zone)",
|
|
299
|
+
default=None,
|
|
300
|
+
),
|
|
301
|
+
] = None,
|
|
302
|
+
longitude: Annotated[
|
|
303
|
+
float | None,
|
|
304
|
+
Field(
|
|
305
|
+
description="Longitude for zone (required for zone)",
|
|
306
|
+
default=None,
|
|
307
|
+
),
|
|
308
|
+
] = None,
|
|
309
|
+
radius: Annotated[
|
|
310
|
+
float | None,
|
|
311
|
+
Field(
|
|
312
|
+
description="Radius in meters for zone (default: 100)",
|
|
313
|
+
default=None,
|
|
314
|
+
),
|
|
315
|
+
] = None,
|
|
316
|
+
passive: Annotated[
|
|
317
|
+
bool | None,
|
|
318
|
+
Field(
|
|
319
|
+
description="Passive zone (won't trigger state changes for person entities)",
|
|
320
|
+
default=None,
|
|
321
|
+
),
|
|
322
|
+
] = None,
|
|
323
|
+
user_id: Annotated[
|
|
324
|
+
str | None,
|
|
325
|
+
Field(
|
|
326
|
+
description="User ID to link to person entity",
|
|
327
|
+
default=None,
|
|
328
|
+
),
|
|
329
|
+
] = None,
|
|
330
|
+
device_trackers: Annotated[
|
|
331
|
+
list[str] | None,
|
|
332
|
+
Field(
|
|
333
|
+
description="List of device_tracker entity IDs for person",
|
|
334
|
+
default=None,
|
|
335
|
+
),
|
|
336
|
+
] = None,
|
|
337
|
+
picture: Annotated[
|
|
338
|
+
str | None,
|
|
339
|
+
Field(
|
|
340
|
+
description="Picture URL for person entity",
|
|
341
|
+
default=None,
|
|
342
|
+
),
|
|
343
|
+
] = None,
|
|
344
|
+
tag_id: Annotated[
|
|
345
|
+
str | None,
|
|
346
|
+
Field(
|
|
347
|
+
description="Tag ID for tag (auto-generated if not provided)",
|
|
348
|
+
default=None,
|
|
349
|
+
),
|
|
350
|
+
] = None,
|
|
351
|
+
description: Annotated[
|
|
352
|
+
str | None,
|
|
353
|
+
Field(
|
|
354
|
+
description="Description for tag",
|
|
355
|
+
default=None,
|
|
356
|
+
),
|
|
357
|
+
] = None,
|
|
358
|
+
) -> dict[str, Any]:
|
|
359
|
+
"""
|
|
360
|
+
Create or update Home Assistant helper entities.
|
|
361
|
+
|
|
362
|
+
Creates new helper if helper_id is omitted, updates existing if helper_id is provided.
|
|
363
|
+
Parameters are validated by Home Assistant - errors return clear messages.
|
|
364
|
+
|
|
365
|
+
QUICK EXAMPLES:
|
|
366
|
+
- ha_config_set_helper("input_boolean", "My Switch", icon="mdi:toggle-switch")
|
|
367
|
+
- ha_config_set_helper("counter", "My Counter", initial=0, step=1)
|
|
368
|
+
- ha_config_set_helper("timer", "Laundry", duration="0:45:00")
|
|
369
|
+
- ha_config_set_helper("zone", "Office", latitude=37.77, longitude=-122.41, radius=100)
|
|
370
|
+
- ha_config_set_helper("schedule", "Work", monday=[{"from": "09:00", "to": "17:00"}])
|
|
371
|
+
|
|
372
|
+
PREFER BUILT-IN HELPERS OVER TEMPLATE SENSORS:
|
|
373
|
+
Before creating a template sensor, check if a built-in helper/integration exists:
|
|
374
|
+
- Use `min_max` integration (type: mean/min/max/sum) instead of template for combining sensors
|
|
375
|
+
- Use `group` instead of template binary sensor for any/all logic
|
|
376
|
+
- Use `counter` instead of template with math for counting
|
|
377
|
+
- Use `input_number` instead of template for storing values
|
|
378
|
+
- Use `schedule` instead of template with weekday checks
|
|
379
|
+
|
|
380
|
+
For detailed parameter info: ha_get_domain_docs("counter"), ha_get_domain_docs("zone"), etc.
|
|
381
|
+
"""
|
|
382
|
+
try:
|
|
383
|
+
# Parse JSON list parameters if provided as strings
|
|
384
|
+
try:
|
|
385
|
+
labels = parse_string_list_param(labels, "labels")
|
|
386
|
+
options = parse_string_list_param(options, "options")
|
|
387
|
+
except ValueError as e:
|
|
388
|
+
return {"success": False, "error": f"Invalid list parameter: {e}"}
|
|
389
|
+
|
|
390
|
+
# Determine if this is a create or update based on helper_id
|
|
391
|
+
action = "update" if helper_id else "create"
|
|
392
|
+
|
|
393
|
+
if action == "create":
|
|
394
|
+
if not name:
|
|
395
|
+
return {
|
|
396
|
+
"success": False,
|
|
397
|
+
"error": "name is required for create action",
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
# Build create message based on helper type
|
|
401
|
+
message: dict[str, Any] = {
|
|
402
|
+
"type": f"{helper_type}/create",
|
|
403
|
+
"name": name,
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
# Icon supported by most helpers except person and tag
|
|
407
|
+
if icon and helper_type not in ("person", "tag"):
|
|
408
|
+
message["icon"] = icon
|
|
409
|
+
|
|
410
|
+
# Type-specific parameters
|
|
411
|
+
if helper_type == "input_select":
|
|
412
|
+
if not options:
|
|
413
|
+
return {
|
|
414
|
+
"success": False,
|
|
415
|
+
"error": "options list is required for input_select",
|
|
416
|
+
}
|
|
417
|
+
if not isinstance(options, list) or len(options) == 0:
|
|
418
|
+
return {
|
|
419
|
+
"success": False,
|
|
420
|
+
"error": "options must be a non-empty list for input_select",
|
|
421
|
+
}
|
|
422
|
+
message["options"] = options
|
|
423
|
+
if initial and initial in options:
|
|
424
|
+
message["initial"] = initial
|
|
425
|
+
|
|
426
|
+
elif helper_type == "input_number":
|
|
427
|
+
# Validate min_value/max_value range
|
|
428
|
+
if (
|
|
429
|
+
min_value is not None
|
|
430
|
+
and max_value is not None
|
|
431
|
+
and min_value > max_value
|
|
432
|
+
):
|
|
433
|
+
return {
|
|
434
|
+
"success": False,
|
|
435
|
+
"error": f"Minimum value ({min_value}) cannot be greater than maximum value ({max_value})",
|
|
436
|
+
"min_value": min_value,
|
|
437
|
+
"max_value": max_value,
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if min_value is not None:
|
|
441
|
+
message["min"] = min_value
|
|
442
|
+
if max_value is not None:
|
|
443
|
+
message["max"] = max_value
|
|
444
|
+
if step is not None:
|
|
445
|
+
message["step"] = step
|
|
446
|
+
if unit_of_measurement:
|
|
447
|
+
message["unit_of_measurement"] = unit_of_measurement
|
|
448
|
+
if mode in ["box", "slider"]:
|
|
449
|
+
message["mode"] = mode
|
|
450
|
+
|
|
451
|
+
elif helper_type == "input_text":
|
|
452
|
+
if min_value is not None:
|
|
453
|
+
message["min"] = int(min_value)
|
|
454
|
+
if max_value is not None:
|
|
455
|
+
message["max"] = int(max_value)
|
|
456
|
+
if mode in ["text", "password"]:
|
|
457
|
+
message["mode"] = mode
|
|
458
|
+
if initial:
|
|
459
|
+
message["initial"] = initial
|
|
460
|
+
|
|
461
|
+
elif helper_type == "input_boolean":
|
|
462
|
+
if initial is not None:
|
|
463
|
+
initial_str = str(initial).lower()
|
|
464
|
+
message["initial"] = initial_str in [
|
|
465
|
+
"true",
|
|
466
|
+
"on",
|
|
467
|
+
"yes",
|
|
468
|
+
"1",
|
|
469
|
+
]
|
|
470
|
+
|
|
471
|
+
elif helper_type == "input_datetime":
|
|
472
|
+
# At least one of has_date or has_time must be True
|
|
473
|
+
if has_date is None and has_time is None:
|
|
474
|
+
# Default to both if not specified
|
|
475
|
+
message["has_date"] = True
|
|
476
|
+
message["has_time"] = True
|
|
477
|
+
elif has_date is None:
|
|
478
|
+
message["has_date"] = False
|
|
479
|
+
message["has_time"] = has_time
|
|
480
|
+
elif has_time is None:
|
|
481
|
+
message["has_date"] = has_date
|
|
482
|
+
message["has_time"] = False
|
|
483
|
+
else:
|
|
484
|
+
message["has_date"] = has_date
|
|
485
|
+
message["has_time"] = has_time
|
|
486
|
+
|
|
487
|
+
# Validate that at least one is True
|
|
488
|
+
if not message["has_date"] and not message["has_time"]:
|
|
489
|
+
return {
|
|
490
|
+
"success": False,
|
|
491
|
+
"error": "At least one of has_date or has_time must be True for input_datetime",
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if initial:
|
|
495
|
+
message["initial"] = initial
|
|
496
|
+
|
|
497
|
+
elif helper_type == "counter":
|
|
498
|
+
# Counter parameters: initial, minimum, maximum, step, restore
|
|
499
|
+
if initial is not None:
|
|
500
|
+
message["initial"] = (
|
|
501
|
+
int(initial) if isinstance(initial, str) else initial
|
|
502
|
+
)
|
|
503
|
+
if min_value is not None:
|
|
504
|
+
message["minimum"] = int(min_value)
|
|
505
|
+
if max_value is not None:
|
|
506
|
+
message["maximum"] = int(max_value)
|
|
507
|
+
if step is not None:
|
|
508
|
+
message["step"] = int(step)
|
|
509
|
+
if restore is not None:
|
|
510
|
+
message["restore"] = restore
|
|
511
|
+
|
|
512
|
+
elif helper_type == "timer":
|
|
513
|
+
# Timer parameters: duration, restore
|
|
514
|
+
if duration:
|
|
515
|
+
message["duration"] = duration
|
|
516
|
+
if restore is not None:
|
|
517
|
+
message["restore"] = restore
|
|
518
|
+
|
|
519
|
+
elif helper_type == "schedule":
|
|
520
|
+
# Schedule parameters: monday-sunday with time ranges
|
|
521
|
+
# Each day is a list of {"from": "HH:MM:SS", "to": "HH:MM:SS"}
|
|
522
|
+
day_params = {
|
|
523
|
+
"monday": monday,
|
|
524
|
+
"tuesday": tuesday,
|
|
525
|
+
"wednesday": wednesday,
|
|
526
|
+
"thursday": thursday,
|
|
527
|
+
"friday": friday,
|
|
528
|
+
"saturday": saturday,
|
|
529
|
+
"sunday": sunday,
|
|
530
|
+
}
|
|
531
|
+
for day_name, day_schedule in day_params.items():
|
|
532
|
+
if day_schedule is not None:
|
|
533
|
+
# Ensure time format has seconds
|
|
534
|
+
formatted_ranges = []
|
|
535
|
+
for time_range in day_schedule:
|
|
536
|
+
formatted_range = {}
|
|
537
|
+
for key in ["from", "to"]:
|
|
538
|
+
if key in time_range:
|
|
539
|
+
time_val = time_range[key]
|
|
540
|
+
# Add seconds if not present
|
|
541
|
+
if time_val.count(":") == 1:
|
|
542
|
+
time_val = f"{time_val}:00"
|
|
543
|
+
formatted_range[key] = time_val
|
|
544
|
+
formatted_ranges.append(formatted_range)
|
|
545
|
+
message[day_name] = formatted_ranges
|
|
546
|
+
|
|
547
|
+
elif helper_type == "zone":
|
|
548
|
+
# Zone parameters - HA validates required fields (latitude, longitude)
|
|
549
|
+
if latitude is not None:
|
|
550
|
+
message["latitude"] = latitude
|
|
551
|
+
if longitude is not None:
|
|
552
|
+
message["longitude"] = longitude
|
|
553
|
+
if radius is not None:
|
|
554
|
+
message["radius"] = radius
|
|
555
|
+
if passive is not None:
|
|
556
|
+
message["passive"] = passive
|
|
557
|
+
|
|
558
|
+
elif helper_type == "person":
|
|
559
|
+
# Person parameters: user_id, device_trackers, picture
|
|
560
|
+
if user_id:
|
|
561
|
+
message["user_id"] = user_id
|
|
562
|
+
if device_trackers:
|
|
563
|
+
message["device_trackers"] = device_trackers
|
|
564
|
+
if picture:
|
|
565
|
+
message["picture"] = picture
|
|
566
|
+
|
|
567
|
+
elif helper_type == "tag":
|
|
568
|
+
# Tag parameters: tag_id, description
|
|
569
|
+
# Note: name goes into entity registry, not tag storage
|
|
570
|
+
if tag_id:
|
|
571
|
+
message["tag_id"] = tag_id
|
|
572
|
+
if description:
|
|
573
|
+
message["description"] = description
|
|
574
|
+
|
|
575
|
+
result = await client.send_websocket_message(message)
|
|
576
|
+
|
|
577
|
+
if result.get("success"):
|
|
578
|
+
helper_data = result.get("result", {})
|
|
579
|
+
entity_id = helper_data.get("entity_id")
|
|
580
|
+
|
|
581
|
+
# Wait for entity to be properly registered before proceeding
|
|
582
|
+
if entity_id:
|
|
583
|
+
logger.debug(f"Waiting for {entity_id} to be registered...")
|
|
584
|
+
# Give the entity a moment to register in the system
|
|
585
|
+
await asyncio.sleep(0.2)
|
|
586
|
+
|
|
587
|
+
# Verify the entity is accessible via state API
|
|
588
|
+
max_verification_attempts = 5
|
|
589
|
+
for attempt in range(max_verification_attempts):
|
|
590
|
+
try:
|
|
591
|
+
state_check = await client.get_state(entity_id)
|
|
592
|
+
if state_check:
|
|
593
|
+
logger.debug(
|
|
594
|
+
f"Entity {entity_id} verified via state API"
|
|
595
|
+
)
|
|
596
|
+
break
|
|
597
|
+
except Exception:
|
|
598
|
+
pass
|
|
599
|
+
|
|
600
|
+
if attempt < max_verification_attempts - 1:
|
|
601
|
+
wait_time = 0.1 * (
|
|
602
|
+
attempt + 1
|
|
603
|
+
) # 0.1s, 0.2s, 0.3s, 0.4s
|
|
604
|
+
logger.debug(
|
|
605
|
+
f"Entity {entity_id} not yet accessible, waiting {wait_time}s..."
|
|
606
|
+
)
|
|
607
|
+
await asyncio.sleep(wait_time)
|
|
608
|
+
|
|
609
|
+
# Update entity registry if area_id or labels specified
|
|
610
|
+
if (area_id or labels) and entity_id:
|
|
611
|
+
update_message: dict[str, Any] = {
|
|
612
|
+
"type": "config/entity_registry/update",
|
|
613
|
+
"entity_id": entity_id,
|
|
614
|
+
}
|
|
615
|
+
if area_id:
|
|
616
|
+
update_message["area_id"] = area_id
|
|
617
|
+
if labels:
|
|
618
|
+
update_message["labels"] = labels
|
|
619
|
+
|
|
620
|
+
update_result = await client.send_websocket_message(
|
|
621
|
+
update_message
|
|
622
|
+
)
|
|
623
|
+
if update_result.get("success"):
|
|
624
|
+
helper_data["area_id"] = area_id
|
|
625
|
+
helper_data["labels"] = labels
|
|
626
|
+
|
|
627
|
+
return {
|
|
628
|
+
"success": True,
|
|
629
|
+
"action": "create",
|
|
630
|
+
"helper_type": helper_type,
|
|
631
|
+
"helper_data": helper_data,
|
|
632
|
+
"entity_id": entity_id,
|
|
633
|
+
"message": f"Successfully created {helper_type}: {name}",
|
|
634
|
+
}
|
|
635
|
+
else:
|
|
636
|
+
return {
|
|
637
|
+
"success": False,
|
|
638
|
+
"error": f"Failed to create helper: {result.get('error', 'Unknown error')}",
|
|
639
|
+
"helper_type": helper_type,
|
|
640
|
+
"name": name,
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
elif action == "update":
|
|
644
|
+
if not helper_id:
|
|
645
|
+
return {
|
|
646
|
+
"success": False,
|
|
647
|
+
"error": "helper_id is required for update action",
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
# For updates, we primarily use entity registry update
|
|
651
|
+
entity_id = (
|
|
652
|
+
helper_id
|
|
653
|
+
if helper_id.startswith(helper_type)
|
|
654
|
+
else f"{helper_type}.{helper_id}"
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
update_msg: dict[str, Any] = {
|
|
658
|
+
"type": "config/entity_registry/update",
|
|
659
|
+
"entity_id": entity_id,
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if name:
|
|
663
|
+
update_msg["name"] = name
|
|
664
|
+
if icon:
|
|
665
|
+
update_msg["icon"] = icon
|
|
666
|
+
if area_id:
|
|
667
|
+
update_msg["area_id"] = area_id
|
|
668
|
+
if labels:
|
|
669
|
+
update_msg["labels"] = labels
|
|
670
|
+
|
|
671
|
+
result = await client.send_websocket_message(update_msg)
|
|
672
|
+
|
|
673
|
+
if result.get("success"):
|
|
674
|
+
entity_data = result.get("result", {}).get("entity_entry", {})
|
|
675
|
+
return {
|
|
676
|
+
"success": True,
|
|
677
|
+
"action": "update",
|
|
678
|
+
"helper_type": helper_type,
|
|
679
|
+
"entity_id": entity_id,
|
|
680
|
+
"updated_data": entity_data,
|
|
681
|
+
"message": f"Successfully updated {helper_type}: {entity_id}",
|
|
682
|
+
}
|
|
683
|
+
else:
|
|
684
|
+
return {
|
|
685
|
+
"success": False,
|
|
686
|
+
"error": f"Failed to update helper: {result.get('error', 'Unknown error')}",
|
|
687
|
+
"entity_id": entity_id,
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
# This should never be reached since action is either "create" or "update"
|
|
691
|
+
return {
|
|
692
|
+
"success": False,
|
|
693
|
+
"error": f"Unexpected action: {action}",
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
except Exception as e:
|
|
697
|
+
return {
|
|
698
|
+
"success": False,
|
|
699
|
+
"error": f"Helper management failed: {str(e)}",
|
|
700
|
+
"action": action,
|
|
701
|
+
"helper_type": helper_type,
|
|
702
|
+
"suggestions": [
|
|
703
|
+
"Check Home Assistant connection",
|
|
704
|
+
"Verify helper_id exists for update operations",
|
|
705
|
+
"Ensure required parameters are provided for the helper type",
|
|
706
|
+
],
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
@mcp.tool(
|
|
710
|
+
annotations={
|
|
711
|
+
"destructiveHint": True,
|
|
712
|
+
"idempotentHint": True,
|
|
713
|
+
"tags": ["helper"],
|
|
714
|
+
"title": "Remove Helper",
|
|
715
|
+
}
|
|
716
|
+
)
|
|
717
|
+
@log_tool_usage
|
|
718
|
+
async def ha_config_remove_helper(
|
|
719
|
+
helper_type: Annotated[
|
|
720
|
+
Literal[
|
|
721
|
+
"input_button",
|
|
722
|
+
"input_boolean",
|
|
723
|
+
"input_select",
|
|
724
|
+
"input_number",
|
|
725
|
+
"input_text",
|
|
726
|
+
"input_datetime",
|
|
727
|
+
"counter",
|
|
728
|
+
"timer",
|
|
729
|
+
"schedule",
|
|
730
|
+
"zone",
|
|
731
|
+
"person",
|
|
732
|
+
"tag",
|
|
733
|
+
],
|
|
734
|
+
Field(description="Type of helper entity to delete"),
|
|
735
|
+
],
|
|
736
|
+
helper_id: Annotated[
|
|
737
|
+
str,
|
|
738
|
+
Field(
|
|
739
|
+
description="Helper ID to delete (e.g., 'my_button' or 'input_button.my_button')"
|
|
740
|
+
),
|
|
741
|
+
],
|
|
742
|
+
) -> dict[str, Any]:
|
|
743
|
+
"""
|
|
744
|
+
Delete a Home Assistant helper entity.
|
|
745
|
+
|
|
746
|
+
SUPPORTED HELPER TYPES:
|
|
747
|
+
- input_button, input_boolean, input_select, input_number, input_text, input_datetime
|
|
748
|
+
- counter, timer, schedule, zone, person, tag
|
|
749
|
+
|
|
750
|
+
EXAMPLES:
|
|
751
|
+
- Delete button: ha_config_remove_helper("input_button", "my_button")
|
|
752
|
+
- Delete counter: ha_config_remove_helper("counter", "my_counter")
|
|
753
|
+
- Delete timer: ha_config_remove_helper("timer", "my_timer")
|
|
754
|
+
- Delete schedule: ha_config_remove_helper("schedule", "work_hours")
|
|
755
|
+
|
|
756
|
+
**WARNING:** Deleting a helper that is used by automations or scripts may cause those automations/scripts to fail.
|
|
757
|
+
Use ha_search_entities() to verify the helper exists before attempting to delete it.
|
|
758
|
+
"""
|
|
759
|
+
try:
|
|
760
|
+
# Convert helper_id to full entity_id if needed
|
|
761
|
+
entity_id = (
|
|
762
|
+
helper_id
|
|
763
|
+
if helper_id.startswith(helper_type)
|
|
764
|
+
else f"{helper_type}.{helper_id}"
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
# Try to get unique_id with retry logic to handle race conditions
|
|
768
|
+
unique_id = None
|
|
769
|
+
registry_result = None
|
|
770
|
+
max_retries = 3
|
|
771
|
+
|
|
772
|
+
for attempt in range(max_retries):
|
|
773
|
+
logger.info(
|
|
774
|
+
f"Getting entity registry for: {entity_id} (attempt {attempt + 1}/{max_retries})"
|
|
775
|
+
)
|
|
776
|
+
|
|
777
|
+
# Check if entity exists via state API first (faster check)
|
|
778
|
+
try:
|
|
779
|
+
state_check = await client.get_state(entity_id)
|
|
780
|
+
if not state_check:
|
|
781
|
+
# Entity doesn't exist in state, wait a bit for registration
|
|
782
|
+
if attempt < max_retries - 1:
|
|
783
|
+
wait_time = 0.5 * (
|
|
784
|
+
2**attempt
|
|
785
|
+
) # Exponential backoff: 0.5s, 1s, 2s
|
|
786
|
+
logger.debug(
|
|
787
|
+
f"Entity {entity_id} not found in state, waiting {wait_time}s before retry..."
|
|
788
|
+
)
|
|
789
|
+
await asyncio.sleep(wait_time)
|
|
790
|
+
continue
|
|
791
|
+
except Exception as e:
|
|
792
|
+
logger.debug(f"State check failed for {entity_id}: {e}")
|
|
793
|
+
|
|
794
|
+
# Try registry lookup
|
|
795
|
+
registry_msg: dict[str, Any] = {
|
|
796
|
+
"type": "config/entity_registry/get",
|
|
797
|
+
"entity_id": entity_id,
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
try:
|
|
801
|
+
registry_result = await client.send_websocket_message(registry_msg)
|
|
802
|
+
|
|
803
|
+
if registry_result.get("success"):
|
|
804
|
+
entity_entry = registry_result.get("result", {})
|
|
805
|
+
unique_id = entity_entry.get("unique_id")
|
|
806
|
+
if unique_id:
|
|
807
|
+
logger.info(f"Found unique_id: {unique_id} for {entity_id}")
|
|
808
|
+
break
|
|
809
|
+
|
|
810
|
+
# If registry lookup failed but we haven't exhausted retries, wait and try again
|
|
811
|
+
if attempt < max_retries - 1:
|
|
812
|
+
wait_time = 0.5 * (2**attempt) # Exponential backoff
|
|
813
|
+
logger.debug(
|
|
814
|
+
f"Registry lookup failed for {entity_id}, waiting {wait_time}s before retry..."
|
|
815
|
+
)
|
|
816
|
+
await asyncio.sleep(wait_time)
|
|
817
|
+
|
|
818
|
+
except Exception as e:
|
|
819
|
+
logger.warning(f"Registry lookup attempt {attempt + 1} failed: {e}")
|
|
820
|
+
if attempt < max_retries - 1:
|
|
821
|
+
wait_time = 0.5 * (2**attempt)
|
|
822
|
+
await asyncio.sleep(wait_time)
|
|
823
|
+
|
|
824
|
+
# Fallback strategy 1: Try deletion with helper_id directly if unique_id not found
|
|
825
|
+
if not unique_id:
|
|
826
|
+
logger.info(
|
|
827
|
+
f"Could not find unique_id for {entity_id}, trying direct deletion with helper_id"
|
|
828
|
+
)
|
|
829
|
+
|
|
830
|
+
# Try deleting using helper_id directly (fallback approach)
|
|
831
|
+
delete_msg: dict[str, Any] = {
|
|
832
|
+
"type": f"{helper_type}/delete",
|
|
833
|
+
f"{helper_type}_id": helper_id,
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
logger.info(f"Sending fallback WebSocket delete message: {delete_msg}")
|
|
837
|
+
result = await client.send_websocket_message(delete_msg)
|
|
838
|
+
|
|
839
|
+
if result.get("success"):
|
|
840
|
+
return {
|
|
841
|
+
"success": True,
|
|
842
|
+
"action": "delete",
|
|
843
|
+
"helper_type": helper_type,
|
|
844
|
+
"helper_id": helper_id,
|
|
845
|
+
"entity_id": entity_id,
|
|
846
|
+
"method": "fallback_direct_id",
|
|
847
|
+
"message": f"Successfully deleted {helper_type}: {helper_id} using direct ID (entity: {entity_id})",
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
# Fallback strategy 2: Check if entity was already deleted
|
|
851
|
+
try:
|
|
852
|
+
final_state_check = await client.get_state(entity_id)
|
|
853
|
+
if not final_state_check:
|
|
854
|
+
logger.info(
|
|
855
|
+
f"Entity {entity_id} no longer exists, considering deletion successful"
|
|
856
|
+
)
|
|
857
|
+
return {
|
|
858
|
+
"success": True,
|
|
859
|
+
"action": "delete",
|
|
860
|
+
"helper_type": helper_type,
|
|
861
|
+
"helper_id": helper_id,
|
|
862
|
+
"entity_id": entity_id,
|
|
863
|
+
"method": "already_deleted",
|
|
864
|
+
"message": f"Helper {helper_id} was already deleted or never properly registered",
|
|
865
|
+
}
|
|
866
|
+
except Exception:
|
|
867
|
+
pass
|
|
868
|
+
|
|
869
|
+
# Final fallback failed
|
|
870
|
+
return {
|
|
871
|
+
"success": False,
|
|
872
|
+
"error": f"Helper not found in entity registry after {max_retries} attempts: {registry_result.get('error', 'Unknown error') if registry_result else 'No registry response'}",
|
|
873
|
+
"helper_id": helper_id,
|
|
874
|
+
"entity_id": entity_id,
|
|
875
|
+
"suggestion": "Helper may not be properly registered or was already deleted. Use ha_search_entities() to verify.",
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
# Delete helper using unique_id (correct API from docs)
|
|
879
|
+
delete_message: dict[str, Any] = {
|
|
880
|
+
"type": f"{helper_type}/delete",
|
|
881
|
+
f"{helper_type}_id": unique_id,
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
logger.info(f"Sending WebSocket delete message: {delete_message}")
|
|
885
|
+
result = await client.send_websocket_message(delete_message)
|
|
886
|
+
logger.info(f"WebSocket delete response: {result}")
|
|
887
|
+
|
|
888
|
+
if result.get("success"):
|
|
889
|
+
return {
|
|
890
|
+
"success": True,
|
|
891
|
+
"action": "delete",
|
|
892
|
+
"helper_type": helper_type,
|
|
893
|
+
"helper_id": helper_id,
|
|
894
|
+
"entity_id": entity_id,
|
|
895
|
+
"unique_id": unique_id,
|
|
896
|
+
"method": "standard",
|
|
897
|
+
"message": f"Successfully deleted {helper_type}: {helper_id} (entity: {entity_id})",
|
|
898
|
+
}
|
|
899
|
+
else:
|
|
900
|
+
error_msg = result.get("error", "Unknown error")
|
|
901
|
+
# Handle specific HA error messages
|
|
902
|
+
if isinstance(error_msg, dict):
|
|
903
|
+
error_msg = error_msg.get("message", str(error_msg))
|
|
904
|
+
|
|
905
|
+
return {
|
|
906
|
+
"success": False,
|
|
907
|
+
"error": f"Failed to delete helper: {error_msg}",
|
|
908
|
+
"helper_id": helper_id,
|
|
909
|
+
"entity_id": entity_id,
|
|
910
|
+
"unique_id": unique_id,
|
|
911
|
+
"suggestion": "Make sure the helper exists and is not being used by automations or scripts",
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
except Exception as e:
|
|
915
|
+
return {
|
|
916
|
+
"success": False,
|
|
917
|
+
"error": f"Helper deletion failed: {str(e)}",
|
|
918
|
+
"helper_type": helper_type,
|
|
919
|
+
"helper_id": helper_id,
|
|
920
|
+
"suggestions": [
|
|
921
|
+
"Check Home Assistant connection",
|
|
922
|
+
"Verify helper_id exists using ha_search_entities()",
|
|
923
|
+
"Ensure helper is not being used by automations or scripts",
|
|
924
|
+
],
|
|
925
|
+
}
|