ha-mcp-dev 7.5.0.dev568__tar.gz → 7.5.0.dev570__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.dev568/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.5.0.dev570}/PKG-INFO +1 -1
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/pyproject.toml +1 -1
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/client/rest_client.py +40 -4
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_config_automations.py +153 -91
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/LICENSE +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/README.md +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/setup.cfg +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/settings_ui.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/smart_search.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/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.dev570"
|
|
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
|
|
@@ -906,6 +907,30 @@ class HomeAssistantClient:
|
|
|
906
907
|
operation = "updated"
|
|
907
908
|
logger.debug(f"Updating automation with unique_id: {unique_id}")
|
|
908
909
|
|
|
910
|
+
# Reject mismatch between resolved storage key and inner ``config["id"]``.
|
|
911
|
+
# HA stores by the inner id field even though the URL carries one too —
|
|
912
|
+
# a divergence silently overwrites the automation whose unique_id matches
|
|
913
|
+
# the inner id, while reporting success for the URL target (#1404).
|
|
914
|
+
config_id = config.get("id")
|
|
915
|
+
if config_id is not None and str(config_id) != str(unique_id):
|
|
916
|
+
if identifier is None:
|
|
917
|
+
raise HomeAssistantAPIError(
|
|
918
|
+
"Cannot create automation with explicit config['id']="
|
|
919
|
+
f"{config_id!r}: Home Assistant stores by the inner id, "
|
|
920
|
+
"which would silently overwrite an existing automation. "
|
|
921
|
+
"Omit 'id' from config to auto-generate, or pass identifier "
|
|
922
|
+
"to update an existing automation.",
|
|
923
|
+
status_code=400,
|
|
924
|
+
)
|
|
925
|
+
raise HomeAssistantAPIError(
|
|
926
|
+
f"Mismatched automation id: identifier={identifier!r} resolves "
|
|
927
|
+
f"to unique_id={unique_id!r}, but config['id']={config_id!r}. "
|
|
928
|
+
"Refusing to write to prevent overwriting the wrong automation. "
|
|
929
|
+
f"Remove 'id' from config or set it to the resolved unique_id "
|
|
930
|
+
f"({unique_id!r}).",
|
|
931
|
+
status_code=400,
|
|
932
|
+
)
|
|
933
|
+
|
|
909
934
|
# Add unique_id to config for updates
|
|
910
935
|
if unique_id and "id" not in config:
|
|
911
936
|
config = {**config, "id": unique_id}
|
|
@@ -939,12 +964,18 @@ class HomeAssistantClient:
|
|
|
939
964
|
) from e
|
|
940
965
|
raise
|
|
941
966
|
|
|
942
|
-
# 3-attempt × 6s upper-bound budget; first poll 0.
|
|
943
|
-
#
|
|
944
|
-
|
|
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)
|
|
945
973
|
|
|
946
974
|
async def _poll_for_automation_entity(self, unique_id: str) -> str | None:
|
|
947
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()
|
|
948
979
|
try:
|
|
949
980
|
for sleep_time in self._POLL_CADENCE:
|
|
950
981
|
await asyncio.sleep(sleep_time)
|
|
@@ -954,8 +985,13 @@ class HomeAssistantClient:
|
|
|
954
985
|
continue
|
|
955
986
|
if state.get("attributes", {}).get("id") == unique_id:
|
|
956
987
|
entity_id = state.get("entity_id")
|
|
988
|
+
elapsed_ms = (time.monotonic() - start_monotonic) * 1000.0
|
|
957
989
|
logger.debug(
|
|
958
|
-
|
|
990
|
+
"entity-registration-elapsed: %.1fms "
|
|
991
|
+
"(unique_id=%s, entity_id=%s)",
|
|
992
|
+
elapsed_ms,
|
|
993
|
+
unique_id,
|
|
994
|
+
entity_id,
|
|
959
995
|
)
|
|
960
996
|
return entity_id
|
|
961
997
|
except HomeAssistantError as e:
|
{ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_config_automations.py
RENAMED
|
@@ -53,6 +53,17 @@ from .util_helpers import (
|
|
|
53
53
|
|
|
54
54
|
logger = logging.getLogger(__name__)
|
|
55
55
|
|
|
56
|
+
# Distinctive prefix of the soft-failure warning emitted by
|
|
57
|
+
# ``ha_config_set_automation`` when ``_poll_for_automation_entity``
|
|
58
|
+
# exhausts ``_POLL_CADENCE`` without matching the new automation.
|
|
59
|
+
# Exported so tests (e.g. ``test_poll_cadence_measurement.py``) can
|
|
60
|
+
# detect a missed registration without hard-coding the literal —
|
|
61
|
+
# rewording the warning becomes a compile-time coupling rather than
|
|
62
|
+
# a silent test drift.
|
|
63
|
+
NOT_VERIFIED_WARNING_PREFIX = (
|
|
64
|
+
"Automation was submitted to Home Assistant but the entity was not found"
|
|
65
|
+
)
|
|
66
|
+
|
|
56
67
|
|
|
57
68
|
def _normalize_automation_config(
|
|
58
69
|
config: Any,
|
|
@@ -279,7 +290,9 @@ class AutomationConfigTools:
|
|
|
279
290
|
):
|
|
280
291
|
return str(state["entity_id"])
|
|
281
292
|
except Exception as e:
|
|
282
|
-
logger.debug(
|
|
293
|
+
logger.debug(
|
|
294
|
+
f"Failed to resolve entity_id for automation {identifier}: {e}"
|
|
295
|
+
)
|
|
283
296
|
return None
|
|
284
297
|
|
|
285
298
|
@tool(
|
|
@@ -334,13 +347,17 @@ class AutomationConfigTools:
|
|
|
334
347
|
"Use ha_search_entities(domain_filter='automation') to list automations",
|
|
335
348
|
],
|
|
336
349
|
)
|
|
337
|
-
normalized_config, config_hash = await self._get_automation_config_internal(
|
|
350
|
+
normalized_config, config_hash = await self._get_automation_config_internal(
|
|
351
|
+
identifier
|
|
352
|
+
)
|
|
338
353
|
|
|
339
354
|
# Resolve entity_id and fetch category from entity registry
|
|
340
355
|
# (injected after hash so transient registry failures don't affect the hash)
|
|
341
356
|
entity_id = await self._resolve_automation_entity_id(identifier)
|
|
342
357
|
if entity_id:
|
|
343
|
-
cat_id = await fetch_entity_category(
|
|
358
|
+
cat_id = await fetch_entity_category(
|
|
359
|
+
self._client, entity_id, "automation"
|
|
360
|
+
)
|
|
344
361
|
if cat_id:
|
|
345
362
|
normalized_config["category"] = cat_id
|
|
346
363
|
|
|
@@ -644,7 +661,10 @@ class AutomationConfigTools:
|
|
|
644
661
|
"Provide the automation entity_id or unique_id",
|
|
645
662
|
"Use ha_search_entities(domain_filter='automation') to find automations",
|
|
646
663
|
],
|
|
647
|
-
context={
|
|
664
|
+
context={
|
|
665
|
+
"action": "python_transform",
|
|
666
|
+
"identifier": identifier,
|
|
667
|
+
},
|
|
648
668
|
)
|
|
649
669
|
)
|
|
650
670
|
if config_hash is None:
|
|
@@ -656,7 +676,10 @@ class AutomationConfigTools:
|
|
|
656
676
|
"Call ha_config_get_automation() first",
|
|
657
677
|
"Use the config_hash from that response",
|
|
658
678
|
],
|
|
659
|
-
context={
|
|
679
|
+
context={
|
|
680
|
+
"action": "python_transform",
|
|
681
|
+
"identifier": identifier,
|
|
682
|
+
},
|
|
660
683
|
)
|
|
661
684
|
)
|
|
662
685
|
|
|
@@ -675,7 +698,10 @@ class AutomationConfigTools:
|
|
|
675
698
|
ErrorCode.VALIDATION_FAILED,
|
|
676
699
|
message,
|
|
677
700
|
suggestions=suggestions,
|
|
678
|
-
context={
|
|
701
|
+
context={
|
|
702
|
+
"action": "python_transform",
|
|
703
|
+
"identifier": identifier,
|
|
704
|
+
},
|
|
679
705
|
)
|
|
680
706
|
)
|
|
681
707
|
|
|
@@ -698,11 +724,20 @@ class AutomationConfigTools:
|
|
|
698
724
|
|
|
699
725
|
# Re-apply category if present
|
|
700
726
|
entity_id = result.get("entity_id")
|
|
701
|
-
if
|
|
727
|
+
if (
|
|
728
|
+
not entity_id
|
|
729
|
+
and identifier
|
|
730
|
+
and identifier.startswith("automation.")
|
|
731
|
+
):
|
|
702
732
|
entity_id = identifier
|
|
703
733
|
if transform_category and entity_id:
|
|
704
734
|
await apply_entity_category(
|
|
705
|
-
self._client,
|
|
735
|
+
self._client,
|
|
736
|
+
entity_id,
|
|
737
|
+
transform_category,
|
|
738
|
+
"automation",
|
|
739
|
+
result,
|
|
740
|
+
"automation",
|
|
706
741
|
)
|
|
707
742
|
|
|
708
743
|
response: dict[str, Any] = {
|
|
@@ -762,12 +797,14 @@ class AutomationConfigTools:
|
|
|
762
797
|
self._client, config_dict
|
|
763
798
|
)
|
|
764
799
|
|
|
765
|
-
result = await self._client.upsert_automation_config(
|
|
800
|
+
result = await self._client.upsert_automation_config(
|
|
801
|
+
config_dict, identifier
|
|
802
|
+
)
|
|
766
803
|
|
|
767
804
|
# If the client could not verify the entity was registered, warn but don't hard-fail.
|
|
768
805
|
if result.get("entity_not_verified"):
|
|
769
806
|
result.setdefault("warnings", []).append(
|
|
770
|
-
"
|
|
807
|
+
f"{NOT_VERIFIED_WARNING_PREFIX} "
|
|
771
808
|
"after polling. The automation may still have been created -- check Home "
|
|
772
809
|
"Assistant logs and try reloading automations. Common causes: "
|
|
773
810
|
"automations.yaml vs automation.yaml filename mismatch, invalid config "
|
|
@@ -784,7 +821,9 @@ class AutomationConfigTools:
|
|
|
784
821
|
if wait_bool and entity_id:
|
|
785
822
|
action_word = "created" if identifier is None else "updated"
|
|
786
823
|
try:
|
|
787
|
-
registered = await wait_for_entity_registered(
|
|
824
|
+
registered = await wait_for_entity_registered(
|
|
825
|
+
self._client, entity_id
|
|
826
|
+
)
|
|
788
827
|
if not registered:
|
|
789
828
|
result.setdefault("warnings", []).append(
|
|
790
829
|
f"Automation {action_word} but {entity_id} not yet queryable. "
|
|
@@ -798,7 +837,12 @@ class AutomationConfigTools:
|
|
|
798
837
|
# Apply category to entity registry if provided
|
|
799
838
|
if effective_category and entity_id:
|
|
800
839
|
await apply_entity_category(
|
|
801
|
-
self._client,
|
|
840
|
+
self._client,
|
|
841
|
+
entity_id,
|
|
842
|
+
effective_category,
|
|
843
|
+
"automation",
|
|
844
|
+
result,
|
|
845
|
+
"automation",
|
|
802
846
|
)
|
|
803
847
|
|
|
804
848
|
if bp_warnings:
|
|
@@ -859,7 +903,9 @@ class AutomationConfigTools:
|
|
|
859
903
|
Returns the current normalized config dict.
|
|
860
904
|
Raises ToolError if the hash does not match (conflict).
|
|
861
905
|
"""
|
|
862
|
-
current_config, current_hash = await self._get_automation_config_internal(
|
|
906
|
+
current_config, current_hash = await self._get_automation_config_internal(
|
|
907
|
+
identifier
|
|
908
|
+
)
|
|
863
909
|
if current_hash != config_hash:
|
|
864
910
|
raise_tool_error(
|
|
865
911
|
create_error_response(
|
|
@@ -880,22 +926,26 @@ class AutomationConfigTools:
|
|
|
880
926
|
try:
|
|
881
927
|
parsed_config = parse_json_param(config, "config")
|
|
882
928
|
except ValueError as e:
|
|
883
|
-
raise_tool_error(
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
929
|
+
raise_tool_error(
|
|
930
|
+
create_error_response(
|
|
931
|
+
code=ErrorCode.VALIDATION_INVALID_JSON,
|
|
932
|
+
message=f"Invalid config parameter: {e}",
|
|
933
|
+
suggestions=[
|
|
934
|
+
"Pass 'config' as a dict, not a JSON string, to avoid escaping issues.",
|
|
935
|
+
"Check for JSON syntax errors: unquoted keys, trailing commas, or invalid escape sequences.",
|
|
936
|
+
],
|
|
937
|
+
context={"parameter": "config"},
|
|
938
|
+
)
|
|
939
|
+
)
|
|
892
940
|
|
|
893
941
|
if parsed_config is None or not isinstance(parsed_config, dict):
|
|
894
|
-
raise_tool_error(
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
942
|
+
raise_tool_error(
|
|
943
|
+
create_validation_error(
|
|
944
|
+
"Config parameter must be a JSON object",
|
|
945
|
+
parameter="config",
|
|
946
|
+
details=f"Received type: {type(parsed_config).__name__}",
|
|
947
|
+
)
|
|
948
|
+
)
|
|
899
949
|
|
|
900
950
|
return cast(dict[str, Any], parsed_config)
|
|
901
951
|
|
|
@@ -924,24 +974,28 @@ class AutomationConfigTools:
|
|
|
924
974
|
context: dict[str, Any] = {"missing_fields": missing_fields}
|
|
925
975
|
if identifier:
|
|
926
976
|
context["identifier"] = identifier
|
|
927
|
-
raise_tool_error(
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
977
|
+
raise_tool_error(
|
|
978
|
+
create_error_response(
|
|
979
|
+
code=ErrorCode.CONFIG_MISSING_REQUIRED_FIELDS,
|
|
980
|
+
message=f"Missing required fields: {', '.join(missing_fields)}",
|
|
981
|
+
details=(
|
|
982
|
+
"Config contains 'sequence', which belongs to scripts. "
|
|
983
|
+
"Automations use 'trigger' and 'action'; scripts use 'sequence'."
|
|
984
|
+
),
|
|
985
|
+
suggestions=[
|
|
986
|
+
"Did you mean ha_config_set_script? Scripts use 'sequence' directly.",
|
|
987
|
+
"For an automation, replace 'sequence' with 'action' and add a 'trigger'.",
|
|
988
|
+
],
|
|
989
|
+
context=context,
|
|
990
|
+
)
|
|
991
|
+
)
|
|
992
|
+
raise_tool_error(
|
|
993
|
+
create_config_error(
|
|
994
|
+
f"Missing required fields: {', '.join(missing_fields)}",
|
|
995
|
+
identifier=identifier,
|
|
996
|
+
missing_fields=missing_fields,
|
|
997
|
+
)
|
|
998
|
+
)
|
|
945
999
|
|
|
946
1000
|
# Issue #1169: reject configs that wrap ``scene.create`` in an
|
|
947
1001
|
# automation with no functional trigger. Models occasionally produce
|
|
@@ -966,31 +1020,33 @@ class AutomationConfigTools:
|
|
|
966
1020
|
if _action_contains_scene_create(a)
|
|
967
1021
|
]
|
|
968
1022
|
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
|
-
|
|
1023
|
+
raise_tool_error(
|
|
1024
|
+
create_error_response(
|
|
1025
|
+
code=ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
1026
|
+
message=(
|
|
1027
|
+
"Empty trigger paired with a scene.create action — "
|
|
1028
|
+
"this automation can never fire. For a state snapshot "
|
|
1029
|
+
"of one or more entities, use ha_config_set_scene "
|
|
1030
|
+
"directly instead of wrapping scene.create in an "
|
|
1031
|
+
"automation."
|
|
1032
|
+
),
|
|
1033
|
+
suggestions=[
|
|
1034
|
+
"ha_config_set_scene(scene_id='...', config={'name': "
|
|
1035
|
+
"'...', 'entities': {'<entity_id>': {...}}}) creates "
|
|
1036
|
+
"a scene without a trigger.",
|
|
1037
|
+
"If the snapshot really should be the result of an "
|
|
1038
|
+
"event, add the trigger that should fire it and keep "
|
|
1039
|
+
"the automation.",
|
|
1040
|
+
"For a state-derived value that recomputes when its "
|
|
1041
|
+
"inputs change, use "
|
|
1042
|
+
"ha_config_set_helper(helper_type='template') instead.",
|
|
1043
|
+
],
|
|
1044
|
+
context={
|
|
1045
|
+
"scene_create_action_indices": scene_create_indices,
|
|
1046
|
+
"identifier": identifier,
|
|
1047
|
+
},
|
|
1048
|
+
)
|
|
1049
|
+
)
|
|
994
1050
|
|
|
995
1051
|
# HA accepts conditions with 'platform' (trigger syntax) but then crashes
|
|
996
1052
|
# with an unhelpful 500 rather than a 400 validation error.
|
|
@@ -998,30 +1054,34 @@ class AutomationConfigTools:
|
|
|
998
1054
|
if not isinstance(cond, dict):
|
|
999
1055
|
continue
|
|
1000
1056
|
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
|
-
|
|
1057
|
+
raise_tool_error(
|
|
1058
|
+
create_error_response(
|
|
1059
|
+
code=ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
1060
|
+
message=(
|
|
1061
|
+
f"Condition at index {idx} uses 'platform' (trigger syntax). "
|
|
1062
|
+
"Conditions use 'condition', not 'platform'."
|
|
1063
|
+
),
|
|
1064
|
+
suggestions=[
|
|
1065
|
+
f"Replace 'platform' with 'condition': "
|
|
1066
|
+
f"{{'condition': '{cond['platform']}', ...}}",
|
|
1067
|
+
"Triggers use 'platform'; conditions use 'condition'.",
|
|
1068
|
+
],
|
|
1069
|
+
context={"condition_index": idx, "found_key": "platform"},
|
|
1070
|
+
)
|
|
1071
|
+
)
|
|
1014
1072
|
|
|
1015
1073
|
# Prevent duplicate creation when config contains an existing automation id
|
|
1016
1074
|
if identifier is None and "id" in config_dict:
|
|
1017
1075
|
existing_id = config_dict["id"]
|
|
1018
|
-
raise_tool_error(
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1076
|
+
raise_tool_error(
|
|
1077
|
+
create_validation_error(
|
|
1078
|
+
f"Config contains 'id' field ('{existing_id}') but no identifier was provided. "
|
|
1079
|
+
"This would create a duplicate automation instead of updating the existing one.",
|
|
1080
|
+
parameter="identifier",
|
|
1081
|
+
details=f"To update, pass identifier='{existing_id}' (or the automation's entity_id). "
|
|
1082
|
+
"To create a genuinely new automation, remove the 'id' field from the config.",
|
|
1083
|
+
)
|
|
1084
|
+
)
|
|
1025
1085
|
|
|
1026
1086
|
@tool(
|
|
1027
1087
|
name="ha_config_remove_automation",
|
|
@@ -1086,7 +1146,9 @@ class AutomationConfigTools:
|
|
|
1086
1146
|
wait_bool = coerce_bool_param(wait, "wait", default=True)
|
|
1087
1147
|
if wait_bool and entity_id_for_wait:
|
|
1088
1148
|
try:
|
|
1089
|
-
removed = await wait_for_entity_removed(
|
|
1149
|
+
removed = await wait_for_entity_removed(
|
|
1150
|
+
self._client, entity_id_for_wait
|
|
1151
|
+
)
|
|
1090
1152
|
if not removed:
|
|
1091
1153
|
result.setdefault("warnings", []).append(
|
|
1092
1154
|
f"Deletion confirmed by API but {entity_id_for_wait} may still appear briefly."
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/resources/skills-vendor/AGENTS.md
RENAMED
|
File without changes
|
{ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/resources/skills-vendor/CLAUDE.md
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/resources/skills-vendor/LICENSE
RENAMED
|
File without changes
|
{ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/resources/skills-vendor/README.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/best_practice_checker.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_config_dashboards.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_config_entry_flow.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_config_helpers.py
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_config_scripts.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_voice_assistant.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/transforms/categorized_search.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/transforms/lite_docstrings.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/utils/kill_signal_diagnostics.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp_dev.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{ha_mcp_dev-7.5.0.dev568 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp_dev.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|