ha-mcp-dev 7.5.0.dev570__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.dev570/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.5.0.dev571}/PKG-INFO +1 -1
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/pyproject.toml +1 -1
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_categories.py +39 -3
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_config_automations.py +72 -1
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_config_dashboards.py +11 -2
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_config_scripts.py +189 -30
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_labels.py +93 -36
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_registry.py +16 -4
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_zones.py +113 -50
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/LICENSE +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/README.md +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/setup.cfg +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.5.0.dev570 → 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.dev570 → 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.dev570 → 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.dev570 → 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.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.5.0.dev570 → 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.dev570 → 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.dev570 → 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.dev570 → 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.dev570 → 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.dev570 → 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.dev570 → 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.dev570 → 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.dev570 → 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.dev570 → 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.dev570 → 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.dev570 → 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.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/settings_ui.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/smart_search.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.5.0.dev570 → 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"
|
|
@@ -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.dev570 → 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
|
)
|
|
@@ -864,6 +865,13 @@ class AutomationConfigTools:
|
|
|
864
865
|
except ToolError:
|
|
865
866
|
raise
|
|
866
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)
|
|
867
875
|
suggestions = [
|
|
868
876
|
"Check automation configuration format",
|
|
869
877
|
"Ensure required fields: alias, trigger, action",
|
|
@@ -882,6 +890,57 @@ class AutomationConfigTools:
|
|
|
882
890
|
suggestions=suggestions,
|
|
883
891
|
)
|
|
884
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
|
+
|
|
885
944
|
async def _get_automation_config_internal(
|
|
886
945
|
self, identifier: str
|
|
887
946
|
) -> tuple[dict[str, Any], str]:
|
|
@@ -889,8 +948,18 @@ class AutomationConfigTools:
|
|
|
889
948
|
|
|
890
949
|
Returns (normalized_config, config_hash) tuple.
|
|
891
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).
|
|
892
956
|
"""
|
|
893
|
-
|
|
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
|
|
894
963
|
normalized_config = _normalize_config_for_roundtrip(config_result)
|
|
895
964
|
config_hash_value = compute_config_hash(normalized_config)
|
|
896
965
|
return normalized_config, config_hash_value
|
|
@@ -1169,6 +1238,8 @@ class AutomationConfigTools:
|
|
|
1169
1238
|
except ToolError:
|
|
1170
1239
|
raise
|
|
1171
1240
|
except Exception as e:
|
|
1241
|
+
if isinstance(e, HomeAssistantAPIError) and e.status_code == 404:
|
|
1242
|
+
await self._raise_automation_not_found(identifier)
|
|
1172
1243
|
exception_to_structured_error(
|
|
1173
1244
|
e,
|
|
1174
1245
|
context={"identifier": identifier, "action": "delete"},
|
{ha_mcp_dev-7.5.0.dev570 → 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"]
|
{ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_config_scripts.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
|
)
|
|
@@ -90,7 +91,10 @@ class ConfigScriptTools:
|
|
|
90
91
|
async def ha_config_get_script(
|
|
91
92
|
self,
|
|
92
93
|
script_id: Annotated[
|
|
93
|
-
str,
|
|
94
|
+
str,
|
|
95
|
+
Field(
|
|
96
|
+
description="Script identifier — bare storage key ('morning_routine') or entity_id form ('script.morning_routine'); a leading 'script.' prefix is stripped before lookup."
|
|
97
|
+
),
|
|
94
98
|
],
|
|
95
99
|
) -> dict[str, Any]:
|
|
96
100
|
"""
|
|
@@ -100,15 +104,28 @@ class ConfigScriptTools:
|
|
|
100
104
|
|
|
101
105
|
The returned `config_hash` is stable across consecutive reads of an unchanged config — `compute_config_hash` documents the underlying contract.
|
|
102
106
|
|
|
103
|
-
The returned `script_id` is the canonical storage key resolved by the REST client (matching what `ha_config_set_script` / `ha_config_remove_script` expect), falling back to the input identifier on the rare path where the REST envelope omits it.
|
|
107
|
+
The returned `script_id` is the canonical bare storage key resolved by the REST client (matching what `ha_config_set_script` / `ha_config_remove_script` expect), falling back to the input identifier on the rare path where the REST envelope omits it. A leading `script.` prefix on the input is stripped before lookup — behavioral parity with `ha_config_get_automation` (mechanism differs: automations resolve via state lookup; scripts strip the prefix).
|
|
104
108
|
|
|
105
109
|
EXAMPLES:
|
|
106
|
-
- Get script: ha_config_get_script("morning_routine")
|
|
107
|
-
- Get script: ha_config_get_script("
|
|
110
|
+
- Get script (bare form): ha_config_get_script("morning_routine")
|
|
111
|
+
- Get script (entity_id form): ha_config_get_script("script.morning_routine")
|
|
108
112
|
|
|
109
113
|
For detailed script configuration help, use ha_get_skill_guide.
|
|
110
114
|
"""
|
|
111
115
|
try:
|
|
116
|
+
# Strip BEFORE validate so a bare ``"script."`` (empty after
|
|
117
|
+
# strip) is rejected as ``VALIDATION_INVALID_PARAMETER`` rather
|
|
118
|
+
# than slipping through validate (non-empty pre-strip) and
|
|
119
|
+
# 404-ing at ``get_script_config("")``. Accept entity_id form
|
|
120
|
+
# (``script.foo``) and bare storage key (``foo``) — behavioral
|
|
121
|
+
# parity with ``ha_config_get_automation`` (mechanism differs:
|
|
122
|
+
# automations resolve via state lookup; scripts strip the
|
|
123
|
+
# prefix). ``_raise_script_not_found`` suggests
|
|
124
|
+
# ``ha_search_entities(domain_filter='script')`` which returns
|
|
125
|
+
# entity_ids; without this strip, feeding that output back into
|
|
126
|
+
# the GET tool fails and reseeds the wrong-tool spiral that
|
|
127
|
+
# #1297 closes.
|
|
128
|
+
script_id = script_id.removeprefix("script.")
|
|
112
129
|
# Empty/whitespace script_id would propagate to
|
|
113
130
|
# ``get_script_config`` and surface as a misleading
|
|
114
131
|
# ``RESOURCE_NOT_FOUND``. Extension of the #1312
|
|
@@ -122,7 +139,7 @@ class ConfigScriptTools:
|
|
|
122
139
|
"Use ha_search_entities(domain_filter='script') to list scripts",
|
|
123
140
|
],
|
|
124
141
|
)
|
|
125
|
-
config_result = await self.
|
|
142
|
+
config_result = await self._fetch_script_config_envelope(script_id)
|
|
126
143
|
# Extract actual script config body and compute hash before category injection
|
|
127
144
|
actual_config = config_result.get("config", config_result)
|
|
128
145
|
config_hash_value = compute_config_hash(actual_config)
|
|
@@ -170,6 +187,76 @@ class ConfigScriptTools:
|
|
|
170
187
|
],
|
|
171
188
|
)
|
|
172
189
|
|
|
190
|
+
async def _list_script_entity_ids(self) -> list[str]:
|
|
191
|
+
"""Best-effort list of bare script IDs (up to 10) from the entity registry.
|
|
192
|
+
|
|
193
|
+
Returns the bare storage keys (e.g. ``morning_routine``), stripping
|
|
194
|
+
the ``script.`` entity_id prefix — ``ha_config_get_script`` /
|
|
195
|
+
``ha_config_set_script`` / ``ha_config_remove_script`` all take the
|
|
196
|
+
bare form, so the entity_id prefix would force callers to strip it
|
|
197
|
+
before retry. Returns an empty list on any failure — caller treats
|
|
198
|
+
absence as "no IDs to report" rather than failing the structured
|
|
199
|
+
error raise. The 10-entry cap lives here (not at the callers) so a
|
|
200
|
+
new call site can't accidentally bloat the error payload.
|
|
201
|
+
"""
|
|
202
|
+
try:
|
|
203
|
+
result = await self._client.send_websocket_message(
|
|
204
|
+
{"type": "config/entity_registry/list"}
|
|
205
|
+
)
|
|
206
|
+
except Exception as e:
|
|
207
|
+
logger.debug("Failed to list script entity_ids from registry: %s", e)
|
|
208
|
+
return []
|
|
209
|
+
entries = result.get("result", []) if isinstance(result, dict) else result
|
|
210
|
+
if not isinstance(entries, list):
|
|
211
|
+
return []
|
|
212
|
+
return [
|
|
213
|
+
entry["entity_id"][len("script.") :]
|
|
214
|
+
for entry in entries
|
|
215
|
+
if isinstance(entry, dict)
|
|
216
|
+
and isinstance(entry.get("entity_id"), str)
|
|
217
|
+
and entry["entity_id"].startswith("script.")
|
|
218
|
+
][:10]
|
|
219
|
+
|
|
220
|
+
async def _raise_script_not_found(self, script_id: str) -> None:
|
|
221
|
+
"""Raise a structured RESOURCE_NOT_FOUND ToolError for a missing script.
|
|
222
|
+
|
|
223
|
+
Single source of truth for the 404→RESOURCE_NOT_FOUND mapping used
|
|
224
|
+
by the GET path (``_fetch_script_config_envelope``) and the
|
|
225
|
+
mutation paths (``ha_config_set_script`` update branch,
|
|
226
|
+
``ha_config_remove_script``). Populates ``available_script_ids``
|
|
227
|
+
(up to 10 bare IDs) from the entity registry.
|
|
228
|
+
"""
|
|
229
|
+
available_ids = await self._list_script_entity_ids()
|
|
230
|
+
raise_tool_error(
|
|
231
|
+
create_error_response(
|
|
232
|
+
ErrorCode.RESOURCE_NOT_FOUND,
|
|
233
|
+
f"Script not found: {script_id}",
|
|
234
|
+
context={
|
|
235
|
+
"script_id": script_id,
|
|
236
|
+
"available_script_ids": available_ids,
|
|
237
|
+
},
|
|
238
|
+
suggestions=[
|
|
239
|
+
"Use ha_search_entities(domain_filter='script') to find existing scripts"
|
|
240
|
+
],
|
|
241
|
+
)
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
async def _fetch_script_config_envelope(self, script_id: str) -> dict[str, Any]:
|
|
245
|
+
"""Fetch the raw REST envelope, mapping 404 to RESOURCE_NOT_FOUND.
|
|
246
|
+
|
|
247
|
+
Returns the dict envelope from ``rest_client.get_script_config``
|
|
248
|
+
(``success``/``script_id``/``config`` keys). Raises a structured
|
|
249
|
+
``RESOURCE_NOT_FOUND`` ToolError via ``_raise_script_not_found`` on
|
|
250
|
+
404. Other ``HomeAssistantAPIError`` instances propagate unchanged
|
|
251
|
+
to caller exception handlers.
|
|
252
|
+
"""
|
|
253
|
+
try:
|
|
254
|
+
return cast(dict[str, Any], await self._client.get_script_config(script_id))
|
|
255
|
+
except HomeAssistantAPIError as e:
|
|
256
|
+
if e.status_code == 404:
|
|
257
|
+
await self._raise_script_not_found(script_id)
|
|
258
|
+
raise
|
|
259
|
+
|
|
173
260
|
async def _get_script_config_internal(
|
|
174
261
|
self, script_id: str
|
|
175
262
|
) -> tuple[dict[str, Any], str]:
|
|
@@ -178,8 +265,11 @@ class ConfigScriptTools:
|
|
|
178
265
|
Returns (actual_config, config_hash) tuple where actual_config is
|
|
179
266
|
the inner script body (not the REST wrapper).
|
|
180
267
|
Used internally by _fetch_and_verify_hash and ha_config_get_script.
|
|
268
|
+
|
|
269
|
+
404 responses from the REST client are mapped to a structured
|
|
270
|
+
``RESOURCE_NOT_FOUND`` ToolError via ``_fetch_script_config_envelope``.
|
|
181
271
|
"""
|
|
182
|
-
config_result = await self.
|
|
272
|
+
config_result = await self._fetch_script_config_envelope(script_id)
|
|
183
273
|
actual_config = config_result.get("config", config_result)
|
|
184
274
|
config_hash_value = compute_config_hash(actual_config)
|
|
185
275
|
return actual_config, config_hash_value
|
|
@@ -223,19 +313,29 @@ class ConfigScriptTools:
|
|
|
223
313
|
try:
|
|
224
314
|
parsed_config = parse_json_param(config, "config")
|
|
225
315
|
except ValueError as e:
|
|
226
|
-
raise_tool_error(
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
316
|
+
raise_tool_error(
|
|
317
|
+
create_error_response(
|
|
318
|
+
ErrorCode.VALIDATION_INVALID_JSON,
|
|
319
|
+
f"Invalid config parameter: {e}",
|
|
320
|
+
context={
|
|
321
|
+
"script_id": script_id,
|
|
322
|
+
"provided_config_type": type(config).__name__,
|
|
323
|
+
},
|
|
324
|
+
)
|
|
325
|
+
)
|
|
231
326
|
|
|
232
327
|
# Ensure config is a dict
|
|
233
328
|
if parsed_config is None or not isinstance(parsed_config, dict):
|
|
234
|
-
raise_tool_error(
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
329
|
+
raise_tool_error(
|
|
330
|
+
create_error_response(
|
|
331
|
+
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
332
|
+
"Config parameter must be a JSON object",
|
|
333
|
+
context={
|
|
334
|
+
"script_id": script_id,
|
|
335
|
+
"provided_type": type(parsed_config).__name__,
|
|
336
|
+
},
|
|
337
|
+
)
|
|
338
|
+
)
|
|
239
339
|
|
|
240
340
|
config_dict = cast(dict[str, Any], parsed_config)
|
|
241
341
|
|
|
@@ -250,11 +350,16 @@ class ConfigScriptTools:
|
|
|
250
350
|
# Strip empty sequence array that would override blueprint
|
|
251
351
|
config_dict = _strip_empty_script_fields(config_dict)
|
|
252
352
|
elif "sequence" not in config_dict:
|
|
253
|
-
raise_tool_error(
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
353
|
+
raise_tool_error(
|
|
354
|
+
create_error_response(
|
|
355
|
+
ErrorCode.VALIDATION_MISSING_PARAMETER,
|
|
356
|
+
"config must include either 'sequence' field (for regular scripts) or 'use_blueprint' field (for blueprint-based scripts)",
|
|
357
|
+
context={
|
|
358
|
+
"script_id": script_id,
|
|
359
|
+
"required_fields": ["sequence OR use_blueprint"],
|
|
360
|
+
},
|
|
361
|
+
)
|
|
362
|
+
)
|
|
258
363
|
|
|
259
364
|
return config_dict, effective_category
|
|
260
365
|
|
|
@@ -270,7 +375,10 @@ class ConfigScriptTools:
|
|
|
270
375
|
async def ha_config_set_script(
|
|
271
376
|
self,
|
|
272
377
|
script_id: Annotated[
|
|
273
|
-
str,
|
|
378
|
+
str,
|
|
379
|
+
Field(
|
|
380
|
+
description="Script identifier — bare storage key ('morning_routine') or entity_id form ('script.morning_routine'); a leading 'script.' prefix is stripped before lookup."
|
|
381
|
+
),
|
|
274
382
|
],
|
|
275
383
|
config: Annotated[
|
|
276
384
|
str | dict[str, Any] | None,
|
|
@@ -447,6 +555,18 @@ class ConfigScriptTools:
|
|
|
447
555
|
"""
|
|
448
556
|
bp_warnings: list[str] = []
|
|
449
557
|
try:
|
|
558
|
+
# Strip BEFORE validate so a bare ``"script."`` (empty after
|
|
559
|
+
# strip) is rejected as ``VALIDATION_INVALID_PARAMETER`` rather
|
|
560
|
+
# than slipping through validate (non-empty pre-strip) and
|
|
561
|
+
# writing a phantom ``script.foo`` storage key — HA keys writes
|
|
562
|
+
# by the literal ``script_id``, so passing ``"script.foo"``
|
|
563
|
+
# unchanged makes the row invisible to a later
|
|
564
|
+
# ``ha_config_get_script("foo")``. Behavioral parity with
|
|
565
|
+
# ``ha_config_get_script`` so an agent that received an
|
|
566
|
+
# entity_id (``script.foo``) from
|
|
567
|
+
# ``ha_search_entities(domain_filter='script')`` can update it
|
|
568
|
+
# without a manual prefix-strip step.
|
|
569
|
+
script_id = script_id.removeprefix("script.")
|
|
450
570
|
# ``script_id`` is required (always non-None). Reject empty/
|
|
451
571
|
# whitespace up-front so the caller gets a structured parameter
|
|
452
572
|
# error instead of a misleading ``RESOURCE_NOT_FOUND`` from
|
|
@@ -488,7 +608,10 @@ class ConfigScriptTools:
|
|
|
488
608
|
"Call ha_config_get_script() first",
|
|
489
609
|
"Use the config_hash from that response",
|
|
490
610
|
],
|
|
491
|
-
context={
|
|
611
|
+
context={
|
|
612
|
+
"action": "python_transform",
|
|
613
|
+
"script_id": script_id,
|
|
614
|
+
},
|
|
492
615
|
)
|
|
493
616
|
)
|
|
494
617
|
|
|
@@ -507,12 +630,18 @@ class ConfigScriptTools:
|
|
|
507
630
|
ErrorCode.VALIDATION_FAILED,
|
|
508
631
|
message,
|
|
509
632
|
suggestions=suggestions,
|
|
510
|
-
context={
|
|
633
|
+
context={
|
|
634
|
+
"action": "python_transform",
|
|
635
|
+
"script_id": script_id,
|
|
636
|
+
},
|
|
511
637
|
)
|
|
512
638
|
)
|
|
513
639
|
|
|
514
640
|
# Validate transformed config
|
|
515
|
-
if
|
|
641
|
+
if (
|
|
642
|
+
"sequence" not in transformed_config
|
|
643
|
+
and "use_blueprint" not in transformed_config
|
|
644
|
+
):
|
|
516
645
|
raise_tool_error(
|
|
517
646
|
create_error_response(
|
|
518
647
|
ErrorCode.VALIDATION_FAILED,
|
|
@@ -521,7 +650,10 @@ class ConfigScriptTools:
|
|
|
521
650
|
"The transform may have removed required fields",
|
|
522
651
|
"Ensure the config still has a 'sequence' or 'use_blueprint' key",
|
|
523
652
|
],
|
|
524
|
-
context={
|
|
653
|
+
context={
|
|
654
|
+
"action": "python_transform",
|
|
655
|
+
"script_id": script_id,
|
|
656
|
+
},
|
|
525
657
|
)
|
|
526
658
|
)
|
|
527
659
|
bp_warnings = _check_best_practices(transformed_config)
|
|
@@ -562,7 +694,9 @@ class ConfigScriptTools:
|
|
|
562
694
|
)
|
|
563
695
|
|
|
564
696
|
config_dict, effective_category = self._validate_script_config(
|
|
565
|
-
config,
|
|
697
|
+
config,
|
|
698
|
+
script_id,
|
|
699
|
+
category,
|
|
566
700
|
)
|
|
567
701
|
|
|
568
702
|
# Optional hash check for full config updates
|
|
@@ -586,7 +720,9 @@ class ConfigScriptTools:
|
|
|
586
720
|
entity_id = f"script.{script_id}"
|
|
587
721
|
if wait_bool:
|
|
588
722
|
try:
|
|
589
|
-
registered = await wait_for_entity_registered(
|
|
723
|
+
registered = await wait_for_entity_registered(
|
|
724
|
+
self._client, entity_id
|
|
725
|
+
)
|
|
590
726
|
if not registered:
|
|
591
727
|
result.setdefault("warnings", []).append(
|
|
592
728
|
f"Script saved but {entity_id} not yet queryable. "
|
|
@@ -600,7 +736,12 @@ class ConfigScriptTools:
|
|
|
600
736
|
# Apply category to entity registry if provided
|
|
601
737
|
if effective_category and entity_id:
|
|
602
738
|
await apply_entity_category(
|
|
603
|
-
self._client,
|
|
739
|
+
self._client,
|
|
740
|
+
entity_id,
|
|
741
|
+
effective_category,
|
|
742
|
+
"script",
|
|
743
|
+
result,
|
|
744
|
+
"script",
|
|
604
745
|
)
|
|
605
746
|
|
|
606
747
|
if bp_warnings:
|
|
@@ -629,6 +770,11 @@ class ConfigScriptTools:
|
|
|
629
770
|
"Config had best-practice issues that may be related: "
|
|
630
771
|
+ "; ".join(bp_warnings)
|
|
631
772
|
)
|
|
773
|
+
# 404 during update only — the create path raises on its own when
|
|
774
|
+
# the upsert hits an unknown identifier server-side. The bare
|
|
775
|
+
# script_id form is what callers pass and what the registry stores.
|
|
776
|
+
if isinstance(e, HomeAssistantAPIError) and e.status_code == 404:
|
|
777
|
+
await self._raise_script_not_found(script_id)
|
|
632
778
|
exception_to_structured_error(
|
|
633
779
|
e,
|
|
634
780
|
context={"script_id": script_id},
|
|
@@ -648,7 +794,10 @@ class ConfigScriptTools:
|
|
|
648
794
|
async def ha_config_remove_script(
|
|
649
795
|
self,
|
|
650
796
|
script_id: Annotated[
|
|
651
|
-
str,
|
|
797
|
+
str,
|
|
798
|
+
Field(
|
|
799
|
+
description="Script identifier to delete — bare storage key ('old_script') or entity_id form ('script.old_script'); a leading 'script.' prefix is stripped before lookup."
|
|
800
|
+
),
|
|
652
801
|
],
|
|
653
802
|
wait: Annotated[
|
|
654
803
|
bool | str,
|
|
@@ -675,6 +824,14 @@ class ConfigScriptTools:
|
|
|
675
824
|
**WARNING:** Deleting a script that is used by automations may cause those automations to fail.
|
|
676
825
|
"""
|
|
677
826
|
try:
|
|
827
|
+
# Strip BEFORE validate so a bare ``"script."`` (empty after
|
|
828
|
+
# strip) is rejected as ``VALIDATION_INVALID_PARAMETER`` rather
|
|
829
|
+
# than slipping through validate (non-empty pre-strip) and
|
|
830
|
+
# producing a ``script.script.foo`` entity_id for the
|
|
831
|
+
# ``wait_for_entity_removed`` watcher below — that mis-formed
|
|
832
|
+
# entity_id never registers so the watcher times out on a
|
|
833
|
+
# phantom. Behavioral parity with ``ha_config_get_script``.
|
|
834
|
+
script_id = script_id.removeprefix("script.")
|
|
678
835
|
# Empty/whitespace would surface as a misleading HA delete-failure.
|
|
679
836
|
validate_identifier_not_empty(
|
|
680
837
|
script_id,
|
|
@@ -705,6 +862,8 @@ class ConfigScriptTools:
|
|
|
705
862
|
except ToolError:
|
|
706
863
|
raise
|
|
707
864
|
except Exception as e:
|
|
865
|
+
if isinstance(e, HomeAssistantAPIError) and e.status_code == 404:
|
|
866
|
+
await self._raise_script_not_found(script_id)
|
|
708
867
|
exception_to_structured_error(
|
|
709
868
|
e,
|
|
710
869
|
context={"script_id": script_id},
|