Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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() to list available categories.', default=None)]",
Comment thread
kingpanther13 marked this conversation as resolved.
Outdated
"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
55 changes: 55 additions & 0 deletions src/ha_mcp/tools/tools_config_automations.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,35 @@ 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 = identifier if identifier.startswith("automation.") else None
if not entity_id:
try:
states = await client.get_states()
for state in states:
if (
state.get("entity_id", "").startswith("automation.")
and state.get("attributes", {}).get("id") == identifier
):
entity_id = state["entity_id"]
break
Comment thread
kingpanther13 marked this conversation as resolved.
Outdated
except Exception as e:
logger.debug(f"Failed to resolve entity_id for automation {identifier}: {e}")

Comment thread
kingpanther13 marked this conversation as resolved.
Outdated
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}")

return {
"success": True,
"action": "get",
Expand Down Expand Up @@ -296,6 +325,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 +363,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 +481,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 +549,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
23 changes: 19 additions & 4 deletions src/ha_mcp/tools/tools_config_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,13 @@ async def ha_config_set_helper(
default=None,
),
] = None,
category: Annotated[
str | None,
Field(
description="Category ID to assign to this helper. Use ha_config_get_category() to list available categories.",
default=None,
),
] = None,
Comment thread
kingpanther13 marked this conversation as resolved.
wait: Annotated[
bool | str,
Field(
Expand Down Expand Up @@ -643,8 +650,8 @@ async def ha_config_set_helper(
except Exception as e:
helper_data["warning"] = f"Helper created but verification failed: {e}"

# Update entity registry if area_id or labels specified
if (area_id or labels) and entity_id:
# Update entity registry if area_id, labels, or category specified
if (area_id or labels or category) and entity_id:
update_message: dict[str, Any] = {
"type": "config/entity_registry/update",
"entity_id": entity_id,
Expand All @@ -653,13 +660,17 @@ async def ha_config_set_helper(
update_message["area_id"] = area_id
if labels:
update_message["labels"] = labels
if category:
update_message["categories"] = {"helpers": category}
Comment thread
kingpanther13 marked this conversation as resolved.
Outdated

update_result = await client.send_websocket_message(
update_message
)
if update_result.get("success"):
Comment thread
kingpanther13 marked this conversation as resolved.
helper_data["area_id"] = area_id
helper_data["labels"] = labels
if category:
Comment thread
kingpanther13 marked this conversation as resolved.
Outdated
helper_data["category"] = category

return {
"success": True,
Expand Down Expand Up @@ -878,8 +889,8 @@ async def ha_config_set_helper(
))
updated_data = result.get("result", {})

# Also update entity registry for icon, area, and labels
if icon or area_id or labels:
# Also update entity registry for icon, area, labels, and category
if icon or area_id or labels or category:
registry_update: dict[str, Any] = {
"type": "config/entity_registry/update",
"entity_id": entity_id,
Expand All @@ -890,6 +901,8 @@ async def ha_config_set_helper(
registry_update["area_id"] = area_id
if labels:
registry_update["labels"] = labels
if category:
registry_update["categories"] = {"helpers": category}
await client.send_websocket_message(registry_update)
Comment thread
kingpanther13 marked this conversation as resolved.
Outdated

else:
Expand All @@ -907,6 +920,8 @@ async def ha_config_set_helper(
update_msg["area_id"] = area_id
if labels:
update_msg["labels"] = labels
if category:
update_msg["categories"] = {"helpers": category}

result = await client.send_websocket_message(update_msg)

Expand Down
Loading