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,508 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration management tools for Home Assistant automations.
|
|
3
|
+
|
|
4
|
+
This module provides tools for retrieving, creating, updating, and removing
|
|
5
|
+
Home Assistant automation configurations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Annotated, Any, cast
|
|
10
|
+
|
|
11
|
+
from pydantic import Field
|
|
12
|
+
|
|
13
|
+
from ..errors import (
|
|
14
|
+
create_config_error,
|
|
15
|
+
create_resource_not_found_error,
|
|
16
|
+
create_validation_error,
|
|
17
|
+
)
|
|
18
|
+
from .helpers import exception_to_structured_error, log_tool_usage
|
|
19
|
+
from .util_helpers import parse_json_param
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _normalize_automation_config(
|
|
25
|
+
config: Any, parent_key: str | None = None, in_choose_or_if: bool = False
|
|
26
|
+
) -> Any:
|
|
27
|
+
"""
|
|
28
|
+
Recursively normalize automation config field names to HA API format.
|
|
29
|
+
|
|
30
|
+
Home Assistant accepts both singular ('trigger', 'action', 'condition')
|
|
31
|
+
and plural ('triggers', 'actions', 'conditions') field names in YAML,
|
|
32
|
+
but the API expects singular forms at the root level.
|
|
33
|
+
|
|
34
|
+
IMPORTANT: Inside 'choose' and 'if' action blocks, the 'conditions' key
|
|
35
|
+
(plural) is required by the HA schema and should NOT be normalized to
|
|
36
|
+
'condition' (singular).
|
|
37
|
+
|
|
38
|
+
IMPORTANT: Inside compound condition blocks ('or', 'and', 'not'), the
|
|
39
|
+
'conditions' key (plural) is required and should NOT be normalized to
|
|
40
|
+
'condition' (singular).
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
config: Automation configuration (dict, list, or primitive)
|
|
44
|
+
parent_key: The parent dictionary key (for context tracking)
|
|
45
|
+
in_choose_or_if: Whether we're inside a choose/if option that requires
|
|
46
|
+
'conditions' (plural) to remain unchanged
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Normalized configuration with singular field names at root level,
|
|
50
|
+
but preserving 'conditions' (plural) inside choose/if blocks and
|
|
51
|
+
compound condition blocks (or/and/not)
|
|
52
|
+
"""
|
|
53
|
+
# Handle lists - recursively process each item
|
|
54
|
+
if isinstance(config, list):
|
|
55
|
+
# If parent is 'choose' or 'if', items are options that need 'conditions' preserved
|
|
56
|
+
is_option_list = parent_key in ("choose", "if")
|
|
57
|
+
return [
|
|
58
|
+
_normalize_automation_config(item, parent_key, is_option_list)
|
|
59
|
+
for item in config
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
# Handle primitives (strings, numbers, etc.)
|
|
63
|
+
if not isinstance(config, dict):
|
|
64
|
+
return config
|
|
65
|
+
|
|
66
|
+
# Process dictionary
|
|
67
|
+
normalized = config.copy()
|
|
68
|
+
|
|
69
|
+
# Check if this dict is a compound condition block (or/and/not)
|
|
70
|
+
# that needs its nested 'conditions' key preserved
|
|
71
|
+
is_compound_condition_block = normalized.get("condition") in ("or", "and", "not")
|
|
72
|
+
|
|
73
|
+
# Map plural field names to singular (HA API format)
|
|
74
|
+
# EXCEPT 'conditions' when inside choose/if blocks OR in compound condition blocks
|
|
75
|
+
field_mappings = {
|
|
76
|
+
"triggers": "trigger",
|
|
77
|
+
"actions": "action",
|
|
78
|
+
# Note: 'sequence' is already singular, but some users might use 'sequences'
|
|
79
|
+
"sequences": "sequence",
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# Only add 'conditions' mapping if NOT inside a choose/if option
|
|
83
|
+
# AND NOT a compound condition block (or/and/not)
|
|
84
|
+
if not in_choose_or_if and not is_compound_condition_block:
|
|
85
|
+
field_mappings["conditions"] = "condition"
|
|
86
|
+
|
|
87
|
+
# Apply field mapping to current level
|
|
88
|
+
for plural, singular in field_mappings.items():
|
|
89
|
+
if plural in normalized and singular not in normalized:
|
|
90
|
+
normalized[singular] = normalized.pop(plural)
|
|
91
|
+
elif plural in normalized and singular in normalized:
|
|
92
|
+
# Both exist - prefer singular, remove plural
|
|
93
|
+
del normalized[plural]
|
|
94
|
+
|
|
95
|
+
# Recursively process all values in the dictionary
|
|
96
|
+
for key, value in normalized.items():
|
|
97
|
+
normalized[key] = _normalize_automation_config(value, key)
|
|
98
|
+
|
|
99
|
+
return normalized
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _normalize_trigger_keys(triggers: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
103
|
+
"""
|
|
104
|
+
Normalize trigger objects for round-trip compatibility.
|
|
105
|
+
|
|
106
|
+
Home Assistant GET API returns triggers with 'trigger' key for the platform type,
|
|
107
|
+
but the SET API expects 'platform' key. This function converts between formats.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
triggers: List of trigger configuration dicts
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
List of triggers with 'platform' key instead of 'trigger' key
|
|
114
|
+
"""
|
|
115
|
+
normalized_triggers = []
|
|
116
|
+
for trigger in triggers:
|
|
117
|
+
normalized_trigger = trigger.copy()
|
|
118
|
+
# Convert 'trigger' key to 'platform' if present and 'platform' is not
|
|
119
|
+
if "trigger" in normalized_trigger and "platform" not in normalized_trigger:
|
|
120
|
+
normalized_trigger["platform"] = normalized_trigger.pop("trigger")
|
|
121
|
+
normalized_triggers.append(normalized_trigger)
|
|
122
|
+
return normalized_triggers
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _normalize_config_for_roundtrip(config: dict[str, Any]) -> dict[str, Any]:
|
|
126
|
+
"""
|
|
127
|
+
Normalize automation config from GET response for direct use in SET.
|
|
128
|
+
|
|
129
|
+
This ensures a config retrieved via ha_config_get_automation can be
|
|
130
|
+
directly passed to ha_config_set_automation without modification.
|
|
131
|
+
|
|
132
|
+
Transformations:
|
|
133
|
+
1. Field names: triggers -> trigger, actions -> action, conditions -> condition
|
|
134
|
+
2. Trigger keys: trigger -> platform (inside each trigger object)
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
config: Raw automation configuration from HA API
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Normalized configuration compatible with SET API
|
|
141
|
+
"""
|
|
142
|
+
# First normalize field names (plural -> singular)
|
|
143
|
+
normalized = _normalize_automation_config(config)
|
|
144
|
+
|
|
145
|
+
# Then normalize trigger keys (trigger -> platform)
|
|
146
|
+
if "trigger" in normalized and isinstance(normalized["trigger"], list):
|
|
147
|
+
normalized["trigger"] = _normalize_trigger_keys(normalized["trigger"])
|
|
148
|
+
|
|
149
|
+
return normalized
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _strip_empty_automation_fields(config: dict[str, Any]) -> dict[str, Any]:
|
|
153
|
+
"""
|
|
154
|
+
Strip empty trigger/action/condition arrays from automation config.
|
|
155
|
+
|
|
156
|
+
Blueprint-based automations should not have trigger/action/condition fields
|
|
157
|
+
since these come from the blueprint itself. If empty arrays are present,
|
|
158
|
+
they override the blueprint's configuration and break the automation.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
config: Automation configuration dict
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Configuration with empty trigger/action/condition arrays removed
|
|
165
|
+
"""
|
|
166
|
+
cleaned = config.copy()
|
|
167
|
+
|
|
168
|
+
# Remove empty arrays for blueprint automations
|
|
169
|
+
for field in ["trigger", "action", "condition"]:
|
|
170
|
+
if field in cleaned and cleaned[field] == []:
|
|
171
|
+
del cleaned[field]
|
|
172
|
+
|
|
173
|
+
return cleaned
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def register_config_automation_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
177
|
+
"""Register Home Assistant automation configuration tools."""
|
|
178
|
+
|
|
179
|
+
@mcp.tool(
|
|
180
|
+
annotations={
|
|
181
|
+
"idempotentHint": True,
|
|
182
|
+
"readOnlyHint": True,
|
|
183
|
+
"tags": ["automation"],
|
|
184
|
+
"title": "Get Automation Config",
|
|
185
|
+
}
|
|
186
|
+
)
|
|
187
|
+
@log_tool_usage
|
|
188
|
+
async def ha_config_get_automation(
|
|
189
|
+
identifier: Annotated[
|
|
190
|
+
str,
|
|
191
|
+
Field(
|
|
192
|
+
description="Automation entity_id (e.g., 'automation.morning_routine') or unique_id"
|
|
193
|
+
),
|
|
194
|
+
],
|
|
195
|
+
) -> dict[str, Any]:
|
|
196
|
+
"""
|
|
197
|
+
Retrieve Home Assistant automation configuration.
|
|
198
|
+
|
|
199
|
+
Returns the complete configuration including triggers, conditions, actions, and mode settings.
|
|
200
|
+
|
|
201
|
+
EXAMPLES:
|
|
202
|
+
- Get automation: ha_config_get_automation("automation.morning_routine")
|
|
203
|
+
- Get by unique_id: ha_config_get_automation("my_unique_automation_id")
|
|
204
|
+
|
|
205
|
+
For comprehensive automation documentation, use: ha_get_domain_docs("automation")
|
|
206
|
+
"""
|
|
207
|
+
try:
|
|
208
|
+
config_result = await client.get_automation_config(identifier)
|
|
209
|
+
# Normalize config for round-trip compatibility (GET → SET)
|
|
210
|
+
normalized_config = _normalize_config_for_roundtrip(config_result)
|
|
211
|
+
return {
|
|
212
|
+
"success": True,
|
|
213
|
+
"action": "get",
|
|
214
|
+
"identifier": identifier,
|
|
215
|
+
"config": normalized_config,
|
|
216
|
+
}
|
|
217
|
+
except Exception as e:
|
|
218
|
+
# Handle 404 errors gracefully (often used to verify deletion)
|
|
219
|
+
error_str = str(e)
|
|
220
|
+
if (
|
|
221
|
+
"404" in error_str
|
|
222
|
+
or "not found" in error_str.lower()
|
|
223
|
+
or "entity not found" in error_str.lower()
|
|
224
|
+
):
|
|
225
|
+
logger.debug(
|
|
226
|
+
f"Automation {identifier} not found (expected for deletion verification)"
|
|
227
|
+
)
|
|
228
|
+
error_response = create_resource_not_found_error(
|
|
229
|
+
"Automation",
|
|
230
|
+
identifier,
|
|
231
|
+
details=f"Automation '{identifier}' does not exist in Home Assistant",
|
|
232
|
+
)
|
|
233
|
+
error_response["action"] = "get"
|
|
234
|
+
error_response["reason"] = "not_found"
|
|
235
|
+
return error_response
|
|
236
|
+
|
|
237
|
+
logger.error(f"Error getting automation: {e}")
|
|
238
|
+
error_response = exception_to_structured_error(
|
|
239
|
+
e,
|
|
240
|
+
context={"identifier": identifier, "action": "get"},
|
|
241
|
+
)
|
|
242
|
+
# Add automation-specific suggestions
|
|
243
|
+
if "error" in error_response and isinstance(error_response["error"], dict):
|
|
244
|
+
error_response["error"]["suggestions"] = [
|
|
245
|
+
"Verify automation exists using ha_search_entities(domain_filter='automation')",
|
|
246
|
+
"Check Home Assistant connection",
|
|
247
|
+
"Use ha_get_domain_docs('automation') for configuration help",
|
|
248
|
+
]
|
|
249
|
+
return error_response
|
|
250
|
+
|
|
251
|
+
@mcp.tool(
|
|
252
|
+
annotations={
|
|
253
|
+
"destructiveHint": True,
|
|
254
|
+
"tags": ["automation"],
|
|
255
|
+
"title": "Create or Update Automation",
|
|
256
|
+
}
|
|
257
|
+
)
|
|
258
|
+
@log_tool_usage
|
|
259
|
+
async def ha_config_set_automation(
|
|
260
|
+
config: Annotated[
|
|
261
|
+
str | dict[str, Any],
|
|
262
|
+
Field(
|
|
263
|
+
description="Complete automation configuration with required fields: 'alias', 'trigger', 'action'. Optional: 'description', 'condition', 'mode', 'max', 'initial_state', 'variables'"
|
|
264
|
+
),
|
|
265
|
+
],
|
|
266
|
+
identifier: Annotated[
|
|
267
|
+
str | None,
|
|
268
|
+
Field(
|
|
269
|
+
description="Automation entity_id or unique_id for updates. Omit to create new automation with generated unique_id.",
|
|
270
|
+
default=None,
|
|
271
|
+
),
|
|
272
|
+
] = None,
|
|
273
|
+
) -> dict[str, Any]:
|
|
274
|
+
"""
|
|
275
|
+
Create or update a Home Assistant automation.
|
|
276
|
+
|
|
277
|
+
Creates a new automation (if identifier omitted) or updates existing automation with provided configuration.
|
|
278
|
+
|
|
279
|
+
AUTOMATION TYPES:
|
|
280
|
+
|
|
281
|
+
1. Regular Automations - Define triggers and actions directly
|
|
282
|
+
2. Blueprint Automations - Use pre-built templates with customizable inputs
|
|
283
|
+
|
|
284
|
+
REQUIRED FIELDS (Regular Automations):
|
|
285
|
+
- alias: Human-readable automation name
|
|
286
|
+
- trigger: List of trigger conditions (time, state, event, etc.)
|
|
287
|
+
- action: List of actions to execute
|
|
288
|
+
|
|
289
|
+
REQUIRED FIELDS (Blueprint Automations):
|
|
290
|
+
- alias: Human-readable automation name
|
|
291
|
+
- use_blueprint: Blueprint configuration
|
|
292
|
+
- path: Blueprint file path (e.g., "motion_light.yaml")
|
|
293
|
+
- input: Dictionary of input values for the blueprint
|
|
294
|
+
|
|
295
|
+
OPTIONAL CONFIG FIELDS (Regular Automations):
|
|
296
|
+
- description: Detailed description of the user's intent (RECOMMENDED: helps safely modify implementation later)
|
|
297
|
+
- condition: Additional conditions that must be met
|
|
298
|
+
- mode: 'single' (default), 'restart', 'queued', 'parallel'
|
|
299
|
+
- max: Maximum concurrent executions (for queued/parallel modes)
|
|
300
|
+
- initial_state: Whether automation starts enabled (true/false)
|
|
301
|
+
- variables: Variables for use in automation
|
|
302
|
+
|
|
303
|
+
BASIC EXAMPLES:
|
|
304
|
+
|
|
305
|
+
Simple time-based automation:
|
|
306
|
+
ha_config_set_automation({
|
|
307
|
+
"alias": "Morning Lights",
|
|
308
|
+
"description": "Turn on bedroom lights at 7 AM to help wake up",
|
|
309
|
+
"trigger": [{"platform": "time", "at": "07:00:00"}],
|
|
310
|
+
"action": [{"service": "light.turn_on", "target": {"area_id": "bedroom"}}]
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
Motion-activated lighting with condition:
|
|
314
|
+
ha_config_set_automation({
|
|
315
|
+
"alias": "Motion Light",
|
|
316
|
+
"trigger": [{"platform": "state", "entity_id": "binary_sensor.motion", "to": "on"}],
|
|
317
|
+
"condition": [{"condition": "sun", "after": "sunset"}],
|
|
318
|
+
"action": [
|
|
319
|
+
{"service": "light.turn_on", "target": {"entity_id": "light.hallway"}},
|
|
320
|
+
{"delay": {"minutes": 5}},
|
|
321
|
+
{"service": "light.turn_off", "target": {"entity_id": "light.hallway"}}
|
|
322
|
+
],
|
|
323
|
+
"mode": "restart"
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
Update existing automation:
|
|
327
|
+
ha_config_set_automation(
|
|
328
|
+
identifier="automation.morning_routine",
|
|
329
|
+
config={
|
|
330
|
+
"alias": "Updated Morning Routine",
|
|
331
|
+
"trigger": [{"platform": "time", "at": "06:30:00"}],
|
|
332
|
+
"action": [
|
|
333
|
+
{"service": "light.turn_on", "target": {"area_id": "bedroom"}},
|
|
334
|
+
{"service": "climate.set_temperature", "target": {"entity_id": "climate.bedroom"}, "data": {"temperature": 22}}
|
|
335
|
+
]
|
|
336
|
+
}
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
BLUEPRINT AUTOMATION EXAMPLES:
|
|
340
|
+
|
|
341
|
+
Create automation from blueprint:
|
|
342
|
+
ha_config_set_automation({
|
|
343
|
+
"alias": "Motion Light Kitchen",
|
|
344
|
+
"use_blueprint": {
|
|
345
|
+
"path": "homeassistant/motion_light.yaml",
|
|
346
|
+
"input": {
|
|
347
|
+
"motion_entity": "binary_sensor.kitchen_motion",
|
|
348
|
+
"light_target": {"entity_id": "light.kitchen"},
|
|
349
|
+
"no_motion_wait": 120
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
Update blueprint automation inputs:
|
|
355
|
+
ha_config_set_automation(
|
|
356
|
+
identifier="automation.motion_light_kitchen",
|
|
357
|
+
config={
|
|
358
|
+
"alias": "Motion Light Kitchen",
|
|
359
|
+
"use_blueprint": {
|
|
360
|
+
"path": "homeassistant/motion_light.yaml",
|
|
361
|
+
"input": {
|
|
362
|
+
"motion_entity": "binary_sensor.kitchen_motion",
|
|
363
|
+
"light_target": {"entity_id": "light.kitchen"},
|
|
364
|
+
"no_motion_wait": 300
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
PREFER NATIVE SOLUTIONS OVER TEMPLATES:
|
|
371
|
+
Before using template triggers/conditions/actions, check if a native option exists:
|
|
372
|
+
- Use `condition: state` with `state: [list]` instead of template for multiple states
|
|
373
|
+
- Use `condition: state` with `attribute:` instead of template for attribute checks
|
|
374
|
+
- Use `condition: numeric_state` instead of template for number comparisons
|
|
375
|
+
- Use `wait_for_trigger` instead of `wait_template` when waiting for state changes
|
|
376
|
+
- Use `choose` action instead of template-based service names
|
|
377
|
+
|
|
378
|
+
TRIGGER TYPES: time, time_pattern, sun, state, numeric_state, event, device, zone, template, and more
|
|
379
|
+
CONDITION TYPES: state, numeric_state, time, sun, template, device, zone, and more
|
|
380
|
+
ACTION TYPES: service calls, delays, wait_for_trigger, wait_template, if/then/else, choose, repeat, parallel
|
|
381
|
+
|
|
382
|
+
For comprehensive automation documentation with all trigger/condition/action types and advanced examples:
|
|
383
|
+
- Use: ha_get_domain_docs("automation")
|
|
384
|
+
- Or visit: https://www.home-assistant.io/docs/automation/
|
|
385
|
+
|
|
386
|
+
TROUBLESHOOTING:
|
|
387
|
+
- Use ha_get_state() to verify entity_ids exist
|
|
388
|
+
- Use ha_search_entities() to find correct entity_ids
|
|
389
|
+
- Use ha_eval_template() to test Jinja2 templates before using in automations
|
|
390
|
+
- Use ha_search_entities(domain_filter='automation') to find existing automations
|
|
391
|
+
"""
|
|
392
|
+
try:
|
|
393
|
+
# Parse JSON config if provided as string
|
|
394
|
+
try:
|
|
395
|
+
parsed_config = parse_json_param(config, "config")
|
|
396
|
+
except ValueError as e:
|
|
397
|
+
return create_validation_error(
|
|
398
|
+
f"Invalid config parameter: {e}",
|
|
399
|
+
parameter="config",
|
|
400
|
+
invalid_json=True,
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
# Ensure config is a dict
|
|
404
|
+
if parsed_config is None or not isinstance(parsed_config, dict):
|
|
405
|
+
return create_validation_error(
|
|
406
|
+
"Config parameter must be a JSON object",
|
|
407
|
+
parameter="config",
|
|
408
|
+
details=f"Received type: {type(parsed_config).__name__}",
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
config_dict = cast(dict[str, Any], parsed_config)
|
|
412
|
+
|
|
413
|
+
# Normalize field names (triggers -> trigger, actions -> action, etc.)
|
|
414
|
+
config_dict = _normalize_automation_config(config_dict)
|
|
415
|
+
|
|
416
|
+
# Validate required fields based on automation type
|
|
417
|
+
# Blueprint automations only need alias, regular automations need trigger and action
|
|
418
|
+
if "use_blueprint" in config_dict:
|
|
419
|
+
required_fields = ["alias"]
|
|
420
|
+
# Strip empty trigger/action/condition arrays that would override blueprint
|
|
421
|
+
config_dict = _strip_empty_automation_fields(config_dict)
|
|
422
|
+
else:
|
|
423
|
+
required_fields = ["alias", "trigger", "action"]
|
|
424
|
+
|
|
425
|
+
missing_fields = [f for f in required_fields if f not in config_dict]
|
|
426
|
+
if missing_fields:
|
|
427
|
+
return create_config_error(
|
|
428
|
+
f"Missing required fields: {', '.join(missing_fields)}",
|
|
429
|
+
identifier=identifier,
|
|
430
|
+
missing_fields=missing_fields,
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
result = await client.upsert_automation_config(config_dict, identifier)
|
|
434
|
+
return {
|
|
435
|
+
"success": True,
|
|
436
|
+
**result,
|
|
437
|
+
"config_provided": config_dict,
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
except Exception as e:
|
|
441
|
+
logger.error(f"Error upserting automation: {e}")
|
|
442
|
+
error_response = exception_to_structured_error(
|
|
443
|
+
e,
|
|
444
|
+
context={"identifier": identifier},
|
|
445
|
+
)
|
|
446
|
+
# Add automation-specific suggestions
|
|
447
|
+
if "error" in error_response and isinstance(error_response["error"], dict):
|
|
448
|
+
error_response["error"]["suggestions"] = [
|
|
449
|
+
"Check automation configuration format",
|
|
450
|
+
"Ensure required fields: alias, trigger, action",
|
|
451
|
+
"Use entity_id format: automation.morning_routine or unique_id",
|
|
452
|
+
"Use ha_search_entities(domain_filter='automation') to find automations",
|
|
453
|
+
"Use ha_get_domain_docs('automation') for comprehensive configuration help",
|
|
454
|
+
]
|
|
455
|
+
return error_response
|
|
456
|
+
|
|
457
|
+
@mcp.tool(
|
|
458
|
+
annotations={
|
|
459
|
+
"destructiveHint": True,
|
|
460
|
+
"idempotentHint": True,
|
|
461
|
+
"tags": ["automation"],
|
|
462
|
+
"title": "Remove Automation",
|
|
463
|
+
}
|
|
464
|
+
)
|
|
465
|
+
@log_tool_usage
|
|
466
|
+
async def ha_config_remove_automation(
|
|
467
|
+
identifier: Annotated[
|
|
468
|
+
str,
|
|
469
|
+
Field(
|
|
470
|
+
description="Automation entity_id (e.g., 'automation.old_automation') or unique_id to delete"
|
|
471
|
+
),
|
|
472
|
+
],
|
|
473
|
+
) -> dict[str, Any]:
|
|
474
|
+
"""
|
|
475
|
+
Delete a Home Assistant automation.
|
|
476
|
+
|
|
477
|
+
EXAMPLES:
|
|
478
|
+
- Delete automation: ha_config_remove_automation("automation.old_automation")
|
|
479
|
+
- Delete by unique_id: ha_config_remove_automation("my_unique_id")
|
|
480
|
+
|
|
481
|
+
**WARNING:** Deleting an automation removes it permanently from your Home Assistant configuration.
|
|
482
|
+
"""
|
|
483
|
+
try:
|
|
484
|
+
result = await client.delete_automation_config(identifier)
|
|
485
|
+
return {"success": True, "action": "delete", **result}
|
|
486
|
+
except Exception as e:
|
|
487
|
+
logger.error(f"Error deleting automation: {e}")
|
|
488
|
+
error_str = str(e).lower()
|
|
489
|
+
if "404" in error_str or "not found" in error_str:
|
|
490
|
+
error_response = create_resource_not_found_error(
|
|
491
|
+
"Automation",
|
|
492
|
+
identifier,
|
|
493
|
+
details=f"Automation '{identifier}' does not exist",
|
|
494
|
+
)
|
|
495
|
+
else:
|
|
496
|
+
error_response = exception_to_structured_error(
|
|
497
|
+
e,
|
|
498
|
+
context={"identifier": identifier},
|
|
499
|
+
)
|
|
500
|
+
error_response["action"] = "delete"
|
|
501
|
+
# Add automation-specific suggestions
|
|
502
|
+
if "error" in error_response and isinstance(error_response["error"], dict):
|
|
503
|
+
error_response["error"]["suggestions"] = [
|
|
504
|
+
"Verify automation exists using ha_search_entities(domain_filter='automation')",
|
|
505
|
+
"Use entity_id format: automation.morning_routine or unique_id",
|
|
506
|
+
"Check Home Assistant connection",
|
|
507
|
+
]
|
|
508
|
+
return error_response
|