Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion site/src/data/tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@
{
"name": "ha_config_set_automation",
"title": "Create or Update Automation",
"description": "Create or update a Home Assistant automation.\n\nCreates a new automation (if identifier omitted) or updates existing automation with provided configuration.\n\nAUTOMATION TYPES:\n\n1. Regular Automations - Define triggers and actions directly\n2. Blueprint Automations - Use pre-built templates with customizable inputs\n\nREQUIRED FIELDS (Regular Automations):\n- alias: Human-readable automation name\n- trigger: List of trigger conditions (time, state, event, etc.)\n- action: List of actions to execute\n\nREQUIRED FIELDS (Blueprint Automations):\n- alias: Human-readable automation name\n- use_blueprint: Blueprint configuration\n - path: Blueprint file path (e.g., \"motion_light.yaml\")\n - input: Dictionary of input values for the blueprint\n\nOPTIONAL CONFIG FIELDS (Regular Automations):\n- description: Detailed description of the user's intent (RECOMMENDED: helps safely modify implementation later)\n- condition: Additional conditions that must be met\n- mode: 'single' (default), 'restart', 'queued', 'parallel'\n- max: Maximum concurrent executions (for queued/parallel modes)\n- initial_state: Whether automation starts enabled (true/false)\n- variables: Variables for use in automation\n\nBASIC EXAMPLES:\n\nSimple time-based automation:\nha_config_set_automation({\n \"alias\": \"Morning Lights\",\n \"description\": \"Turn on bedroom lights at 7 AM to help wake up\",\n \"trigger\": [{\"platform\": \"time\", \"at\": \"07:00:00\"}],\n \"action\": [{\"service\": \"light.turn_on\", \"target\": {\"area_id\": \"bedroom\"}}]\n})\n\nMotion-activated lighting with condition:\nha_config_set_automation({\n \"alias\": \"Motion Light\",\n \"trigger\": [{\"platform\": \"state\", \"entity_id\": \"binary_sensor.motion\", \"to\": \"on\"}],\n \"condition\": [{\"condition\": \"sun\", \"after\": \"sunset\"}],\n \"action\": [\n {\"service\": \"light.turn_on\", \"target\": {\"entity_id\": \"light.hallway\"}},\n {\"delay\": {\"minutes\": 5}},\n {\"service\": \"light.turn_off\", \"target\": {\"entity_id\": \"light.hallway\"}}\n ],\n \"mode\": \"restart\"\n})\n\nUpdate existing automation:\nha_config_set_automation(\n identifier=\"automation.morning_routine\",\n config={\n \"alias\": \"Updated Morning Routine\",\n \"trigger\": [{\"platform\": \"time\", \"at\": \"06:30:00\"}],\n \"action\": [\n {\"service\": \"light.turn_on\", \"target\": {\"area_id\": \"bedroom\"}},\n {\"service\": \"climate.set_temperature\", \"target\": {\"entity_id\": \"climate.bedroom\"}, \"data\": {\"temperature\": 22}}\n ]\n }\n)\n\nBLUEPRINT AUTOMATION EXAMPLES:\n\nCreate automation from blueprint:\nha_config_set_automation({\n \"alias\": \"Motion Light Kitchen\",\n \"use_blueprint\": {\n \"path\": \"homeassistant/motion_light.yaml\",\n \"input\": {\n \"motion_entity\": \"binary_sensor.kitchen_motion\",\n \"light_target\": {\"entity_id\": \"light.kitchen\"},\n \"no_motion_wait\": 120\n }\n }\n})\n\nUpdate blueprint automation inputs:\nha_config_set_automation(\n identifier=\"automation.motion_light_kitchen\",\n config={\n \"alias\": \"Motion Light Kitchen\",\n \"use_blueprint\": {\n \"path\": \"homeassistant/motion_light.yaml\",\n \"input\": {\n \"motion_entity\": \"binary_sensor.kitchen_motion\",\n \"light_target\": {\"entity_id\": \"light.kitchen\"},\n \"no_motion_wait\": 300\n }\n }\n }\n})\n\nPREFER NATIVE SOLUTIONS OVER TEMPLATES:\nBefore using template triggers/conditions/actions, check if a native option exists:\n- Use `condition: state` with `state: [list]` instead of template for multiple states\n- Use `condition: state` with `attribute:` instead of template for attribute checks\n- Use `condition: numeric_state` instead of template for number comparisons\n- Use `wait_for_trigger` instead of `wait_template` when waiting for state changes\n- Use `choose` action instead of template-based service names\n\nTRIGGER TYPES: time, time_pattern, sun, state, numeric_state, event, device, zone, template, and more\nCONDITION TYPES: state, numeric_state, time, sun, template, device, zone, and more\nACTION TYPES: service calls, delays, wait_for_trigger, wait_template, if/then/else, choose, repeat, parallel\n\nFor comprehensive automation documentation with all trigger/condition/action types and advanced examples:\n- Use: ha_get_skill_home_assistant_best_practices\n- Or visit: https://www.home-assistant.io/docs/automation/\n\nTROUBLESHOOTING:\n- Use ha_get_state() to verify entity_ids exist\n- Use ha_search_entities() to find correct entity_ids\n- Use ha_eval_template() to test Jinja2 templates before using in automations\n- Use ha_search_entities(domain_filter='automation') to find existing automations",
"description": "Create or update a Home Assistant automation.\n\nCreates a new automation (if identifier omitted) or updates existing automation with provided configuration.\n\nAUTOMATION TYPES:\n\n1. Regular Automations - Define triggers and actions directly\n2. Blueprint Automations - Use pre-built templates with customizable inputs\n\nREQUIRED FIELDS (Regular Automations):\n- alias: Human-readable automation name\n- trigger: List of trigger conditions (time, state, event, etc.)\n- action: List of actions to execute\n\nREQUIRED FIELDS (Blueprint Automations):\n- alias: Human-readable automation name\n- use_blueprint: Blueprint configuration\n - path: Blueprint file path (e.g., \"motion_light.yaml\")\n - input: Dictionary of input values for the blueprint\n\nOPTIONAL CONFIG FIELDS (Regular Automations):\n- description: Detailed description of the user's intent (RECOMMENDED: helps safely modify implementation later)\n- category: Category ID for organization (use ha_config_get_category to list, ha_config_set_category to create)\n- condition: Additional conditions that must be met\n- mode: 'single' (default), 'restart', 'queued', 'parallel'\n- max: Maximum concurrent executions (for queued/parallel modes)\n- initial_state: Whether automation starts enabled (true/false)\n- variables: Variables for use in automation\n\nBASIC EXAMPLES:\n\nSimple time-based automation:\nha_config_set_automation({\n \"alias\": \"Morning Lights\",\n \"description\": \"Turn on bedroom lights at 7 AM to help wake up\",\n \"trigger\": [{\"platform\": \"time\", \"at\": \"07:00:00\"}],\n \"action\": [{\"service\": \"light.turn_on\", \"target\": {\"area_id\": \"bedroom\"}}]\n})\n\nMotion-activated lighting with condition:\nha_config_set_automation({\n \"alias\": \"Motion Light\",\n \"trigger\": [{\"platform\": \"state\", \"entity_id\": \"binary_sensor.motion\", \"to\": \"on\"}],\n \"condition\": [{\"condition\": \"sun\", \"after\": \"sunset\"}],\n \"action\": [\n {\"service\": \"light.turn_on\", \"target\": {\"entity_id\": \"light.hallway\"}},\n {\"delay\": {\"minutes\": 5}},\n {\"service\": \"light.turn_off\", \"target\": {\"entity_id\": \"light.hallway\"}}\n ],\n \"mode\": \"restart\"\n})\n\nUpdate existing automation:\nha_config_set_automation(\n identifier=\"automation.morning_routine\",\n config={\n \"alias\": \"Updated Morning Routine\",\n \"trigger\": [{\"platform\": \"time\", \"at\": \"06:30:00\"}],\n \"action\": [\n {\"service\": \"light.turn_on\", \"target\": {\"area_id\": \"bedroom\"}},\n {\"service\": \"climate.set_temperature\", \"target\": {\"entity_id\": \"climate.bedroom\"}, \"data\": {\"temperature\": 22}}\n ]\n }\n)\n\nBLUEPRINT AUTOMATION EXAMPLES:\n\nCreate automation from blueprint:\nha_config_set_automation({\n \"alias\": \"Motion Light Kitchen\",\n \"use_blueprint\": {\n \"path\": \"homeassistant/motion_light.yaml\",\n \"input\": {\n \"motion_entity\": \"binary_sensor.kitchen_motion\",\n \"light_target\": {\"entity_id\": \"light.kitchen\"},\n \"no_motion_wait\": 120\n }\n }\n})\n\nUpdate blueprint automation inputs:\nha_config_set_automation(\n identifier=\"automation.motion_light_kitchen\",\n config={\n \"alias\": \"Motion Light Kitchen\",\n \"use_blueprint\": {\n \"path\": \"homeassistant/motion_light.yaml\",\n \"input\": {\n \"motion_entity\": \"binary_sensor.kitchen_motion\",\n \"light_target\": {\"entity_id\": \"light.kitchen\"},\n \"no_motion_wait\": 300\n }\n }\n }\n})\n\nPREFER NATIVE SOLUTIONS OVER TEMPLATES:\nBefore using template triggers/conditions/actions, check if a native option exists:\n- Use `condition: state` with `state: [list]` instead of template for multiple states\n- Use `condition: state` with `attribute:` instead of template for attribute checks\n- Use `condition: numeric_state` instead of template for number comparisons\n- Use `wait_for_trigger` instead of `wait_template` when waiting for state changes\n- Use `choose` action instead of template-based service names\n\nTRIGGER TYPES: time, time_pattern, sun, state, numeric_state, event, device, zone, template, and more\nCONDITION TYPES: state, numeric_state, time, sun, template, device, zone, and more\nACTION TYPES: service calls, delays, wait_for_trigger, wait_template, if/then/else, choose, repeat, parallel\n\nFor comprehensive automation documentation with all trigger/condition/action types and advanced examples:\n- Use: ha_get_skill_home_assistant_best_practices\n- Or visit: https://www.home-assistant.io/docs/automation/\n\nTROUBLESHOOTING:\n- Use ha_get_state() to verify entity_ids exist\n- Use ha_search_entities() to find correct entity_ids\n- Use ha_eval_template() to test Jinja2 templates before using in automations\n- Use ha_search_entities(domain_filter='automation') to find existing automations",
"inputSchema": {
"properties": {
"config": {
Expand All @@ -309,6 +309,10 @@
"type": "Annotated[str | None, Field(description='Automation entity_id or unique_id for updates. Omit to create new automation with generated unique_id.', default=None)]",
"default": null
},
"category": {
"type": "Annotated[str | None, Field(description=\"Category ID to assign to this automation. Use ha_config_get_category(scope='automation') to list available categories, or ha_config_set_category() to create one.\", default=None)]",
"default": null
},
"wait": {
"type": "Annotated[bool | str, Field(description='Wait for automation to be queryable before returning. Default: True. Set to False for bulk operations.', default=True)]",
"default": true
Expand Down Expand Up @@ -1510,6 +1514,10 @@
"type": "Annotated[str | None, Field(description='Description for tag', default=None)]",
"default": null
},
"category": {
"type": "Annotated[str | None, Field(description=\"Category ID to assign to this helper. Use ha_config_get_category(scope='helpers') to list available categories, or ha_config_set_category() to create one.\", default=None)]",
"default": null
},
"wait": {
"type": "Annotated[bool | str, Field(description='Wait for helper entity to be queryable before returning. Default: True. Set to False for bulk operations.', default=True)]",
"default": true
Expand Down Expand Up @@ -2075,6 +2083,10 @@
"config": {
"type": "Annotated[str | dict[str, Any], Field(description=\"Script configuration dictionary. Must include EITHER 'sequence' (for regular scripts) OR 'use_blueprint' (for blueprint-based scripts). Optional fields: 'alias', 'description', 'icon', 'mode', 'max', 'fields'\")]"
},
"category": {
"type": "Annotated[str | None, Field(description=\"Category ID to assign to this script. Use ha_config_get_category(scope='script') to list available categories, or ha_config_set_category() to create one.\", default=None)]",
"default": null
},
"wait": {
"type": "Annotated[bool | str, Field(description='Wait for script to be queryable before returning. Default: True. Set to False for bulk operations.', default=True)]",
"default": true
Expand Down
78 changes: 64 additions & 14 deletions src/ha_mcp/tools/tools_config_automations.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,27 @@ def _strip_empty_automation_fields(config: dict[str, Any]) -> dict[str, Any]:
def register_config_automation_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
"""Register Home Assistant automation configuration tools."""

async def _resolve_automation_entity_id(identifier: str) -> str | None:
"""Resolve an automation identifier to its entity_id.

If identifier is already an entity_id (starts with "automation."),
returns it directly. Otherwise, searches states to find the entity
whose unique_id matches the identifier.
"""
if identifier.startswith("automation."):
return identifier
try:
states = await client.get_states()
for state in states:
if (
state.get("entity_id", "").startswith("automation.")
and state.get("attributes", {}).get("id") == identifier
):
return str(state["entity_id"])
except Exception as e:
logger.debug(f"Failed to resolve entity_id for automation {identifier}: {e}")
return None

@mcp.tool(
tags={"Automations"},
annotations={
Expand Down Expand Up @@ -237,6 +258,22 @@ async def ha_config_get_automation(
config_result = await client.get_automation_config(identifier)
# Normalize config for round-trip compatibility (GET → SET)
normalized_config = _normalize_config_for_roundtrip(config_result)

# Resolve entity_id and fetch category from entity registry
entity_id = await _resolve_automation_entity_id(identifier)
if entity_id:
try:
reg_result = await client.send_websocket_message(
{"type": "config/entity_registry/get", "entity_id": entity_id}
)
if reg_result.get("success"):
categories = reg_result.get("result", {}).get("categories", {})
cat_id = categories.get("automation")
if cat_id:
normalized_config["category"] = cat_id
except Exception as e:
logger.debug(f"Failed to fetch category for automation {entity_id}: {e}")
Comment thread
kingpanther13 marked this conversation as resolved.
Outdated

return {
"success": True,
"action": "get",
Expand Down Expand Up @@ -296,6 +333,13 @@ async def ha_config_set_automation(
default=None,
),
] = None,
category: Annotated[
str | None,
Field(
description="Category ID to assign to this automation. Use ha_config_get_category(scope='automation') to list available categories, or ha_config_set_category() to create one.",
default=None,
),
] = None,
wait: Annotated[
bool | str,
Field(
Expand Down Expand Up @@ -327,6 +371,7 @@ async def ha_config_set_automation(

OPTIONAL CONFIG FIELDS (Regular Automations):
- description: Detailed description of the user's intent (RECOMMENDED: helps safely modify implementation later)
- category: Category ID for organization (use ha_config_get_category to list, ha_config_set_category to create)
- condition: Additional conditions that must be met
- mode: 'single' (default), 'restart', 'queued', 'parallel'
- max: Maximum concurrent executions (for queued/parallel modes)
Expand Down Expand Up @@ -444,6 +489,11 @@ async def ha_config_set_automation(

config_dict = cast(dict[str, Any], parsed_config)

# Extract category before sending to HA REST API (which rejects unknown keys).
# Parameter takes precedence over config dict value.
config_category = config_dict.pop("category", None)
effective_category = category or config_category
Comment thread
kingpanther13 marked this conversation as resolved.
Outdated

# Normalize field names (triggers -> trigger, actions -> action, etc.)
config_dict = _normalize_automation_config(config_dict)

Expand Down Expand Up @@ -507,6 +557,19 @@ async def ha_config_set_automation(
except Exception as e:
result["warning"] = f"Automation created but verification failed: {e}"

# Apply category to entity registry if provided
if effective_category and entity_id:
try:
await client.send_websocket_message({
"type": "config/entity_registry/update",
"entity_id": entity_id,
"categories": {"automation": effective_category},
})
result["category"] = effective_category
except Exception as e:
logger.warning(f"Failed to set category for {entity_id}: {e}")
result["category_warning"] = f"Automation saved but failed to set category: {e}"

Comment thread
kingpanther13 marked this conversation as resolved.
if bp_warnings:
result["best_practice_warnings"] = bp_warnings

Expand Down Expand Up @@ -572,20 +635,7 @@ async def ha_config_remove_automation(
"""
try:
# Resolve entity_id for wait verification (identifier may be a unique_id)
entity_id_for_wait: str | None = None
if identifier.startswith("automation."):
entity_id_for_wait = identifier
else:
# Try to find entity_id by matching unique_id in automation states
try:
states = await client.get_states()
for state in states:
eid = state.get("entity_id", "")
if eid.startswith("automation.") and state.get("attributes", {}).get("id") == identifier:
entity_id_for_wait = eid
break
except Exception as e:
logger.warning(f"Could not resolve unique_id '{identifier}' to entity_id: {e} — wait verification will be skipped")
entity_id_for_wait = await _resolve_automation_entity_id(identifier)
Comment thread
kingpanther13 marked this conversation as resolved.

result = await client.delete_automation_config(identifier)

Expand Down
Loading