ha-mcp-dev 7.3.0.dev396__tar.gz → 7.3.0.dev397__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.3.0.dev396/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.3.0.dev397}/PKG-INFO +1 -1
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/pyproject.toml +1 -1
- ha_mcp_dev-7.3.0.dev397/src/ha_mcp/tools/reference_validator.py +275 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/tools_config_automations.py +11 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/tools_config_scripts.py +11 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/util_helpers.py +31 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp_dev.egg-info/SOURCES.txt +1 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/LICENSE +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/README.md +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/setup.cfg +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/smart_search.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/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.3.0.
|
|
7
|
+
version = "7.3.0.dev397"
|
|
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"
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""Reference validator for automation and script configs.
|
|
2
|
+
|
|
3
|
+
Walks a config dict and extracts every literal service and entity
|
|
4
|
+
reference, then cross-checks them against the live service and entity
|
|
5
|
+
registries. Produces soft warnings that flow into the existing response
|
|
6
|
+
alongside ``best_practice_warnings``.
|
|
7
|
+
|
|
8
|
+
Intentional limits (documented, not bugs):
|
|
9
|
+
|
|
10
|
+
- **Templates** (strings containing ``{{``) are counted and skipped.
|
|
11
|
+
Jinja is not rendered here; template-safe validation would need
|
|
12
|
+
``POST /api/template`` round-trips and is a later follow-up.
|
|
13
|
+
- **Blueprint automations** (``use_blueprint`` at the root) are skipped
|
|
14
|
+
wholesale. Post-substitution config is not exposed by any HA API, so
|
|
15
|
+
the effective refs cannot be ground-truthed.
|
|
16
|
+
- **``device_id`` / ``area_id`` / ``label_id``** are NOT checked yet.
|
|
17
|
+
They require separate registry fetches and are planned for a later
|
|
18
|
+
pass; see #940.
|
|
19
|
+
|
|
20
|
+
Background: #940 (hallucinated ``notify.mobile_app_andrew_phone`` that
|
|
21
|
+
``ha_config_set_automation`` accepted silently).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import asyncio
|
|
27
|
+
import logging
|
|
28
|
+
from typing import Any, TypedDict
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
# Keys whose value (literal string) names a Home Assistant service in
|
|
33
|
+
# the form ``<domain>.<service_name>``. Both legacy (``service:``) and
|
|
34
|
+
# the new-style (``action:``) keys are recognized — HA accepts either
|
|
35
|
+
# inside automation/script action blocks.
|
|
36
|
+
_SERVICE_KEYS: frozenset[str] = frozenset({"service", "action"})
|
|
37
|
+
|
|
38
|
+
# Keys whose value (string or list of strings) names an entity. These
|
|
39
|
+
# appear in triggers, conditions, ``target:`` blocks, and service
|
|
40
|
+
# ``data:`` blocks — the walker is depth-agnostic so the location
|
|
41
|
+
# doesn't matter.
|
|
42
|
+
_ENTITY_KEYS: frozenset[str] = frozenset({"entity_id"})
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ExtractedRef(TypedDict):
|
|
46
|
+
"""One reference pulled out of the config tree."""
|
|
47
|
+
|
|
48
|
+
path: str
|
|
49
|
+
value: str
|
|
50
|
+
kind: str # "service" | "entity"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class WalkerResult(TypedDict):
|
|
54
|
+
"""Return value of :func:`extract_refs`."""
|
|
55
|
+
|
|
56
|
+
refs: list[ExtractedRef]
|
|
57
|
+
unvalidated_templates: int
|
|
58
|
+
blueprint_skipped: bool
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ValidationWarning(TypedDict):
|
|
62
|
+
"""One warning in the tool response."""
|
|
63
|
+
|
|
64
|
+
path: str
|
|
65
|
+
value: str
|
|
66
|
+
kind: str
|
|
67
|
+
reason: str
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def extract_refs(config: Any) -> WalkerResult:
|
|
71
|
+
"""Pull every literal service/entity reference out of *config*.
|
|
72
|
+
|
|
73
|
+
Pure function: no network, no mutation of the input. The caller
|
|
74
|
+
decides what to do with the extracted refs.
|
|
75
|
+
|
|
76
|
+
Blueprint configs short-circuit with an empty ref list and
|
|
77
|
+
``blueprint_skipped=True`` — the effective post-substitution config
|
|
78
|
+
is not reachable from ha-mcp, so it cannot be validated.
|
|
79
|
+
"""
|
|
80
|
+
if isinstance(config, dict) and "use_blueprint" in config:
|
|
81
|
+
return {
|
|
82
|
+
"refs": [],
|
|
83
|
+
"unvalidated_templates": 0,
|
|
84
|
+
"blueprint_skipped": True,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
refs: list[ExtractedRef] = []
|
|
88
|
+
# Wrapped in a single-element list so the inner closure can mutate
|
|
89
|
+
# it without a ``nonlocal`` dance.
|
|
90
|
+
unvalidated_templates = [0]
|
|
91
|
+
|
|
92
|
+
def _walk(node: Any, path: str) -> None:
|
|
93
|
+
if isinstance(node, dict):
|
|
94
|
+
for key, value in node.items():
|
|
95
|
+
sub_path = f"{path}.{key}" if path else key
|
|
96
|
+
|
|
97
|
+
if key in _SERVICE_KEYS and isinstance(value, str):
|
|
98
|
+
if _is_template(value):
|
|
99
|
+
unvalidated_templates[0] += 1
|
|
100
|
+
else:
|
|
101
|
+
refs.append(
|
|
102
|
+
{"path": sub_path, "value": value, "kind": "service"}
|
|
103
|
+
)
|
|
104
|
+
continue
|
|
105
|
+
|
|
106
|
+
if key in _ENTITY_KEYS:
|
|
107
|
+
if isinstance(value, str):
|
|
108
|
+
if _is_template(value):
|
|
109
|
+
unvalidated_templates[0] += 1
|
|
110
|
+
else:
|
|
111
|
+
refs.append(
|
|
112
|
+
{"path": sub_path, "value": value, "kind": "entity"}
|
|
113
|
+
)
|
|
114
|
+
continue
|
|
115
|
+
if isinstance(value, list):
|
|
116
|
+
for i, item in enumerate(value):
|
|
117
|
+
if not isinstance(item, str):
|
|
118
|
+
continue
|
|
119
|
+
item_path = f"{sub_path}[{i}]"
|
|
120
|
+
if _is_template(item):
|
|
121
|
+
unvalidated_templates[0] += 1
|
|
122
|
+
else:
|
|
123
|
+
refs.append(
|
|
124
|
+
{
|
|
125
|
+
"path": item_path,
|
|
126
|
+
"value": item,
|
|
127
|
+
"kind": "entity",
|
|
128
|
+
}
|
|
129
|
+
)
|
|
130
|
+
continue
|
|
131
|
+
|
|
132
|
+
# Neither a service nor an entity key: recurse so deeply
|
|
133
|
+
# nested action blocks (choose/if/parallel/repeat) still
|
|
134
|
+
# get walked.
|
|
135
|
+
_walk(value, sub_path)
|
|
136
|
+
|
|
137
|
+
elif isinstance(node, list):
|
|
138
|
+
for i, item in enumerate(node):
|
|
139
|
+
_walk(item, f"{path}[{i}]")
|
|
140
|
+
# Primitives: nothing to extract.
|
|
141
|
+
|
|
142
|
+
_walk(config, "")
|
|
143
|
+
return {
|
|
144
|
+
"refs": refs,
|
|
145
|
+
"unvalidated_templates": unvalidated_templates[0],
|
|
146
|
+
"blueprint_skipped": False,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _is_template(value: str) -> bool:
|
|
151
|
+
"""Return True if *value* looks like a Jinja template."""
|
|
152
|
+
return "{{" in value
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def build_service_index(services_payload: Any) -> dict[str, set[str]]:
|
|
156
|
+
"""Turn ``/api/services`` output into a ``{domain: {services}}`` map.
|
|
157
|
+
|
|
158
|
+
HA returns a list of ``{"domain": str, "services": {name: {...}}}``
|
|
159
|
+
objects — one per domain. Any malformed entry is skipped silently.
|
|
160
|
+
"""
|
|
161
|
+
index: dict[str, set[str]] = {}
|
|
162
|
+
if not isinstance(services_payload, list):
|
|
163
|
+
return index
|
|
164
|
+
for entry in services_payload:
|
|
165
|
+
if not isinstance(entry, dict):
|
|
166
|
+
continue
|
|
167
|
+
domain = entry.get("domain")
|
|
168
|
+
services = entry.get("services")
|
|
169
|
+
if not isinstance(domain, str) or not isinstance(services, dict):
|
|
170
|
+
continue
|
|
171
|
+
index[domain] = set(services.keys())
|
|
172
|
+
return index
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def build_entity_set(states_payload: Any) -> set[str]:
|
|
176
|
+
"""Turn ``/api/states`` output into a set of entity_ids."""
|
|
177
|
+
entities: set[str] = set()
|
|
178
|
+
if not isinstance(states_payload, list):
|
|
179
|
+
return entities
|
|
180
|
+
for entry in states_payload:
|
|
181
|
+
if isinstance(entry, dict):
|
|
182
|
+
entity_id = entry.get("entity_id")
|
|
183
|
+
if isinstance(entity_id, str):
|
|
184
|
+
entities.add(entity_id)
|
|
185
|
+
return entities
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def check_refs(
|
|
189
|
+
refs: list[ExtractedRef],
|
|
190
|
+
service_index: dict[str, set[str]],
|
|
191
|
+
entity_set: set[str],
|
|
192
|
+
) -> list[ValidationWarning]:
|
|
193
|
+
"""Return one warning per ref that isn't in the registry."""
|
|
194
|
+
warnings: list[ValidationWarning] = []
|
|
195
|
+
for ref in refs:
|
|
196
|
+
value = ref["value"]
|
|
197
|
+
if ref["kind"] == "service":
|
|
198
|
+
domain, _, service_name = value.partition(".")
|
|
199
|
+
if (
|
|
200
|
+
not service_name
|
|
201
|
+
or domain not in service_index
|
|
202
|
+
or service_name not in service_index[domain]
|
|
203
|
+
):
|
|
204
|
+
warnings.append(
|
|
205
|
+
{
|
|
206
|
+
"path": ref["path"],
|
|
207
|
+
"value": value,
|
|
208
|
+
"kind": "service",
|
|
209
|
+
"reason": "not found in service registry",
|
|
210
|
+
}
|
|
211
|
+
)
|
|
212
|
+
elif ref["kind"] == "entity":
|
|
213
|
+
if value not in entity_set:
|
|
214
|
+
warnings.append(
|
|
215
|
+
{
|
|
216
|
+
"path": ref["path"],
|
|
217
|
+
"value": value,
|
|
218
|
+
"kind": "entity",
|
|
219
|
+
"reason": "not found in entity registry",
|
|
220
|
+
}
|
|
221
|
+
)
|
|
222
|
+
return warnings
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
async def validate_config_references(
|
|
226
|
+
client: Any, config: dict[str, Any]
|
|
227
|
+
) -> dict[str, Any]:
|
|
228
|
+
"""Walk *config*, fetch registries, return validation metadata.
|
|
229
|
+
|
|
230
|
+
Errors from the two registry fetches are logged and swallowed so
|
|
231
|
+
validation can never break the happy path of
|
|
232
|
+
``ha_config_set_automation`` / ``ha_config_set_script``.
|
|
233
|
+
|
|
234
|
+
Returns a dict with three keys:
|
|
235
|
+
|
|
236
|
+
- ``warnings`` - list of :class:`ValidationWarning`, empty on success
|
|
237
|
+
- ``unvalidated_templates`` - int, templated strings skipped by the
|
|
238
|
+
walker
|
|
239
|
+
- ``blueprint_skipped`` - bool, True iff the root config uses
|
|
240
|
+
``use_blueprint``
|
|
241
|
+
"""
|
|
242
|
+
walker_result = extract_refs(config)
|
|
243
|
+
|
|
244
|
+
if walker_result["blueprint_skipped"] or not walker_result["refs"]:
|
|
245
|
+
return {
|
|
246
|
+
"warnings": [],
|
|
247
|
+
"unvalidated_templates": walker_result["unvalidated_templates"],
|
|
248
|
+
"blueprint_skipped": walker_result["blueprint_skipped"],
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
try:
|
|
252
|
+
services_payload, states_payload = await asyncio.gather(
|
|
253
|
+
client.get_services(),
|
|
254
|
+
client.get_states(),
|
|
255
|
+
)
|
|
256
|
+
except Exception:
|
|
257
|
+
logger.exception(
|
|
258
|
+
"Reference validator: failed to fetch service/entity registries; "
|
|
259
|
+
"skipping validation for this call"
|
|
260
|
+
)
|
|
261
|
+
return {
|
|
262
|
+
"warnings": [],
|
|
263
|
+
"unvalidated_templates": walker_result["unvalidated_templates"],
|
|
264
|
+
"blueprint_skipped": False,
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
service_index = build_service_index(services_payload)
|
|
268
|
+
entity_set = build_entity_set(states_payload)
|
|
269
|
+
warnings = check_refs(walker_result["refs"], service_index, entity_set)
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
"warnings": warnings,
|
|
273
|
+
"unvalidated_templates": walker_result["unvalidated_templates"],
|
|
274
|
+
"blueprint_skipped": False,
|
|
275
|
+
}
|
{ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/tools_config_automations.py
RENAMED
|
@@ -37,10 +37,12 @@ from .helpers import (
|
|
|
37
37
|
raise_tool_error,
|
|
38
38
|
register_tool_methods,
|
|
39
39
|
)
|
|
40
|
+
from .reference_validator import validate_config_references
|
|
40
41
|
from .util_helpers import (
|
|
41
42
|
apply_entity_category,
|
|
42
43
|
coerce_bool_param,
|
|
43
44
|
fetch_entity_category,
|
|
45
|
+
merge_validation_meta,
|
|
44
46
|
parse_json_param,
|
|
45
47
|
wait_for_entity_registered,
|
|
46
48
|
wait_for_entity_removed,
|
|
@@ -641,6 +643,13 @@ class AutomationConfigTools:
|
|
|
641
643
|
config_dict, skill_prefix=_get_skill_prefix()
|
|
642
644
|
)
|
|
643
645
|
|
|
646
|
+
# Cross-check literal service and entity references against
|
|
647
|
+
# the live registries. Soft warnings only — the write still
|
|
648
|
+
# happens, even when references don't resolve (#940).
|
|
649
|
+
validation_meta = await validate_config_references(
|
|
650
|
+
self._client, config_dict
|
|
651
|
+
)
|
|
652
|
+
|
|
644
653
|
result = await self._client.upsert_automation_config(config_dict, identifier)
|
|
645
654
|
|
|
646
655
|
# If the client could not verify the entity was registered, warn but don't hard-fail.
|
|
@@ -677,6 +686,8 @@ class AutomationConfigTools:
|
|
|
677
686
|
if bp_warnings:
|
|
678
687
|
result["best_practice_warnings"] = bp_warnings
|
|
679
688
|
|
|
689
|
+
merge_validation_meta(result, validation_meta)
|
|
690
|
+
|
|
680
691
|
return {
|
|
681
692
|
"success": True,
|
|
682
693
|
**result,
|
{ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/tools_config_scripts.py
RENAMED
|
@@ -31,10 +31,12 @@ from .helpers import (
|
|
|
31
31
|
raise_tool_error,
|
|
32
32
|
register_tool_methods,
|
|
33
33
|
)
|
|
34
|
+
from .reference_validator import validate_config_references
|
|
34
35
|
from .util_helpers import (
|
|
35
36
|
apply_entity_category,
|
|
36
37
|
coerce_bool_param,
|
|
37
38
|
fetch_entity_category,
|
|
39
|
+
merge_validation_meta,
|
|
38
40
|
parse_json_param,
|
|
39
41
|
wait_for_entity_registered,
|
|
40
42
|
wait_for_entity_removed,
|
|
@@ -523,6 +525,13 @@ class ConfigScriptTools:
|
|
|
523
525
|
config_dict, skill_prefix=_get_skill_prefix()
|
|
524
526
|
)
|
|
525
527
|
|
|
528
|
+
# Cross-check literal service and entity references against
|
|
529
|
+
# the live registries. Soft warnings only — the write still
|
|
530
|
+
# happens, even when references don't resolve (#940).
|
|
531
|
+
validation_meta = await validate_config_references(
|
|
532
|
+
self._client, config_dict
|
|
533
|
+
)
|
|
534
|
+
|
|
526
535
|
result = await self._client.upsert_script_config(config_dict, script_id)
|
|
527
536
|
|
|
528
537
|
# Wait for script to be queryable
|
|
@@ -545,6 +554,8 @@ class ConfigScriptTools:
|
|
|
545
554
|
if bp_warnings:
|
|
546
555
|
result["best_practice_warnings"] = bp_warnings
|
|
547
556
|
|
|
557
|
+
merge_validation_meta(result, validation_meta)
|
|
558
|
+
|
|
548
559
|
return {
|
|
549
560
|
"success": True,
|
|
550
561
|
**result,
|
|
@@ -530,3 +530,34 @@ async def apply_entity_category(
|
|
|
530
530
|
result_dict["category_warning"] = (
|
|
531
531
|
f"{entity_type.capitalize()} saved but failed to set category: {e}"
|
|
532
532
|
)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def merge_validation_meta(
|
|
536
|
+
result: dict[str, Any], validation_meta: dict[str, Any]
|
|
537
|
+
) -> None:
|
|
538
|
+
"""Attach reference-validator output to a set-tool success ``result``.
|
|
539
|
+
|
|
540
|
+
Produces a single nested ``validation`` field when there's anything
|
|
541
|
+
worth reporting - warnings, skipped templates, or a blueprint
|
|
542
|
+
short-circuit. Keeps the happy-path response unchanged.
|
|
543
|
+
|
|
544
|
+
Shared between ``ha_config_set_automation`` and
|
|
545
|
+
``ha_config_set_script``; see
|
|
546
|
+
:mod:`ha_mcp.tools.reference_validator` for the validator itself
|
|
547
|
+
and #940 for background.
|
|
548
|
+
"""
|
|
549
|
+
warnings = validation_meta.get("warnings") or []
|
|
550
|
+
unvalidated_templates = validation_meta.get("unvalidated_templates") or 0
|
|
551
|
+
blueprint_skipped = bool(validation_meta.get("blueprint_skipped"))
|
|
552
|
+
|
|
553
|
+
if not warnings and not unvalidated_templates and not blueprint_skipped:
|
|
554
|
+
return
|
|
555
|
+
|
|
556
|
+
entry: dict[str, Any] = {}
|
|
557
|
+
if warnings:
|
|
558
|
+
entry["warnings"] = warnings
|
|
559
|
+
if unvalidated_templates:
|
|
560
|
+
entry["unvalidated_templates"] = unvalidated_templates
|
|
561
|
+
if blueprint_skipped:
|
|
562
|
+
entry["blueprint_skipped"] = True
|
|
563
|
+
result["validation"] = entry
|
|
@@ -43,6 +43,7 @@ src/ha_mcp/tools/best_practice_checker.py
|
|
|
43
43
|
src/ha_mcp/tools/device_control.py
|
|
44
44
|
src/ha_mcp/tools/enhanced.py
|
|
45
45
|
src/ha_mcp/tools/helpers.py
|
|
46
|
+
src/ha_mcp/tools/reference_validator.py
|
|
46
47
|
src/ha_mcp/tools/registry.py
|
|
47
48
|
src/ha_mcp/tools/smart_search.py
|
|
48
49
|
src/ha_mcp/tools/tools_addons.py
|
|
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.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/resources/skills-vendor/AGENTS.md
RENAMED
|
File without changes
|
{ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/resources/skills-vendor/CLAUDE.md
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/resources/skills-vendor/LICENSE
RENAMED
|
File without changes
|
{ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/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
|
{ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/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
|
{ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/tools_config_dashboards.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/tools_config_entry_flow.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/tools_config_helpers.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.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/tools/tools_voice_assistant.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp/transforms/categorized_search.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
|
{ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/src/ha_mcp_dev.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{ha_mcp_dev-7.3.0.dev396 → ha_mcp_dev-7.3.0.dev397}/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
|