ha-mcp-dev 7.4.1.dev458__tar.gz → 7.4.1.dev460__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.4.1.dev458/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.4.1.dev460}/PKG-INFO +1 -1
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/pyproject.toml +1 -1
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/server.py +63 -10
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/tools/best_practice_checker.py +237 -24
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/tools/tools_config_automations.py +19 -8
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/tools/tools_config_entry_flow.py +121 -13
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/tools/tools_config_helpers.py +940 -273
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/tools/tools_config_scripts.py +13 -10
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/LICENSE +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/README.md +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/setup.cfg +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/settings_ui.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/tools/smart_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/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.4.1.
|
|
7
|
+
version = "7.4.1.dev460"
|
|
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"
|
|
@@ -58,6 +58,48 @@ class HaResourcesAsTools(ResourcesAsTools):
|
|
|
58
58
|
"list_resources": LIST_TOOL_NAME,
|
|
59
59
|
"read_resource": READ_TOOL_NAME,
|
|
60
60
|
}
|
|
61
|
+
# Shared action-phrased keyword block for retrieval. Some MCP clients
|
|
62
|
+
# (Claude Code, others) rank candidate tools by token-overlap between
|
|
63
|
+
# the user's natural-language query and each tool's `description`
|
|
64
|
+
# field; FastMCP's terse defaults ("List MCP resources") never overlap
|
|
65
|
+
# with task-phrased queries like "create automation" or "writing
|
|
66
|
+
# trigger". This block lists the workflow positions where consulting
|
|
67
|
+
# the bundled skill reference files matters, so retrieval surfaces
|
|
68
|
+
# this tool when an agent is about to write config.
|
|
69
|
+
_USE_BEFORE_KEYWORDS = (
|
|
70
|
+
"Use BEFORE: creating or editing automations, scripts, scenes, "
|
|
71
|
+
"helpers, or dashboards; writing triggers, conditions, actions, "
|
|
72
|
+
"wait_template, or service calls; renaming entities or migrating "
|
|
73
|
+
"device_id to entity_id; calling ha_config_set_automation, "
|
|
74
|
+
"ha_config_set_script, ha_config_set_helper, ha_config_set_dashboard, "
|
|
75
|
+
"or ha_set_entity."
|
|
76
|
+
)
|
|
77
|
+
_DESCRIPTIONS: ClassVar[dict[str, str]] = {
|
|
78
|
+
LIST_TOOL_NAME: (
|
|
79
|
+
"List all available MCP resources, including bundled skill "
|
|
80
|
+
"reference files. " + _USE_BEFORE_KEYWORDS + " Pair with "
|
|
81
|
+
"ha_read_resource to load a specific guide."
|
|
82
|
+
),
|
|
83
|
+
READ_TOOL_NAME: (
|
|
84
|
+
"Get the contents of an MCP resource by URI. Use this to load "
|
|
85
|
+
"skill reference files (e.g., "
|
|
86
|
+
"skill://home-assistant-best-practices/references/"
|
|
87
|
+
"automation-patterns.md) for guidance on native conditions and "
|
|
88
|
+
"triggers, helper selection, automation modes, template "
|
|
89
|
+
"guidelines, device control, and safe refactoring. "
|
|
90
|
+
+ _USE_BEFORE_KEYWORDS
|
|
91
|
+
+ " Use ha_list_resources to discover available URIs."
|
|
92
|
+
),
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
def _rewrite(cls, tool: Tool, new_name: str) -> Tool:
|
|
97
|
+
"""Return a copy of ``tool`` renamed and re-described for ha-mcp."""
|
|
98
|
+
update: dict[str, Any] = {"name": new_name}
|
|
99
|
+
description = cls._DESCRIPTIONS.get(new_name)
|
|
100
|
+
if description is not None:
|
|
101
|
+
update["description"] = description
|
|
102
|
+
return tool.model_copy(update=update)
|
|
61
103
|
|
|
62
104
|
async def list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]:
|
|
63
105
|
# Scan the entire result rather than slicing the tail so a future
|
|
@@ -72,7 +114,7 @@ class HaResourcesAsTools(ResourcesAsTools):
|
|
|
72
114
|
if new_name is None:
|
|
73
115
|
renamed.append(tool)
|
|
74
116
|
continue
|
|
75
|
-
renamed.append(
|
|
117
|
+
renamed.append(self._rewrite(tool, new_name))
|
|
76
118
|
matches += 1
|
|
77
119
|
if matches != len(self._RENAMES):
|
|
78
120
|
logger.warning(
|
|
@@ -93,13 +135,9 @@ class HaResourcesAsTools(ResourcesAsTools):
|
|
|
93
135
|
version: VersionSpec | None = None,
|
|
94
136
|
) -> Tool | None:
|
|
95
137
|
if name == self.LIST_TOOL_NAME:
|
|
96
|
-
return self._make_list_resources_tool().
|
|
97
|
-
update={"name": self.LIST_TOOL_NAME}
|
|
98
|
-
)
|
|
138
|
+
return self._rewrite(self._make_list_resources_tool(), self.LIST_TOOL_NAME)
|
|
99
139
|
if name == self.READ_TOOL_NAME:
|
|
100
|
-
return self._make_read_resource_tool().
|
|
101
|
-
update={"name": self.READ_TOOL_NAME}
|
|
102
|
-
)
|
|
140
|
+
return self._rewrite(self._make_read_resource_tool(), self.READ_TOOL_NAME)
|
|
103
141
|
return await call_next(name, version=version)
|
|
104
142
|
|
|
105
143
|
# Server icon configuration using GitHub-hosted images
|
|
@@ -772,12 +810,27 @@ class HomeAssistantSmartMCPServer(EnhancedToolsMixin):
|
|
|
772
810
|
tool_name = f"ha_get_skill_{skill_name.replace('-', '_')}"
|
|
773
811
|
uri = f"skill://{skill_name}/SKILL.md"
|
|
774
812
|
|
|
813
|
+
# Action-phrased keyword block for retrieval. Some MCP clients
|
|
814
|
+
# rank candidate tools by token-overlap between the user's
|
|
815
|
+
# query and each tool's `description`; the upstream SKILL.md
|
|
816
|
+
# description is symptom-framed ("Agent uses Jinja2 templates
|
|
817
|
+
# where..."), which doesn't overlap with task-phrased queries
|
|
818
|
+
# like "create automation" / "set automation config" / "writing
|
|
819
|
+
# trigger". This block re-anchors the description in those
|
|
820
|
+
# task verbs so it surfaces when an agent is about to write
|
|
821
|
+
# config. Same shared keyword block as
|
|
822
|
+
# HaResourcesAsTools._USE_BEFORE_KEYWORDS above.
|
|
775
823
|
tool_description = (
|
|
824
|
+
f"Get available reference files for the {skill_name} skill. "
|
|
776
825
|
f"CALL THIS FIRST before performing matching actions. "
|
|
777
826
|
f"{description}\n\n"
|
|
778
|
-
f"
|
|
779
|
-
f"
|
|
780
|
-
f"
|
|
827
|
+
f"{HaResourcesAsTools._USE_BEFORE_KEYWORDS} The reference "
|
|
828
|
+
f"files below cover automation patterns, helper selection, "
|
|
829
|
+
f"template guidelines, device control, dashboards, and safe "
|
|
830
|
+
f"refactoring.\n\n"
|
|
831
|
+
f"Read each reference file via resources/read (or "
|
|
832
|
+
f"ha_read_resource as a fallback) using the file URI to load "
|
|
833
|
+
f"specific guides as needed."
|
|
781
834
|
)
|
|
782
835
|
|
|
783
836
|
ref_files = self._collect_skill_ref_files(skill_dir, skill_name)
|
{ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/tools/best_practice_checker.py
RENAMED
|
@@ -8,6 +8,30 @@ file via the bundled SkillsDirectoryProvider. The ``skill_prefix`` kwarg
|
|
|
8
8
|
lets callers pass any URL prefix (e.g., a GitHub mirror) when skill://
|
|
9
9
|
isn't reachable, or ``None`` to omit references entirely.
|
|
10
10
|
|
|
11
|
+
Each warning carries the native alternative inline (a concrete example or
|
|
12
|
+
short explanation) before the URI suffix, so clients that don't auto-fetch
|
|
13
|
+
resource URIs still receive actionable guidance.
|
|
14
|
+
|
|
15
|
+
The checker covers two layers:
|
|
16
|
+
|
|
17
|
+
1. *Specific* detectors for known anti-pattern shapes — each emits a tailored
|
|
18
|
+
message that names the native alternative concretely.
|
|
19
|
+
2. A *generic* fallback that fires when ``{{ ... }}`` or ``{% ... %}`` shows
|
|
20
|
+
up in a logic position (condition / trigger / wait_template / target field)
|
|
21
|
+
without matching a specific pattern. This catches new template misuse
|
|
22
|
+
without waiting for a regex to be added.
|
|
23
|
+
|
|
24
|
+
Allowlist by design — these positions are NOT walked by any recursion path,
|
|
25
|
+
so templates in them never trigger a warning even when present. They are the
|
|
26
|
+
documented legitimate dynamic-data positions per
|
|
27
|
+
``template-guidelines.md#when-templates-are-appropriate``:
|
|
28
|
+
|
|
29
|
+
* Action ``data.*`` fields (notification messages, brightness, volume, etc.)
|
|
30
|
+
* Notification ``message`` / ``title`` bodies
|
|
31
|
+
* Action ``event_data.*`` (HA evaluates event_data as a template at runtime)
|
|
32
|
+
* Top-level ``variables.*``
|
|
33
|
+
* Action ``service_data.*`` (legacy alias for ``data``)
|
|
34
|
+
|
|
11
35
|
Anti-patterns sourced from:
|
|
12
36
|
https://github.com/homeassistant-ai/skills
|
|
13
37
|
skill://home-assistant-best-practices
|
|
@@ -39,6 +63,14 @@ _RE_WEEKDAY = re.compile(
|
|
|
39
63
|
r"\bnow\(\)\s*\.\s*(?:weekday|isoweekday)\s*\("
|
|
40
64
|
r"|\bnow\(\)\s*\.\s*strftime\s*\(\s*['\"]%[Aaw]['\"]"
|
|
41
65
|
)
|
|
66
|
+
# Date-component checks: now().date(), now().year/month/day.
|
|
67
|
+
# `\b` after year/month/day prevents matching `day_of_week`/`day_of_year`/etc.;
|
|
68
|
+
# `(?!\s*\()` rejects method-call shapes like `now().day()` that don't exist
|
|
69
|
+
# in HA's Jinja env.
|
|
70
|
+
_RE_NOW_DATE = re.compile(
|
|
71
|
+
r"\bnow\(\)\s*\.\s*date\s*\("
|
|
72
|
+
r"|\bnow\(\)\s*\.\s*(?:year|month|day)\b(?!\s*\()"
|
|
73
|
+
)
|
|
42
74
|
# sun.sun entity references
|
|
43
75
|
_RE_SUN = re.compile(r"(?:is_state|state_attr|states)\s*\(\s*['\"]sun\.sun['\"]")
|
|
44
76
|
# states('x') in [...] or states('x') in (...)
|
|
@@ -47,6 +79,18 @@ _RE_STATE_IN = re.compile(r"states\s*\([^)]+\)\s+in\s+[\[(]")
|
|
|
47
79
|
_RE_DIRECT_STATE = re.compile(r"\bstates\.\w+\.\w+\.state\b")
|
|
48
80
|
# Motion entity pattern
|
|
49
81
|
_RE_MOTION = re.compile(r"binary_sensor\.\w*motion", re.IGNORECASE)
|
|
82
|
+
# Any Jinja template marker — catch-all and target-field scan.
|
|
83
|
+
_RE_ANY_TEMPLATE = re.compile(r"\{\{|\{%")
|
|
84
|
+
# `this.X` self-reference (e.g. `{{ this.entity_id }}`)
|
|
85
|
+
_RE_THIS_REFERENCE = re.compile(r"\bthis\s*\.\s*\w+")
|
|
86
|
+
|
|
87
|
+
# Target sub-fields scanned for templates. These are the only keys allowed
|
|
88
|
+
# under ``target:`` in HA's modern action schema.
|
|
89
|
+
_TARGET_FIELDS = ("entity_id", "device_id", "area_id", "floor_id", "label_id")
|
|
90
|
+
|
|
91
|
+
# Keys that hold the service/action name in an action step. HA accepts both
|
|
92
|
+
# ``service:`` (legacy) and ``action:`` (modern, 2024+) for the same field.
|
|
93
|
+
_SERVICE_KEYS = ("service", "action")
|
|
50
94
|
|
|
51
95
|
|
|
52
96
|
# ---------------------------------------------------------------------------
|
|
@@ -76,7 +120,7 @@ def check_automation_config(
|
|
|
76
120
|
# Condition templates
|
|
77
121
|
_check_condition_templates(config.get("condition", []), warnings, skill_prefix)
|
|
78
122
|
|
|
79
|
-
# Action tree (wait_template + nested conditions)
|
|
123
|
+
# Action tree (wait_template + nested conditions + target templates)
|
|
80
124
|
_check_action_tree(config.get("action", []), warnings, skill_prefix)
|
|
81
125
|
|
|
82
126
|
# Trigger templates + device_id
|
|
@@ -132,12 +176,20 @@ def _check_condition_templates(
|
|
|
132
176
|
for cond in _as_list(conditions):
|
|
133
177
|
if isinstance(cond, str) and "{{" in cond:
|
|
134
178
|
# Shorthand template condition
|
|
135
|
-
_check_template_string(cond, warnings, skill_prefix)
|
|
179
|
+
_check_template_string(cond, warnings, skill_prefix, "condition")
|
|
136
180
|
elif isinstance(cond, dict):
|
|
137
181
|
if cond.get("condition") == "template":
|
|
138
182
|
vt = cond.get("value_template", "")
|
|
139
183
|
if isinstance(vt, str):
|
|
140
|
-
_check_template_string(vt, warnings, skill_prefix)
|
|
184
|
+
_check_template_string(vt, warnings, skill_prefix, "condition")
|
|
185
|
+
else:
|
|
186
|
+
# Non-template conditions (numeric_state, state, etc.) can
|
|
187
|
+
# still carry a `value_template` field (numeric_state uses one
|
|
188
|
+
# to compute the numeric value being compared). Scan it too,
|
|
189
|
+
# otherwise these templates slip past every detector.
|
|
190
|
+
vt = cond.get("value_template", "")
|
|
191
|
+
if isinstance(vt, str) and "{{" in vt:
|
|
192
|
+
_check_template_string(vt, warnings, skill_prefix, "condition")
|
|
141
193
|
# Recurse into compound conditions (and/or/not)
|
|
142
194
|
nested = cond.get("conditions")
|
|
143
195
|
if nested:
|
|
@@ -145,54 +197,99 @@ def _check_condition_templates(
|
|
|
145
197
|
|
|
146
198
|
|
|
147
199
|
def _check_template_string(
|
|
148
|
-
template: str,
|
|
200
|
+
template: str,
|
|
201
|
+
warnings: list[str],
|
|
202
|
+
skill_prefix: str | None,
|
|
203
|
+
position: str,
|
|
149
204
|
) -> None:
|
|
150
|
-
"""Check a single template string for known anti-patterns.
|
|
205
|
+
"""Check a single template string for known anti-patterns.
|
|
206
|
+
|
|
207
|
+
``position`` is currently only "condition" (the function is called from
|
|
208
|
+
``_check_condition_templates``). It's parameterized so both the warning
|
|
209
|
+
prefix AND the suggestion text adapt if a future caller passes "trigger".
|
|
210
|
+
The native shapes named here (numeric_state, state, time, sun) work as
|
|
211
|
+
both conditions and triggers in HA — only the noun changes.
|
|
212
|
+
"""
|
|
213
|
+
initial_count = len(warnings)
|
|
214
|
+
label = position.capitalize()
|
|
215
|
+
|
|
151
216
|
if _RE_NUMERIC_CMP.search(template):
|
|
152
217
|
warnings.append(
|
|
153
|
-
"
|
|
154
|
-
"`numeric_state`
|
|
218
|
+
f"{label} uses template with float/int comparison — use native "
|
|
219
|
+
f"`numeric_state` {position} instead "
|
|
220
|
+
f"(e.g., `{position}: numeric_state, entity_id: sensor.temp, above: 25`). "
|
|
221
|
+
"Native conditions are validated at config load and don't bypass HA's schema."
|
|
155
222
|
+ _ref(skill_prefix, "automation-patterns.md#native-conditions")
|
|
156
223
|
)
|
|
157
224
|
if _RE_SUN.search(template):
|
|
158
225
|
warnings.append(
|
|
159
|
-
"
|
|
160
|
-
"`sun`
|
|
226
|
+
f"{label} uses template referencing `sun.sun` — use native "
|
|
227
|
+
f"`sun` {position} instead "
|
|
228
|
+
f"(e.g., `{position}: sun, after: sunset` or `before: sunrise`)."
|
|
161
229
|
+ _ref(skill_prefix, "automation-patterns.md#native-conditions")
|
|
162
230
|
)
|
|
163
231
|
elif _RE_IS_STATE.search(template):
|
|
164
232
|
# Only flag if not already flagged as sun pattern
|
|
165
233
|
warnings.append(
|
|
166
|
-
"
|
|
167
|
-
"`state`
|
|
234
|
+
f"{label} uses template with `is_state()` — use native "
|
|
235
|
+
f"`state` {position} instead "
|
|
236
|
+
f"(e.g., `{position}: state, entity_id: light.bedroom, state: 'on'`)."
|
|
168
237
|
+ _ref(skill_prefix, "automation-patterns.md#native-conditions")
|
|
169
238
|
)
|
|
170
239
|
if _RE_NOW_TIME.search(template):
|
|
171
240
|
warnings.append(
|
|
172
|
-
"
|
|
173
|
-
"`time`
|
|
241
|
+
f"{label} uses template with `now().hour/minute` — use native "
|
|
242
|
+
f"`time` {position} instead "
|
|
243
|
+
f"(e.g., `{position}: time, after: '09:00:00', before: '17:00:00'`)."
|
|
174
244
|
+ _ref(skill_prefix, "automation-patterns.md#native-conditions")
|
|
175
245
|
)
|
|
176
246
|
if _RE_WEEKDAY.search(template):
|
|
177
247
|
warnings.append(
|
|
178
|
-
"
|
|
179
|
-
"`time`
|
|
248
|
+
f"{label} uses template for day-of-week check — use native "
|
|
249
|
+
f"`time` {position} with `weekday:` list instead "
|
|
250
|
+
f"(e.g., `{position}: time, weekday: ['mon', 'tue', 'wed']`)."
|
|
251
|
+
+ _ref(skill_prefix, "automation-patterns.md#native-conditions")
|
|
252
|
+
)
|
|
253
|
+
if _RE_NOW_DATE.search(template):
|
|
254
|
+
warnings.append(
|
|
255
|
+
f"{label} uses date-based check (`now().date()` / `now().year/month/day`) — "
|
|
256
|
+
"for one-shot date-specific firing, use a `time` trigger and self-disable via "
|
|
257
|
+
"`automation.turn_off` with a hardcoded `entity_id` (the next `00:01` fire IS the "
|
|
258
|
+
"target date on creation day). For recurring date logic, expose a `sensor.date` via "
|
|
259
|
+
f"the `time_date` integration and use a `state` {position}."
|
|
180
260
|
+ _ref(skill_prefix, "automation-patterns.md#native-conditions")
|
|
181
261
|
)
|
|
182
262
|
if _RE_STATE_IN.search(template):
|
|
183
263
|
warnings.append(
|
|
184
|
-
"
|
|
185
|
-
"`state`
|
|
264
|
+
f"{label} uses template with `states(...) in [...]` — use native "
|
|
265
|
+
f"`state` {position} with `state:` list instead "
|
|
266
|
+
f"(e.g., `{position}: state, entity_id: climate.living_room, state: ['heat', 'cool']`)."
|
|
186
267
|
+ _ref(skill_prefix, "automation-patterns.md#native-conditions")
|
|
187
268
|
)
|
|
188
269
|
if _RE_DIRECT_STATE.search(template):
|
|
189
270
|
warnings.append(
|
|
190
|
-
"
|
|
191
|
-
"errors if entity doesn't exist — use `states('entity_id')` "
|
|
192
|
-
"function instead."
|
|
271
|
+
f"{label} template uses `states.domain.entity.state` direct access which "
|
|
272
|
+
"errors if entity doesn't exist — use the `states('entity_id')` "
|
|
273
|
+
"function instead (returns 'unknown' if missing rather than raising)."
|
|
193
274
|
+ _ref(skill_prefix, "template-guidelines.md#common-patterns")
|
|
194
275
|
)
|
|
195
276
|
|
|
277
|
+
# Generic fallback: any Jinja in this logic position that didn't match
|
|
278
|
+
# a specific detector. Catches new anti-patterns (issue #1011) and
|
|
279
|
+
# reframes #695 from "enumerate bad shapes" to "surface every template
|
|
280
|
+
# in a logic position". Specific detectors above keep their tailored
|
|
281
|
+
# messages.
|
|
282
|
+
if (
|
|
283
|
+
len(warnings) == initial_count
|
|
284
|
+
and _RE_ANY_TEMPLATE.search(template)
|
|
285
|
+
):
|
|
286
|
+
warnings.append(
|
|
287
|
+
f"Template detected in {position} — if this maps to a native option "
|
|
288
|
+
"(`numeric_state`, `state`, `time`, `sun`, `zone`, `device`), use that "
|
|
289
|
+
"instead. Templates fail silently at runtime and bypass schema validation."
|
|
290
|
+
+ _ref(skill_prefix, "template-guidelines.md#when-to-avoid-templates")
|
|
291
|
+
)
|
|
292
|
+
|
|
196
293
|
|
|
197
294
|
# ---------------------------------------------------------------------------
|
|
198
295
|
# Action tree checks
|
|
@@ -223,11 +320,22 @@ def _check_repeat_actions(
|
|
|
223
320
|
def _check_action_tree(
|
|
224
321
|
actions: Any, warnings: list[str], skill_prefix: str | None
|
|
225
322
|
) -> None:
|
|
226
|
-
"""Walk action tree checking for wait_template and
|
|
323
|
+
"""Walk action tree checking for wait_template, nested conditions, and target templates."""
|
|
227
324
|
for action in _as_list(actions):
|
|
228
325
|
if not isinstance(action, dict):
|
|
229
326
|
continue
|
|
230
327
|
|
|
328
|
+
# Inline condition steps (e.g. `- condition: template, value_template: ...`
|
|
329
|
+
# in a sequence). Detect by `condition: <str>` AND no service/action key
|
|
330
|
+
# present — a service-call step uses `condition:` as a legacy run-if
|
|
331
|
+
# filter, not as a step kind. Without this branch, templates in
|
|
332
|
+
# condition shorthand inside scripts/automation actions slipped past
|
|
333
|
+
# the checker; only conditions in `if:`, `choose.conditions`, and
|
|
334
|
+
# `repeat.while/until` were inspected.
|
|
335
|
+
cond_kind = action.get("condition")
|
|
336
|
+
if isinstance(cond_kind, str) and not any(k in action for k in _SERVICE_KEYS):
|
|
337
|
+
_check_condition_templates([action], warnings, skill_prefix)
|
|
338
|
+
|
|
231
339
|
if "wait_template" in action:
|
|
232
340
|
warnings.append(
|
|
233
341
|
"Action uses `wait_template` — consider `wait_for_trigger` "
|
|
@@ -237,6 +345,20 @@ def _check_action_tree(
|
|
|
237
345
|
+ _ref(skill_prefix, "automation-patterns.md#wait-actions")
|
|
238
346
|
)
|
|
239
347
|
|
|
348
|
+
# Templated service dispatch: `service:`/`action:` containing `{{ }}`
|
|
349
|
+
# or any `service_template:` field. The native alternative is a
|
|
350
|
+
# `choose` (or `if/then/else`) action that picks between hardcoded
|
|
351
|
+
# service names based on state.
|
|
352
|
+
_check_service_template(action, warnings, skill_prefix)
|
|
353
|
+
|
|
354
|
+
# Templates in target sub-fields. Action `data`, `event_data`,
|
|
355
|
+
# `service_data`, notification message/title, and `variables` are
|
|
356
|
+
# legitimate dynamic-data positions per template-guidelines.md and
|
|
357
|
+
# are not walked by any recursion path here.
|
|
358
|
+
target = action.get("target")
|
|
359
|
+
if isinstance(target, dict):
|
|
360
|
+
_check_target_dict(target, warnings, skill_prefix)
|
|
361
|
+
|
|
240
362
|
# Nested conditions in choose/if/repeat
|
|
241
363
|
if "choose" in action:
|
|
242
364
|
_check_choose_actions(action["choose"], warnings, skill_prefix)
|
|
@@ -252,6 +374,82 @@ def _check_action_tree(
|
|
|
252
374
|
if "repeat" in action and isinstance(action["repeat"], dict):
|
|
253
375
|
_check_repeat_actions(action["repeat"], warnings, skill_prefix)
|
|
254
376
|
|
|
377
|
+
# `parallel:` runs sub-actions concurrently — same shape as `sequence`,
|
|
378
|
+
# different semantics. Recurse so templates inside parallel branches
|
|
379
|
+
# are inspected the same as templates inside choose/repeat sequences.
|
|
380
|
+
if "parallel" in action and isinstance(action["parallel"], list):
|
|
381
|
+
_check_action_tree(action["parallel"], warnings, skill_prefix)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _check_service_template(
|
|
385
|
+
action: dict[str, Any], warnings: list[str], skill_prefix: str | None
|
|
386
|
+
) -> None:
|
|
387
|
+
"""Flag template-based service dispatch in an action.
|
|
388
|
+
|
|
389
|
+
Three shapes:
|
|
390
|
+
- ``service_template:`` — legacy explicit way to template a service name.
|
|
391
|
+
Flag any value.
|
|
392
|
+
- ``service:`` containing ``{{`` — modern syntax with a template.
|
|
393
|
+
- ``action:`` containing ``{{`` — HA's 2024+ rename of ``service:``.
|
|
394
|
+
|
|
395
|
+
The native alternative is a ``choose`` (or ``if/then/else``) action that
|
|
396
|
+
dispatches to different hardcoded service names based on state.
|
|
397
|
+
"""
|
|
398
|
+
if "service_template" in action:
|
|
399
|
+
warnings.append(
|
|
400
|
+
"Action uses `service_template` (legacy templated service dispatch) — "
|
|
401
|
+
"use a `choose` (or `if/then/else`) action that dispatches to different "
|
|
402
|
+
"hardcoded `action:` names based on state. Native dispatch validates "
|
|
403
|
+
"each service name at config load."
|
|
404
|
+
+ _ref(skill_prefix, "automation-patterns.md#ifthen-vs-choose")
|
|
405
|
+
)
|
|
406
|
+
return
|
|
407
|
+
for key in _SERVICE_KEYS:
|
|
408
|
+
value = action.get(key)
|
|
409
|
+
if isinstance(value, str) and _RE_ANY_TEMPLATE.search(value):
|
|
410
|
+
warnings.append(
|
|
411
|
+
f"Action `{key}:` field contains a template — use a `choose` "
|
|
412
|
+
"(or `if/then/else`) action with hardcoded service names instead. "
|
|
413
|
+
"Templates here bypass HA's service-name validation and fail "
|
|
414
|
+
"silently if the resolved string is invalid."
|
|
415
|
+
+ _ref(skill_prefix, "automation-patterns.md#ifthen-vs-choose")
|
|
416
|
+
)
|
|
417
|
+
return
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _check_target_dict(
|
|
421
|
+
target: dict[str, Any], warnings: list[str], skill_prefix: str | None
|
|
422
|
+
) -> None:
|
|
423
|
+
"""Flag any Jinja in target.entity_id/device_id/area_id/floor_id/label_id.
|
|
424
|
+
|
|
425
|
+
Templates in target fields bypass HA's entity-existence validation at
|
|
426
|
+
config load and fail silently if they resolve to a non-existent entity.
|
|
427
|
+
`{{ this.entity_id }}`-style self-references are especially pointless —
|
|
428
|
+
the calling automation/script already knows its own entity_id, so
|
|
429
|
+
hardcoding the literal is both simpler and safer.
|
|
430
|
+
"""
|
|
431
|
+
for field in _TARGET_FIELDS:
|
|
432
|
+
value = target.get(field)
|
|
433
|
+
for item in _as_list(value):
|
|
434
|
+
if not isinstance(item, str) or not _RE_ANY_TEMPLATE.search(item):
|
|
435
|
+
continue
|
|
436
|
+
if _RE_THIS_REFERENCE.search(item):
|
|
437
|
+
warnings.append(
|
|
438
|
+
f"Action `target.{field}` uses a `this.*` self-reference template — "
|
|
439
|
+
f"hardcode the literal value instead. The self-reference is always "
|
|
440
|
+
f"resolvable at write time, so the template adds runtime cost without "
|
|
441
|
+
f"any flexibility."
|
|
442
|
+
+ _ref(skill_prefix, "template-guidelines.md#when-to-avoid-templates")
|
|
443
|
+
)
|
|
444
|
+
else:
|
|
445
|
+
warnings.append(
|
|
446
|
+
f"Action `target.{field}` uses a template — prefer a hardcoded literal, "
|
|
447
|
+
f"or use a `choose` action with native conditions to dispatch to different "
|
|
448
|
+
f"hardcoded targets. Templates in target fields fail silently if they "
|
|
449
|
+
f"resolve to a non-existent entity."
|
|
450
|
+
+ _ref(skill_prefix, "template-guidelines.md#when-to-avoid-templates")
|
|
451
|
+
)
|
|
452
|
+
|
|
255
453
|
|
|
256
454
|
# ---------------------------------------------------------------------------
|
|
257
455
|
# Trigger checks
|
|
@@ -277,14 +475,16 @@ def _check_triggers(
|
|
|
277
475
|
+ _ref(skill_prefix, "device-control.md#entity-id-vs-device-id")
|
|
278
476
|
)
|
|
279
477
|
|
|
280
|
-
# Template trigger
|
|
478
|
+
# Template trigger — specific shapes first, generic fallback after.
|
|
281
479
|
if platform == "template":
|
|
282
480
|
vt = trigger.get("value_template", "")
|
|
283
481
|
if isinstance(vt, str):
|
|
482
|
+
initial = len(warnings)
|
|
284
483
|
if _RE_NUMERIC_CMP.search(vt):
|
|
285
484
|
warnings.append(
|
|
286
485
|
"Trigger uses template with float/int comparison — "
|
|
287
|
-
"use native `numeric_state` trigger instead
|
|
486
|
+
"use native `numeric_state` trigger instead "
|
|
487
|
+
"(e.g., `platform: numeric_state, entity_id: sensor.temp, above: 30`)."
|
|
288
488
|
+ _ref(
|
|
289
489
|
skill_prefix,
|
|
290
490
|
"automation-patterns.md#trigger-types",
|
|
@@ -293,7 +493,20 @@ def _check_triggers(
|
|
|
293
493
|
if _RE_IS_STATE.search(vt):
|
|
294
494
|
warnings.append(
|
|
295
495
|
"Trigger uses template with `is_state()` — use "
|
|
296
|
-
"native `state` trigger instead
|
|
496
|
+
"native `state` trigger instead "
|
|
497
|
+
"(e.g., `platform: state, entity_id: light.x, to: 'on'`)."
|
|
498
|
+
+ _ref(
|
|
499
|
+
skill_prefix,
|
|
500
|
+
"automation-patterns.md#trigger-types",
|
|
501
|
+
)
|
|
502
|
+
)
|
|
503
|
+
# Generic fallback for unmatched template triggers.
|
|
504
|
+
if len(warnings) == initial and _RE_ANY_TEMPLATE.search(vt):
|
|
505
|
+
warnings.append(
|
|
506
|
+
"Trigger uses `template` platform — if this maps to a native option "
|
|
507
|
+
"(`state`, `numeric_state`, `time`, `time_pattern`, `sun`, `zone`, "
|
|
508
|
+
"`event`), use that instead. Native triggers are event-driven; "
|
|
509
|
+
"template triggers re-evaluate on every state change."
|
|
297
510
|
+ _ref(
|
|
298
511
|
skill_prefix,
|
|
299
512
|
"automation-patterns.md#trigger-types",
|
{ha_mcp_dev-7.4.1.dev458 → ha_mcp_dev-7.4.1.dev460}/src/ha_mcp/tools/tools_config_automations.py
RENAMED
|
@@ -371,6 +371,25 @@ class AutomationConfigTools:
|
|
|
371
371
|
"""
|
|
372
372
|
Create or update a Home Assistant automation.
|
|
373
373
|
|
|
374
|
+
PREFER NATIVE SOLUTIONS OVER TEMPLATES (read this before writing any `{{ ... }}`):
|
|
375
|
+
Native triggers/conditions/actions are validated at config load, fail loudly, and
|
|
376
|
+
do not bypass HA's schema. Templates fail silently at runtime and obscure intent.
|
|
377
|
+
- `condition: numeric_state` instead of `{{ states('x') | float > N }}`
|
|
378
|
+
- `condition: state` (with `state:` list) instead of `{{ is_state(...) }}` /
|
|
379
|
+
`{{ states(x) in [...] }}`
|
|
380
|
+
- `condition: time` instead of `{{ now().hour ... }}` or `{{ now().weekday() ... }}`
|
|
381
|
+
- `condition: sun` instead of `{{ is_state('sun.sun', ...) }}`
|
|
382
|
+
- `wait_for_trigger` instead of `wait_template`
|
|
383
|
+
- `choose` action instead of template-based service names
|
|
384
|
+
- For one-shot date firing, use a `time` trigger plus `automation.turn_off` on a
|
|
385
|
+
hardcoded entity_id — not `{{ now().date() ... }}`.
|
|
386
|
+
- Hardcode `target.entity_id` literals — never `{{ this.entity_id }}`.
|
|
387
|
+
Templates are appropriate ONLY in `data.*` fields, notification message/title,
|
|
388
|
+
`event_data`, and `variables`. The reactive best-practice checker on this tool
|
|
389
|
+
will surface anything in a logic position that should be native; consult the
|
|
390
|
+
`best_practice_warnings` field on the response and fix before re-submitting.
|
|
391
|
+
For comprehensive guidance, call `ha_get_skill_home_assistant_best_practices`.
|
|
392
|
+
|
|
374
393
|
Supports two modes: full config replacement OR Python transformation.
|
|
375
394
|
|
|
376
395
|
WHEN TO USE WHICH MODE:
|
|
@@ -478,14 +497,6 @@ class AutomationConfigTools:
|
|
|
478
497
|
}
|
|
479
498
|
)
|
|
480
499
|
|
|
481
|
-
PREFER NATIVE SOLUTIONS OVER TEMPLATES:
|
|
482
|
-
Before using template triggers/conditions/actions, check if a native option exists:
|
|
483
|
-
- Use `condition: state` with `state: [list]` instead of template for multiple states
|
|
484
|
-
- Use `condition: state` with `attribute:` instead of template for attribute checks
|
|
485
|
-
- Use `condition: numeric_state` instead of template for number comparisons
|
|
486
|
-
- Use `wait_for_trigger` instead of `wait_template` when waiting for state changes
|
|
487
|
-
- Use `choose` action instead of template-based service names
|
|
488
|
-
|
|
489
500
|
TRIGGER TYPES: time, time_pattern, sun, state, numeric_state, event, device, zone, template, and more
|
|
490
501
|
CONDITION TYPES: state, numeric_state, time, sun, template, device, zone, and more
|
|
491
502
|
ACTION TYPES: service calls, delays, wait_for_trigger, wait_template, if/then/else, choose, repeat, parallel
|