ha-mcp-dev 7.5.0.dev569__tar.gz → 7.5.0.dev571__tar.gz
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_dev-7.5.0.dev569/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.5.0.dev571}/PKG-INFO +1 -1
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/pyproject.toml +1 -1
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/client/rest_client.py +16 -4
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_categories.py +39 -3
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_config_automations.py +225 -92
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_config_dashboards.py +11 -2
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_config_scripts.py +189 -30
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_labels.py +93 -36
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_registry.py +16 -4
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_zones.py +113 -50
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/LICENSE +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/README.md +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/setup.cfg +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/settings_ui.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/smart_search.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/tests/test_env_manager.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ha-mcp-dev"
|
|
7
|
-
version = "7.5.0.
|
|
7
|
+
version = "7.5.0.dev571"
|
|
8
8
|
description = "Home Assistant MCP Server - Complete control of Home Assistant through MCP"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.13,<3.14"
|
|
@@ -7,6 +7,7 @@ import json
|
|
|
7
7
|
import logging
|
|
8
8
|
import os
|
|
9
9
|
import ssl
|
|
10
|
+
import time
|
|
10
11
|
from typing import Any
|
|
11
12
|
|
|
12
13
|
import httpx
|
|
@@ -963,12 +964,18 @@ class HomeAssistantClient:
|
|
|
963
964
|
) from e
|
|
964
965
|
raise
|
|
965
966
|
|
|
966
|
-
# 3-attempt × 6s upper-bound budget; first poll 0.
|
|
967
|
-
#
|
|
968
|
-
|
|
967
|
+
# 3-attempt × 6s upper-bound budget; first poll 0.025s is a 5×
|
|
968
|
+
# cushion above the ~4ms HA-Core entity-registration latency
|
|
969
|
+
# measured by ``test_poll_cadence_measurement.py`` (#1389 — p50
|
|
970
|
+
# 104.1-104.8 ms on the prior 0.1s first-poll, all from the sleep
|
|
971
|
+
# itself with ~4 ms of real registration work).
|
|
972
|
+
_POLL_CADENCE: tuple[float, ...] = (0.025, 1.0, 4.975)
|
|
969
973
|
|
|
970
974
|
async def _poll_for_automation_entity(self, unique_id: str) -> str | None:
|
|
971
975
|
"""Poll HA state to find the entity_id assigned to a newly created automation."""
|
|
976
|
+
# Measure cumulative elapsed from function entry to first successful match.
|
|
977
|
+
# Feeds the #1389 p50/p99 validation of `_POLL_CADENCE`.
|
|
978
|
+
start_monotonic = time.monotonic()
|
|
972
979
|
try:
|
|
973
980
|
for sleep_time in self._POLL_CADENCE:
|
|
974
981
|
await asyncio.sleep(sleep_time)
|
|
@@ -978,8 +985,13 @@ class HomeAssistantClient:
|
|
|
978
985
|
continue
|
|
979
986
|
if state.get("attributes", {}).get("id") == unique_id:
|
|
980
987
|
entity_id = state.get("entity_id")
|
|
988
|
+
elapsed_ms = (time.monotonic() - start_monotonic) * 1000.0
|
|
981
989
|
logger.debug(
|
|
982
|
-
|
|
990
|
+
"entity-registration-elapsed: %.1fms "
|
|
991
|
+
"(unique_id=%s, entity_id=%s)",
|
|
992
|
+
elapsed_ms,
|
|
993
|
+
unique_id,
|
|
994
|
+
entity_id,
|
|
983
995
|
)
|
|
984
996
|
return entity_id
|
|
985
997
|
except HomeAssistantError as e:
|
|
@@ -36,7 +36,11 @@ class CategoryTools:
|
|
|
36
36
|
@tool(
|
|
37
37
|
name="ha_config_get_category",
|
|
38
38
|
tags={"Labels & Categories"},
|
|
39
|
-
annotations={
|
|
39
|
+
annotations={
|
|
40
|
+
"idempotentHint": True,
|
|
41
|
+
"readOnlyHint": True,
|
|
42
|
+
"title": "Get Category",
|
|
43
|
+
},
|
|
40
44
|
)
|
|
41
45
|
@log_tool_usage
|
|
42
46
|
async def ha_config_get_category(
|
|
@@ -133,7 +137,7 @@ class CategoryTools:
|
|
|
133
137
|
available_ids = [cat.get("category_id") for cat in categories[:10]]
|
|
134
138
|
raise_tool_error(
|
|
135
139
|
create_error_response(
|
|
136
|
-
ErrorCode.
|
|
140
|
+
ErrorCode.RESOURCE_NOT_FOUND,
|
|
137
141
|
f"Category not found: {category_id}",
|
|
138
142
|
context={
|
|
139
143
|
"category_id": category_id,
|
|
@@ -247,6 +251,22 @@ class CategoryTools:
|
|
|
247
251
|
"message": f"Successfully {action_past} category: {name}",
|
|
248
252
|
}
|
|
249
253
|
else:
|
|
254
|
+
error_str = str(result.get("error", "")).lower()
|
|
255
|
+
if "not found" in error_str or "doesn't exist" in error_str:
|
|
256
|
+
raise_tool_error(
|
|
257
|
+
create_error_response(
|
|
258
|
+
ErrorCode.RESOURCE_NOT_FOUND,
|
|
259
|
+
f"Category not found: {category_id}",
|
|
260
|
+
context={
|
|
261
|
+
"name": name,
|
|
262
|
+
"scope": scope,
|
|
263
|
+
"category_id": category_id,
|
|
264
|
+
},
|
|
265
|
+
suggestions=[
|
|
266
|
+
f"Use ha_config_get_category('{scope}') without category_id to see all categories",
|
|
267
|
+
],
|
|
268
|
+
)
|
|
269
|
+
)
|
|
250
270
|
raise_tool_error(
|
|
251
271
|
create_error_response(
|
|
252
272
|
ErrorCode.SERVICE_CALL_FAILED,
|
|
@@ -276,7 +296,11 @@ class CategoryTools:
|
|
|
276
296
|
@tool(
|
|
277
297
|
name="ha_config_remove_category",
|
|
278
298
|
tags={"Labels & Categories"},
|
|
279
|
-
annotations={
|
|
299
|
+
annotations={
|
|
300
|
+
"destructiveHint": True,
|
|
301
|
+
"idempotentHint": True,
|
|
302
|
+
"title": "Remove Category",
|
|
303
|
+
},
|
|
280
304
|
)
|
|
281
305
|
@log_tool_usage
|
|
282
306
|
async def ha_config_remove_category(
|
|
@@ -333,6 +357,18 @@ class CategoryTools:
|
|
|
333
357
|
"message": f"Successfully deleted category: {category_id}",
|
|
334
358
|
}
|
|
335
359
|
else:
|
|
360
|
+
error_str = str(result.get("error", "")).lower()
|
|
361
|
+
if "not found" in error_str or "doesn't exist" in error_str:
|
|
362
|
+
raise_tool_error(
|
|
363
|
+
create_error_response(
|
|
364
|
+
ErrorCode.RESOURCE_NOT_FOUND,
|
|
365
|
+
f"Category not found: {category_id}",
|
|
366
|
+
context={"category_id": category_id, "scope": scope},
|
|
367
|
+
suggestions=[
|
|
368
|
+
f"Use ha_config_get_category('{scope}') without category_id to see all categories",
|
|
369
|
+
],
|
|
370
|
+
)
|
|
371
|
+
)
|
|
336
372
|
raise_tool_error(
|
|
337
373
|
create_error_response(
|
|
338
374
|
ErrorCode.SERVICE_CALL_FAILED,
|
{ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_config_automations.py
RENAMED
|
@@ -13,6 +13,7 @@ from fastmcp.tools import tool
|
|
|
13
13
|
from pydantic import Field
|
|
14
14
|
|
|
15
15
|
from ..client.rest_client import (
|
|
16
|
+
HomeAssistantAPIError,
|
|
16
17
|
HomeAssistantAuthError,
|
|
17
18
|
HomeAssistantConnectionError,
|
|
18
19
|
)
|
|
@@ -53,6 +54,17 @@ from .util_helpers import (
|
|
|
53
54
|
|
|
54
55
|
logger = logging.getLogger(__name__)
|
|
55
56
|
|
|
57
|
+
# Distinctive prefix of the soft-failure warning emitted by
|
|
58
|
+
# ``ha_config_set_automation`` when ``_poll_for_automation_entity``
|
|
59
|
+
# exhausts ``_POLL_CADENCE`` without matching the new automation.
|
|
60
|
+
# Exported so tests (e.g. ``test_poll_cadence_measurement.py``) can
|
|
61
|
+
# detect a missed registration without hard-coding the literal —
|
|
62
|
+
# rewording the warning becomes a compile-time coupling rather than
|
|
63
|
+
# a silent test drift.
|
|
64
|
+
NOT_VERIFIED_WARNING_PREFIX = (
|
|
65
|
+
"Automation was submitted to Home Assistant but the entity was not found"
|
|
66
|
+
)
|
|
67
|
+
|
|
56
68
|
|
|
57
69
|
def _normalize_automation_config(
|
|
58
70
|
config: Any,
|
|
@@ -279,7 +291,9 @@ class AutomationConfigTools:
|
|
|
279
291
|
):
|
|
280
292
|
return str(state["entity_id"])
|
|
281
293
|
except Exception as e:
|
|
282
|
-
logger.debug(
|
|
294
|
+
logger.debug(
|
|
295
|
+
f"Failed to resolve entity_id for automation {identifier}: {e}"
|
|
296
|
+
)
|
|
283
297
|
return None
|
|
284
298
|
|
|
285
299
|
@tool(
|
|
@@ -334,13 +348,17 @@ class AutomationConfigTools:
|
|
|
334
348
|
"Use ha_search_entities(domain_filter='automation') to list automations",
|
|
335
349
|
],
|
|
336
350
|
)
|
|
337
|
-
normalized_config, config_hash = await self._get_automation_config_internal(
|
|
351
|
+
normalized_config, config_hash = await self._get_automation_config_internal(
|
|
352
|
+
identifier
|
|
353
|
+
)
|
|
338
354
|
|
|
339
355
|
# Resolve entity_id and fetch category from entity registry
|
|
340
356
|
# (injected after hash so transient registry failures don't affect the hash)
|
|
341
357
|
entity_id = await self._resolve_automation_entity_id(identifier)
|
|
342
358
|
if entity_id:
|
|
343
|
-
cat_id = await fetch_entity_category(
|
|
359
|
+
cat_id = await fetch_entity_category(
|
|
360
|
+
self._client, entity_id, "automation"
|
|
361
|
+
)
|
|
344
362
|
if cat_id:
|
|
345
363
|
normalized_config["category"] = cat_id
|
|
346
364
|
|
|
@@ -644,7 +662,10 @@ class AutomationConfigTools:
|
|
|
644
662
|
"Provide the automation entity_id or unique_id",
|
|
645
663
|
"Use ha_search_entities(domain_filter='automation') to find automations",
|
|
646
664
|
],
|
|
647
|
-
context={
|
|
665
|
+
context={
|
|
666
|
+
"action": "python_transform",
|
|
667
|
+
"identifier": identifier,
|
|
668
|
+
},
|
|
648
669
|
)
|
|
649
670
|
)
|
|
650
671
|
if config_hash is None:
|
|
@@ -656,7 +677,10 @@ class AutomationConfigTools:
|
|
|
656
677
|
"Call ha_config_get_automation() first",
|
|
657
678
|
"Use the config_hash from that response",
|
|
658
679
|
],
|
|
659
|
-
context={
|
|
680
|
+
context={
|
|
681
|
+
"action": "python_transform",
|
|
682
|
+
"identifier": identifier,
|
|
683
|
+
},
|
|
660
684
|
)
|
|
661
685
|
)
|
|
662
686
|
|
|
@@ -675,7 +699,10 @@ class AutomationConfigTools:
|
|
|
675
699
|
ErrorCode.VALIDATION_FAILED,
|
|
676
700
|
message,
|
|
677
701
|
suggestions=suggestions,
|
|
678
|
-
context={
|
|
702
|
+
context={
|
|
703
|
+
"action": "python_transform",
|
|
704
|
+
"identifier": identifier,
|
|
705
|
+
},
|
|
679
706
|
)
|
|
680
707
|
)
|
|
681
708
|
|
|
@@ -698,11 +725,20 @@ class AutomationConfigTools:
|
|
|
698
725
|
|
|
699
726
|
# Re-apply category if present
|
|
700
727
|
entity_id = result.get("entity_id")
|
|
701
|
-
if
|
|
728
|
+
if (
|
|
729
|
+
not entity_id
|
|
730
|
+
and identifier
|
|
731
|
+
and identifier.startswith("automation.")
|
|
732
|
+
):
|
|
702
733
|
entity_id = identifier
|
|
703
734
|
if transform_category and entity_id:
|
|
704
735
|
await apply_entity_category(
|
|
705
|
-
self._client,
|
|
736
|
+
self._client,
|
|
737
|
+
entity_id,
|
|
738
|
+
transform_category,
|
|
739
|
+
"automation",
|
|
740
|
+
result,
|
|
741
|
+
"automation",
|
|
706
742
|
)
|
|
707
743
|
|
|
708
744
|
response: dict[str, Any] = {
|
|
@@ -762,12 +798,14 @@ class AutomationConfigTools:
|
|
|
762
798
|
self._client, config_dict
|
|
763
799
|
)
|
|
764
800
|
|
|
765
|
-
result = await self._client.upsert_automation_config(
|
|
801
|
+
result = await self._client.upsert_automation_config(
|
|
802
|
+
config_dict, identifier
|
|
803
|
+
)
|
|
766
804
|
|
|
767
805
|
# If the client could not verify the entity was registered, warn but don't hard-fail.
|
|
768
806
|
if result.get("entity_not_verified"):
|
|
769
807
|
result.setdefault("warnings", []).append(
|
|
770
|
-
"
|
|
808
|
+
f"{NOT_VERIFIED_WARNING_PREFIX} "
|
|
771
809
|
"after polling. The automation may still have been created -- check Home "
|
|
772
810
|
"Assistant logs and try reloading automations. Common causes: "
|
|
773
811
|
"automations.yaml vs automation.yaml filename mismatch, invalid config "
|
|
@@ -784,7 +822,9 @@ class AutomationConfigTools:
|
|
|
784
822
|
if wait_bool and entity_id:
|
|
785
823
|
action_word = "created" if identifier is None else "updated"
|
|
786
824
|
try:
|
|
787
|
-
registered = await wait_for_entity_registered(
|
|
825
|
+
registered = await wait_for_entity_registered(
|
|
826
|
+
self._client, entity_id
|
|
827
|
+
)
|
|
788
828
|
if not registered:
|
|
789
829
|
result.setdefault("warnings", []).append(
|
|
790
830
|
f"Automation {action_word} but {entity_id} not yet queryable. "
|
|
@@ -798,7 +838,12 @@ class AutomationConfigTools:
|
|
|
798
838
|
# Apply category to entity registry if provided
|
|
799
839
|
if effective_category and entity_id:
|
|
800
840
|
await apply_entity_category(
|
|
801
|
-
self._client,
|
|
841
|
+
self._client,
|
|
842
|
+
entity_id,
|
|
843
|
+
effective_category,
|
|
844
|
+
"automation",
|
|
845
|
+
result,
|
|
846
|
+
"automation",
|
|
802
847
|
)
|
|
803
848
|
|
|
804
849
|
if bp_warnings:
|
|
@@ -820,6 +865,13 @@ class AutomationConfigTools:
|
|
|
820
865
|
except ToolError:
|
|
821
866
|
raise
|
|
822
867
|
except Exception as e:
|
|
868
|
+
# 404 during update only — create (identifier=None) never hits this branch.
|
|
869
|
+
if (
|
|
870
|
+
identifier
|
|
871
|
+
and isinstance(e, HomeAssistantAPIError)
|
|
872
|
+
and e.status_code == 404
|
|
873
|
+
):
|
|
874
|
+
await self._raise_automation_not_found(identifier)
|
|
823
875
|
suggestions = [
|
|
824
876
|
"Check automation configuration format",
|
|
825
877
|
"Ensure required fields: alias, trigger, action",
|
|
@@ -838,6 +890,57 @@ class AutomationConfigTools:
|
|
|
838
890
|
suggestions=suggestions,
|
|
839
891
|
)
|
|
840
892
|
|
|
893
|
+
async def _list_automation_entity_ids(self) -> list[str]:
|
|
894
|
+
"""Best-effort list of automation entity_ids (up to 10) from the entity registry.
|
|
895
|
+
|
|
896
|
+
Used to populate ``available_automation_ids`` in RESOURCE_NOT_FOUND
|
|
897
|
+
error context. Returns an empty list on any failure — caller treats
|
|
898
|
+
absence as "no IDs to report" rather than failing the structured
|
|
899
|
+
error raise. The 10-entry cap lives here (not at the callers) so a
|
|
900
|
+
new call site can't accidentally bloat the error payload.
|
|
901
|
+
"""
|
|
902
|
+
try:
|
|
903
|
+
result = await self._client.send_websocket_message(
|
|
904
|
+
{"type": "config/entity_registry/list"}
|
|
905
|
+
)
|
|
906
|
+
except Exception as e:
|
|
907
|
+
logger.debug("Failed to list automation entity_ids from registry: %s", e)
|
|
908
|
+
return []
|
|
909
|
+
entries = result.get("result", []) if isinstance(result, dict) else result
|
|
910
|
+
if not isinstance(entries, list):
|
|
911
|
+
return []
|
|
912
|
+
return [
|
|
913
|
+
entry["entity_id"]
|
|
914
|
+
for entry in entries
|
|
915
|
+
if isinstance(entry, dict)
|
|
916
|
+
and isinstance(entry.get("entity_id"), str)
|
|
917
|
+
and entry["entity_id"].startswith("automation.")
|
|
918
|
+
][:10]
|
|
919
|
+
|
|
920
|
+
async def _raise_automation_not_found(self, identifier: str) -> None:
|
|
921
|
+
"""Raise a structured RESOURCE_NOT_FOUND ToolError for a missing automation.
|
|
922
|
+
|
|
923
|
+
Single source of truth for the 404→RESOURCE_NOT_FOUND mapping used
|
|
924
|
+
by both the GET path (``_get_automation_config_internal``) and the
|
|
925
|
+
mutation paths (``ha_config_set_automation`` update branch,
|
|
926
|
+
``ha_config_remove_automation``). Populates
|
|
927
|
+
``available_automation_ids`` (up to 10) from the entity registry.
|
|
928
|
+
"""
|
|
929
|
+
available_ids = await self._list_automation_entity_ids()
|
|
930
|
+
raise_tool_error(
|
|
931
|
+
create_error_response(
|
|
932
|
+
ErrorCode.RESOURCE_NOT_FOUND,
|
|
933
|
+
f"Automation not found: {identifier}",
|
|
934
|
+
context={
|
|
935
|
+
"automation_id": identifier,
|
|
936
|
+
"available_automation_ids": available_ids,
|
|
937
|
+
},
|
|
938
|
+
suggestions=[
|
|
939
|
+
"Use ha_search_entities(domain_filter='automation') to find existing automations"
|
|
940
|
+
],
|
|
941
|
+
)
|
|
942
|
+
)
|
|
943
|
+
|
|
841
944
|
async def _get_automation_config_internal(
|
|
842
945
|
self, identifier: str
|
|
843
946
|
) -> tuple[dict[str, Any], str]:
|
|
@@ -845,8 +948,18 @@ class AutomationConfigTools:
|
|
|
845
948
|
|
|
846
949
|
Returns (normalized_config, config_hash) tuple.
|
|
847
950
|
Used internally by _fetch_and_verify_hash and ha_config_get_automation.
|
|
951
|
+
|
|
952
|
+
Raises a structured ``RESOURCE_NOT_FOUND`` ToolError via
|
|
953
|
+
``_raise_automation_not_found`` when the REST client returns 404.
|
|
954
|
+
Other ``HomeAssistantAPIError`` instances propagate unchanged to
|
|
955
|
+
caller exception handlers (``exception_to_structured_error`` route).
|
|
848
956
|
"""
|
|
849
|
-
|
|
957
|
+
try:
|
|
958
|
+
config_result = await self._client.get_automation_config(identifier)
|
|
959
|
+
except HomeAssistantAPIError as e:
|
|
960
|
+
if e.status_code == 404:
|
|
961
|
+
await self._raise_automation_not_found(identifier)
|
|
962
|
+
raise
|
|
850
963
|
normalized_config = _normalize_config_for_roundtrip(config_result)
|
|
851
964
|
config_hash_value = compute_config_hash(normalized_config)
|
|
852
965
|
return normalized_config, config_hash_value
|
|
@@ -859,7 +972,9 @@ class AutomationConfigTools:
|
|
|
859
972
|
Returns the current normalized config dict.
|
|
860
973
|
Raises ToolError if the hash does not match (conflict).
|
|
861
974
|
"""
|
|
862
|
-
current_config, current_hash = await self._get_automation_config_internal(
|
|
975
|
+
current_config, current_hash = await self._get_automation_config_internal(
|
|
976
|
+
identifier
|
|
977
|
+
)
|
|
863
978
|
if current_hash != config_hash:
|
|
864
979
|
raise_tool_error(
|
|
865
980
|
create_error_response(
|
|
@@ -880,22 +995,26 @@ class AutomationConfigTools:
|
|
|
880
995
|
try:
|
|
881
996
|
parsed_config = parse_json_param(config, "config")
|
|
882
997
|
except ValueError as e:
|
|
883
|
-
raise_tool_error(
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
998
|
+
raise_tool_error(
|
|
999
|
+
create_error_response(
|
|
1000
|
+
code=ErrorCode.VALIDATION_INVALID_JSON,
|
|
1001
|
+
message=f"Invalid config parameter: {e}",
|
|
1002
|
+
suggestions=[
|
|
1003
|
+
"Pass 'config' as a dict, not a JSON string, to avoid escaping issues.",
|
|
1004
|
+
"Check for JSON syntax errors: unquoted keys, trailing commas, or invalid escape sequences.",
|
|
1005
|
+
],
|
|
1006
|
+
context={"parameter": "config"},
|
|
1007
|
+
)
|
|
1008
|
+
)
|
|
892
1009
|
|
|
893
1010
|
if parsed_config is None or not isinstance(parsed_config, dict):
|
|
894
|
-
raise_tool_error(
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
1011
|
+
raise_tool_error(
|
|
1012
|
+
create_validation_error(
|
|
1013
|
+
"Config parameter must be a JSON object",
|
|
1014
|
+
parameter="config",
|
|
1015
|
+
details=f"Received type: {type(parsed_config).__name__}",
|
|
1016
|
+
)
|
|
1017
|
+
)
|
|
899
1018
|
|
|
900
1019
|
return cast(dict[str, Any], parsed_config)
|
|
901
1020
|
|
|
@@ -924,24 +1043,28 @@ class AutomationConfigTools:
|
|
|
924
1043
|
context: dict[str, Any] = {"missing_fields": missing_fields}
|
|
925
1044
|
if identifier:
|
|
926
1045
|
context["identifier"] = identifier
|
|
927
|
-
raise_tool_error(
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1046
|
+
raise_tool_error(
|
|
1047
|
+
create_error_response(
|
|
1048
|
+
code=ErrorCode.CONFIG_MISSING_REQUIRED_FIELDS,
|
|
1049
|
+
message=f"Missing required fields: {', '.join(missing_fields)}",
|
|
1050
|
+
details=(
|
|
1051
|
+
"Config contains 'sequence', which belongs to scripts. "
|
|
1052
|
+
"Automations use 'trigger' and 'action'; scripts use 'sequence'."
|
|
1053
|
+
),
|
|
1054
|
+
suggestions=[
|
|
1055
|
+
"Did you mean ha_config_set_script? Scripts use 'sequence' directly.",
|
|
1056
|
+
"For an automation, replace 'sequence' with 'action' and add a 'trigger'.",
|
|
1057
|
+
],
|
|
1058
|
+
context=context,
|
|
1059
|
+
)
|
|
1060
|
+
)
|
|
1061
|
+
raise_tool_error(
|
|
1062
|
+
create_config_error(
|
|
1063
|
+
f"Missing required fields: {', '.join(missing_fields)}",
|
|
1064
|
+
identifier=identifier,
|
|
1065
|
+
missing_fields=missing_fields,
|
|
1066
|
+
)
|
|
1067
|
+
)
|
|
945
1068
|
|
|
946
1069
|
# Issue #1169: reject configs that wrap ``scene.create`` in an
|
|
947
1070
|
# automation with no functional trigger. Models occasionally produce
|
|
@@ -966,31 +1089,33 @@ class AutomationConfigTools:
|
|
|
966
1089
|
if _action_contains_scene_create(a)
|
|
967
1090
|
]
|
|
968
1091
|
if scene_create_indices:
|
|
969
|
-
raise_tool_error(
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1092
|
+
raise_tool_error(
|
|
1093
|
+
create_error_response(
|
|
1094
|
+
code=ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
1095
|
+
message=(
|
|
1096
|
+
"Empty trigger paired with a scene.create action — "
|
|
1097
|
+
"this automation can never fire. For a state snapshot "
|
|
1098
|
+
"of one or more entities, use ha_config_set_scene "
|
|
1099
|
+
"directly instead of wrapping scene.create in an "
|
|
1100
|
+
"automation."
|
|
1101
|
+
),
|
|
1102
|
+
suggestions=[
|
|
1103
|
+
"ha_config_set_scene(scene_id='...', config={'name': "
|
|
1104
|
+
"'...', 'entities': {'<entity_id>': {...}}}) creates "
|
|
1105
|
+
"a scene without a trigger.",
|
|
1106
|
+
"If the snapshot really should be the result of an "
|
|
1107
|
+
"event, add the trigger that should fire it and keep "
|
|
1108
|
+
"the automation.",
|
|
1109
|
+
"For a state-derived value that recomputes when its "
|
|
1110
|
+
"inputs change, use "
|
|
1111
|
+
"ha_config_set_helper(helper_type='template') instead.",
|
|
1112
|
+
],
|
|
1113
|
+
context={
|
|
1114
|
+
"scene_create_action_indices": scene_create_indices,
|
|
1115
|
+
"identifier": identifier,
|
|
1116
|
+
},
|
|
1117
|
+
)
|
|
1118
|
+
)
|
|
994
1119
|
|
|
995
1120
|
# HA accepts conditions with 'platform' (trigger syntax) but then crashes
|
|
996
1121
|
# with an unhelpful 500 rather than a 400 validation error.
|
|
@@ -998,30 +1123,34 @@ class AutomationConfigTools:
|
|
|
998
1123
|
if not isinstance(cond, dict):
|
|
999
1124
|
continue
|
|
1000
1125
|
if "platform" in cond and "condition" not in cond:
|
|
1001
|
-
raise_tool_error(
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1126
|
+
raise_tool_error(
|
|
1127
|
+
create_error_response(
|
|
1128
|
+
code=ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
1129
|
+
message=(
|
|
1130
|
+
f"Condition at index {idx} uses 'platform' (trigger syntax). "
|
|
1131
|
+
"Conditions use 'condition', not 'platform'."
|
|
1132
|
+
),
|
|
1133
|
+
suggestions=[
|
|
1134
|
+
f"Replace 'platform' with 'condition': "
|
|
1135
|
+
f"{{'condition': '{cond['platform']}', ...}}",
|
|
1136
|
+
"Triggers use 'platform'; conditions use 'condition'.",
|
|
1137
|
+
],
|
|
1138
|
+
context={"condition_index": idx, "found_key": "platform"},
|
|
1139
|
+
)
|
|
1140
|
+
)
|
|
1014
1141
|
|
|
1015
1142
|
# Prevent duplicate creation when config contains an existing automation id
|
|
1016
1143
|
if identifier is None and "id" in config_dict:
|
|
1017
1144
|
existing_id = config_dict["id"]
|
|
1018
|
-
raise_tool_error(
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1145
|
+
raise_tool_error(
|
|
1146
|
+
create_validation_error(
|
|
1147
|
+
f"Config contains 'id' field ('{existing_id}') but no identifier was provided. "
|
|
1148
|
+
"This would create a duplicate automation instead of updating the existing one.",
|
|
1149
|
+
parameter="identifier",
|
|
1150
|
+
details=f"To update, pass identifier='{existing_id}' (or the automation's entity_id). "
|
|
1151
|
+
"To create a genuinely new automation, remove the 'id' field from the config.",
|
|
1152
|
+
)
|
|
1153
|
+
)
|
|
1025
1154
|
|
|
1026
1155
|
@tool(
|
|
1027
1156
|
name="ha_config_remove_automation",
|
|
@@ -1086,7 +1215,9 @@ class AutomationConfigTools:
|
|
|
1086
1215
|
wait_bool = coerce_bool_param(wait, "wait", default=True)
|
|
1087
1216
|
if wait_bool and entity_id_for_wait:
|
|
1088
1217
|
try:
|
|
1089
|
-
removed = await wait_for_entity_removed(
|
|
1218
|
+
removed = await wait_for_entity_removed(
|
|
1219
|
+
self._client, entity_id_for_wait
|
|
1220
|
+
)
|
|
1090
1221
|
if not removed:
|
|
1091
1222
|
result.setdefault("warnings", []).append(
|
|
1092
1223
|
f"Deletion confirmed by API but {entity_id_for_wait} may still appear briefly."
|
|
@@ -1107,6 +1238,8 @@ class AutomationConfigTools:
|
|
|
1107
1238
|
except ToolError:
|
|
1108
1239
|
raise
|
|
1109
1240
|
except Exception as e:
|
|
1241
|
+
if isinstance(e, HomeAssistantAPIError) and e.status_code == 404:
|
|
1242
|
+
await self._raise_automation_not_found(identifier)
|
|
1110
1243
|
exception_to_structured_error(
|
|
1111
1244
|
e,
|
|
1112
1245
|
context={"identifier": identifier, "action": "delete"},
|
{ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_config_dashboards.py
RENAMED
|
@@ -1530,8 +1530,13 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
|
|
|
1530
1530
|
],
|
|
1531
1531
|
context={"action": "delete"},
|
|
1532
1532
|
)
|
|
1533
|
-
resolved,
|
|
1533
|
+
resolved, dashboards = await _resolve_dashboard(client, url_path)
|
|
1534
1534
|
if resolved is None:
|
|
1535
|
+
available_ids = [
|
|
1536
|
+
d.get("url_path")
|
|
1537
|
+
for d in (dashboards or [])[:10]
|
|
1538
|
+
if d.get("url_path")
|
|
1539
|
+
]
|
|
1535
1540
|
raise_tool_error(
|
|
1536
1541
|
create_error_response(
|
|
1537
1542
|
ErrorCode.RESOURCE_NOT_FOUND,
|
|
@@ -1541,7 +1546,11 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
|
|
|
1541
1546
|
"Use ha_config_get_dashboard(list_only=True) to see available dashboards",
|
|
1542
1547
|
"YAML-mode and default dashboards are not deletable via this tool",
|
|
1543
1548
|
],
|
|
1544
|
-
context={
|
|
1549
|
+
context={
|
|
1550
|
+
"action": "delete",
|
|
1551
|
+
"url_path": url_path,
|
|
1552
|
+
"available_dashboard_ids": available_ids,
|
|
1553
|
+
},
|
|
1545
1554
|
)
|
|
1546
1555
|
)
|
|
1547
1556
|
resolved_id = resolved["id"]
|