ha-mcp-dev 7.2.0.dev319__tar.gz → 7.2.0.dev321__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.2.0.dev319/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.2.0.dev321}/PKG-INFO +1 -1
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/pyproject.toml +2 -1
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_config_automations.py +58 -14
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_config_helpers.py +43 -1
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_config_scripts.py +27 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_yaml_config.py +2 -2
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/util_helpers.py +69 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/LICENSE +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/README.md +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/setup.cfg +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/smart_search.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/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.2.0.
|
|
7
|
+
version = "7.2.0.dev321"
|
|
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"
|
|
@@ -175,6 +175,7 @@ dev = [
|
|
|
175
175
|
"pytest-xdist>=3.8.0",
|
|
176
176
|
"requests>=2.25.0",
|
|
177
177
|
"lefthook>=1.10.0",
|
|
178
|
+
"ruamel.yaml>=0.18.0",
|
|
178
179
|
"ruff>=0.12.12",
|
|
179
180
|
"testcontainers>=4.13.0",
|
|
180
181
|
"ast-grep-cli>=0.42.0",
|
{ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_config_automations.py
RENAMED
|
@@ -24,7 +24,9 @@ from .best_practice_checker import (
|
|
|
24
24
|
)
|
|
25
25
|
from .helpers import exception_to_structured_error, log_tool_usage, raise_tool_error
|
|
26
26
|
from .util_helpers import (
|
|
27
|
+
apply_entity_category,
|
|
27
28
|
coerce_bool_param,
|
|
29
|
+
fetch_entity_category,
|
|
28
30
|
parse_json_param,
|
|
29
31
|
wait_for_entity_registered,
|
|
30
32
|
wait_for_entity_removed,
|
|
@@ -205,6 +207,27 @@ def _strip_empty_automation_fields(config: dict[str, Any]) -> dict[str, Any]:
|
|
|
205
207
|
def register_config_automation_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
206
208
|
"""Register Home Assistant automation configuration tools."""
|
|
207
209
|
|
|
210
|
+
async def _resolve_automation_entity_id(identifier: str) -> str | None:
|
|
211
|
+
"""Resolve an automation identifier to its entity_id.
|
|
212
|
+
|
|
213
|
+
If identifier is already an entity_id (starts with "automation."),
|
|
214
|
+
returns it directly. Otherwise, searches states to find the entity
|
|
215
|
+
whose unique_id matches the identifier.
|
|
216
|
+
"""
|
|
217
|
+
if identifier.startswith("automation."):
|
|
218
|
+
return identifier
|
|
219
|
+
try:
|
|
220
|
+
states = await client.get_states()
|
|
221
|
+
for state in states:
|
|
222
|
+
if (
|
|
223
|
+
state.get("entity_id", "").startswith("automation.")
|
|
224
|
+
and state.get("attributes", {}).get("id") == identifier
|
|
225
|
+
):
|
|
226
|
+
return str(state["entity_id"])
|
|
227
|
+
except Exception as e:
|
|
228
|
+
logger.debug(f"Failed to resolve entity_id for automation {identifier}: {e}")
|
|
229
|
+
return None
|
|
230
|
+
|
|
208
231
|
@mcp.tool(
|
|
209
232
|
tags={"Automations"},
|
|
210
233
|
annotations={
|
|
@@ -237,6 +260,14 @@ def register_config_automation_tools(mcp: Any, client: Any, **kwargs: Any) -> No
|
|
|
237
260
|
config_result = await client.get_automation_config(identifier)
|
|
238
261
|
# Normalize config for round-trip compatibility (GET → SET)
|
|
239
262
|
normalized_config = _normalize_config_for_roundtrip(config_result)
|
|
263
|
+
|
|
264
|
+
# Resolve entity_id and fetch category from entity registry
|
|
265
|
+
entity_id = await _resolve_automation_entity_id(identifier)
|
|
266
|
+
if entity_id:
|
|
267
|
+
cat_id = await fetch_entity_category(client, entity_id, "automation")
|
|
268
|
+
if cat_id:
|
|
269
|
+
normalized_config["category"] = cat_id
|
|
270
|
+
|
|
240
271
|
return {
|
|
241
272
|
"success": True,
|
|
242
273
|
"action": "get",
|
|
@@ -296,6 +327,13 @@ def register_config_automation_tools(mcp: Any, client: Any, **kwargs: Any) -> No
|
|
|
296
327
|
default=None,
|
|
297
328
|
),
|
|
298
329
|
] = None,
|
|
330
|
+
category: Annotated[
|
|
331
|
+
str | None,
|
|
332
|
+
Field(
|
|
333
|
+
description="Category ID to assign to this automation. Use ha_config_get_category(scope='automation') to list available categories, or ha_config_set_category() to create one.",
|
|
334
|
+
default=None,
|
|
335
|
+
),
|
|
336
|
+
] = None,
|
|
299
337
|
wait: Annotated[
|
|
300
338
|
bool | str,
|
|
301
339
|
Field(
|
|
@@ -327,6 +365,7 @@ def register_config_automation_tools(mcp: Any, client: Any, **kwargs: Any) -> No
|
|
|
327
365
|
|
|
328
366
|
OPTIONAL CONFIG FIELDS (Regular Automations):
|
|
329
367
|
- description: Detailed description of the user's intent (RECOMMENDED: helps safely modify implementation later)
|
|
368
|
+
- category: Category ID for organization (use ha_config_get_category to list, ha_config_set_category to create)
|
|
330
369
|
- condition: Additional conditions that must be met
|
|
331
370
|
- mode: 'single' (default), 'restart', 'queued', 'parallel'
|
|
332
371
|
- max: Maximum concurrent executions (for queued/parallel modes)
|
|
@@ -444,6 +483,11 @@ def register_config_automation_tools(mcp: Any, client: Any, **kwargs: Any) -> No
|
|
|
444
483
|
|
|
445
484
|
config_dict = cast(dict[str, Any], parsed_config)
|
|
446
485
|
|
|
486
|
+
# Extract category before sending to HA REST API (which rejects unknown keys).
|
|
487
|
+
# Parameter takes precedence over config dict value.
|
|
488
|
+
config_category = config_dict.pop("category", None)
|
|
489
|
+
effective_category = category if category is not None else config_category
|
|
490
|
+
|
|
447
491
|
# Normalize field names (triggers -> trigger, actions -> action, etc.)
|
|
448
492
|
config_dict = _normalize_automation_config(config_dict)
|
|
449
493
|
|
|
@@ -499,6 +543,9 @@ def register_config_automation_tools(mcp: Any, client: Any, **kwargs: Any) -> No
|
|
|
499
543
|
# Wait for automation to be queryable
|
|
500
544
|
wait_bool = coerce_bool_param(wait, "wait", default=True)
|
|
501
545
|
entity_id = result.get("entity_id")
|
|
546
|
+
# On updates, entity_id may not be in the result — derive from identifier
|
|
547
|
+
if not entity_id and identifier and identifier.startswith("automation."):
|
|
548
|
+
entity_id = identifier
|
|
502
549
|
if wait_bool and entity_id:
|
|
503
550
|
try:
|
|
504
551
|
registered = await wait_for_entity_registered(client, entity_id)
|
|
@@ -507,6 +554,12 @@ def register_config_automation_tools(mcp: Any, client: Any, **kwargs: Any) -> No
|
|
|
507
554
|
except Exception as e:
|
|
508
555
|
result["warning"] = f"Automation created but verification failed: {e}"
|
|
509
556
|
|
|
557
|
+
# Apply category to entity registry if provided
|
|
558
|
+
if effective_category and entity_id:
|
|
559
|
+
await apply_entity_category(
|
|
560
|
+
client, entity_id, effective_category, "automation", result, "automation"
|
|
561
|
+
)
|
|
562
|
+
|
|
510
563
|
if bp_warnings:
|
|
511
564
|
result["best_practice_warnings"] = bp_warnings
|
|
512
565
|
|
|
@@ -572,20 +625,11 @@ def register_config_automation_tools(mcp: Any, client: Any, **kwargs: Any) -> No
|
|
|
572
625
|
"""
|
|
573
626
|
try:
|
|
574
627
|
# Resolve entity_id for wait verification (identifier may be a unique_id)
|
|
575
|
-
entity_id_for_wait
|
|
576
|
-
if
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
try:
|
|
581
|
-
states = await client.get_states()
|
|
582
|
-
for state in states:
|
|
583
|
-
eid = state.get("entity_id", "")
|
|
584
|
-
if eid.startswith("automation.") and state.get("attributes", {}).get("id") == identifier:
|
|
585
|
-
entity_id_for_wait = eid
|
|
586
|
-
break
|
|
587
|
-
except Exception as e:
|
|
588
|
-
logger.warning(f"Could not resolve unique_id '{identifier}' to entity_id: {e} — wait verification will be skipped")
|
|
628
|
+
entity_id_for_wait = await _resolve_automation_entity_id(identifier)
|
|
629
|
+
if not entity_id_for_wait:
|
|
630
|
+
logger.warning(
|
|
631
|
+
f"Could not resolve unique_id '{identifier}' to entity_id — wait verification will be skipped"
|
|
632
|
+
)
|
|
589
633
|
|
|
590
634
|
result = await client.delete_automation_config(identifier)
|
|
591
635
|
|
{ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_config_helpers.py
RENAMED
|
@@ -16,6 +16,7 @@ from pydantic import Field
|
|
|
16
16
|
from ..errors import ErrorCode, create_error_response
|
|
17
17
|
from .helpers import exception_to_structured_error, log_tool_usage, raise_tool_error
|
|
18
18
|
from .util_helpers import (
|
|
19
|
+
apply_entity_category,
|
|
19
20
|
coerce_bool_param,
|
|
20
21
|
parse_string_list_param,
|
|
21
22
|
wait_for_entity_registered,
|
|
@@ -405,6 +406,13 @@ def register_config_helper_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
405
406
|
default=None,
|
|
406
407
|
),
|
|
407
408
|
] = None,
|
|
409
|
+
category: Annotated[
|
|
410
|
+
str | None,
|
|
411
|
+
Field(
|
|
412
|
+
description="Category ID to assign to this helper. Use ha_config_get_category(scope='helpers') to list available categories, or ha_config_set_category() to create one.",
|
|
413
|
+
default=None,
|
|
414
|
+
),
|
|
415
|
+
] = None,
|
|
408
416
|
wait: Annotated[
|
|
409
417
|
bool | str,
|
|
410
418
|
Field(
|
|
@@ -632,6 +640,9 @@ def register_config_helper_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
632
640
|
if result.get("success"):
|
|
633
641
|
helper_data = result.get("result", {})
|
|
634
642
|
entity_id = helper_data.get("entity_id")
|
|
643
|
+
# Some helper types don't return entity_id — derive from result id
|
|
644
|
+
if not entity_id and helper_data.get("id"):
|
|
645
|
+
entity_id = f"{helper_type}.{helper_data['id']}"
|
|
635
646
|
|
|
636
647
|
# Wait for entity to be properly registered before proceeding
|
|
637
648
|
wait_bool = coerce_bool_param(wait, "wait", default=True)
|
|
@@ -660,6 +671,18 @@ def register_config_helper_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
660
671
|
if update_result.get("success"):
|
|
661
672
|
helper_data["area_id"] = area_id
|
|
662
673
|
helper_data["labels"] = labels
|
|
674
|
+
else:
|
|
675
|
+
error_detail = update_result.get("error", {})
|
|
676
|
+
error_msg = error_detail.get("message", "Unknown error") if isinstance(error_detail, dict) else str(error_detail)
|
|
677
|
+
helper_data["warning"] = (
|
|
678
|
+
f"Helper created but entity registry update failed: {error_msg}"
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
# Apply category via shared helper (consistent with automations/scripts)
|
|
682
|
+
if category and entity_id:
|
|
683
|
+
await apply_entity_category(
|
|
684
|
+
client, entity_id, category, "helpers", helper_data, "helper"
|
|
685
|
+
)
|
|
663
686
|
|
|
664
687
|
return {
|
|
665
688
|
"success": True,
|
|
@@ -890,7 +913,20 @@ def register_config_helper_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
890
913
|
registry_update["area_id"] = area_id
|
|
891
914
|
if labels:
|
|
892
915
|
registry_update["labels"] = labels
|
|
893
|
-
await client.send_websocket_message(registry_update)
|
|
916
|
+
reg_result = await client.send_websocket_message(registry_update)
|
|
917
|
+
if not reg_result.get("success"):
|
|
918
|
+
error_detail = reg_result.get("error", {})
|
|
919
|
+
error_msg = error_detail.get("message", "Unknown error") if isinstance(error_detail, dict) else str(error_detail)
|
|
920
|
+
logger.warning(f"Entity registry update failed for {entity_id}: {error_msg}")
|
|
921
|
+
updated_data["warning"] = (
|
|
922
|
+
f"Config updated but entity registry update failed: {error_msg}"
|
|
923
|
+
)
|
|
924
|
+
|
|
925
|
+
# Apply category via shared helper
|
|
926
|
+
if category:
|
|
927
|
+
await apply_entity_category(
|
|
928
|
+
client, entity_id, category, "helpers", updated_data, "helper"
|
|
929
|
+
)
|
|
894
930
|
|
|
895
931
|
else:
|
|
896
932
|
# Standard helpers: entity registry update only
|
|
@@ -919,6 +955,12 @@ def register_config_helper_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
919
955
|
context={"helper_type": helper_type, "entity_id": entity_id},
|
|
920
956
|
))
|
|
921
957
|
|
|
958
|
+
# Apply category via shared helper
|
|
959
|
+
if category:
|
|
960
|
+
await apply_entity_category(
|
|
961
|
+
client, entity_id, category, "helpers", updated_data, "helper"
|
|
962
|
+
)
|
|
963
|
+
|
|
922
964
|
# Wait for entity to reflect the update
|
|
923
965
|
wait_bool = coerce_bool_param(wait, "wait", default=True)
|
|
924
966
|
response: dict[str, Any] = {
|
{ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_config_scripts.py
RENAMED
|
@@ -20,7 +20,9 @@ from .best_practice_checker import (
|
|
|
20
20
|
)
|
|
21
21
|
from .helpers import exception_to_structured_error, log_tool_usage, raise_tool_error
|
|
22
22
|
from .util_helpers import (
|
|
23
|
+
apply_entity_category,
|
|
23
24
|
coerce_bool_param,
|
|
25
|
+
fetch_entity_category,
|
|
24
26
|
parse_json_param,
|
|
25
27
|
wait_for_entity_registered,
|
|
26
28
|
wait_for_entity_removed,
|
|
@@ -82,6 +84,13 @@ def register_config_script_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
82
84
|
"""
|
|
83
85
|
try:
|
|
84
86
|
config_result = await client.get_script_config(script_id)
|
|
87
|
+
|
|
88
|
+
# Fetch category from entity registry (best-effort)
|
|
89
|
+
entity_id = f"script.{script_id}"
|
|
90
|
+
cat_id = await fetch_entity_category(client, entity_id, "script")
|
|
91
|
+
if cat_id:
|
|
92
|
+
config_result["category"] = cat_id
|
|
93
|
+
|
|
85
94
|
return {
|
|
86
95
|
"success": True,
|
|
87
96
|
"action": "get",
|
|
@@ -119,6 +128,13 @@ def register_config_script_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
119
128
|
description="Script configuration dictionary. Must include EITHER 'sequence' (for regular scripts) OR 'use_blueprint' (for blueprint-based scripts). Optional fields: 'alias', 'description', 'icon', 'mode', 'max', 'fields'"
|
|
120
129
|
),
|
|
121
130
|
],
|
|
131
|
+
category: Annotated[
|
|
132
|
+
str | None,
|
|
133
|
+
Field(
|
|
134
|
+
description="Category ID to assign to this script. Use ha_config_get_category(scope='script') to list available categories, or ha_config_set_category() to create one.",
|
|
135
|
+
default=None,
|
|
136
|
+
),
|
|
137
|
+
] = None,
|
|
122
138
|
wait: Annotated[
|
|
123
139
|
bool | str,
|
|
124
140
|
Field(
|
|
@@ -259,6 +275,11 @@ def register_config_script_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
259
275
|
|
|
260
276
|
config_dict = cast(dict[str, Any], parsed_config)
|
|
261
277
|
|
|
278
|
+
# Extract category before sending to HA REST API (which rejects unknown keys).
|
|
279
|
+
# Parameter takes precedence over config dict value.
|
|
280
|
+
config_category = config_dict.pop("category", None)
|
|
281
|
+
effective_category = category if category is not None else config_category
|
|
282
|
+
|
|
262
283
|
# Validate required fields based on script type
|
|
263
284
|
# Blueprint scripts only need use_blueprint, regular scripts need sequence
|
|
264
285
|
if "use_blueprint" in config_dict:
|
|
@@ -289,6 +310,12 @@ def register_config_script_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
289
310
|
except Exception as e:
|
|
290
311
|
result["warning"] = f"Script created but verification failed: {e}"
|
|
291
312
|
|
|
313
|
+
# Apply category to entity registry if provided
|
|
314
|
+
if effective_category and entity_id:
|
|
315
|
+
await apply_entity_category(
|
|
316
|
+
client, entity_id, effective_category, "script", result, "script"
|
|
317
|
+
)
|
|
318
|
+
|
|
292
319
|
if bp_warnings:
|
|
293
320
|
result["best_practice_warnings"] = bp_warnings
|
|
294
321
|
|
|
@@ -123,8 +123,8 @@ def register_yaml_config_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
123
123
|
a full HA restart ('restart_required'). Only template, mqtt, and
|
|
124
124
|
group support reload ('reload_available' with 'reload_service').
|
|
125
125
|
|
|
126
|
-
|
|
127
|
-
|
|
126
|
+
YAML comments and Home Assistant tags (!include, !secret, etc.)
|
|
127
|
+
are preserved through edits.
|
|
128
128
|
"""
|
|
129
129
|
try:
|
|
130
130
|
# Validate action
|
|
@@ -426,3 +426,72 @@ async def wait_for_state_change(
|
|
|
426
426
|
|
|
427
427
|
logger.warning(f"Entity {entity_id} state did not change within {timeout}s")
|
|
428
428
|
return None
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
async def fetch_entity_category(
|
|
432
|
+
client: Any, entity_id: str, scope: str
|
|
433
|
+
) -> str | None:
|
|
434
|
+
"""Fetch a category ID for an entity from the entity registry.
|
|
435
|
+
|
|
436
|
+
Args:
|
|
437
|
+
client: HomeAssistantClient instance
|
|
438
|
+
entity_id: Entity to look up (e.g., 'automation.morning_routine')
|
|
439
|
+
scope: Category scope (e.g., 'automation', 'script', 'helpers')
|
|
440
|
+
|
|
441
|
+
Returns:
|
|
442
|
+
Category ID string if set, None otherwise
|
|
443
|
+
"""
|
|
444
|
+
try:
|
|
445
|
+
result = await client.send_websocket_message(
|
|
446
|
+
{"type": "config/entity_registry/get", "entity_id": entity_id}
|
|
447
|
+
)
|
|
448
|
+
if result.get("success"):
|
|
449
|
+
categories = result.get("result", {}).get("categories", {})
|
|
450
|
+
cat_id = categories.get(scope)
|
|
451
|
+
return str(cat_id) if cat_id is not None else None
|
|
452
|
+
except Exception as e:
|
|
453
|
+
logger.warning(f"Failed to fetch category for {entity_id}: {e}")
|
|
454
|
+
return None
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
async def apply_entity_category(
|
|
458
|
+
client: Any,
|
|
459
|
+
entity_id: str,
|
|
460
|
+
category: str,
|
|
461
|
+
scope: str,
|
|
462
|
+
result_dict: dict[str, Any],
|
|
463
|
+
entity_type: str = "entity",
|
|
464
|
+
) -> None:
|
|
465
|
+
"""Apply a category to an entity via the entity registry.
|
|
466
|
+
|
|
467
|
+
Updates result_dict in-place with 'category' on success or
|
|
468
|
+
'category_warning' on failure.
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
client: HomeAssistantClient instance
|
|
472
|
+
entity_id: Entity to update
|
|
473
|
+
category: Category ID to assign
|
|
474
|
+
scope: Category scope (e.g., 'automation', 'script')
|
|
475
|
+
result_dict: Tool result dict to update with category status
|
|
476
|
+
entity_type: Human-readable type for warning messages
|
|
477
|
+
"""
|
|
478
|
+
try:
|
|
479
|
+
ws_result = await client.send_websocket_message({
|
|
480
|
+
"type": "config/entity_registry/update",
|
|
481
|
+
"entity_id": entity_id,
|
|
482
|
+
"categories": {scope: category},
|
|
483
|
+
})
|
|
484
|
+
if ws_result.get("success"):
|
|
485
|
+
result_dict["category"] = category
|
|
486
|
+
else:
|
|
487
|
+
error_detail = ws_result.get("error", {})
|
|
488
|
+
error_msg = error_detail.get("message", "Unknown error") if isinstance(error_detail, dict) else str(error_detail)
|
|
489
|
+
logger.warning(f"Failed to set category for {entity_id}: {error_msg}")
|
|
490
|
+
result_dict["category_warning"] = (
|
|
491
|
+
f"{entity_type.capitalize()} saved but failed to set category: {error_msg}"
|
|
492
|
+
)
|
|
493
|
+
except Exception as e:
|
|
494
|
+
logger.warning(f"Failed to set category for {entity_id}: {e}")
|
|
495
|
+
result_dict["category_warning"] = (
|
|
496
|
+
f"{entity_type.capitalize()} saved but failed to set category: {e}"
|
|
497
|
+
)
|
|
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.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/resources/skills-vendor/AGENTS.md
RENAMED
|
File without changes
|
{ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/resources/skills-vendor/CLAUDE.md
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/resources/skills-vendor/LICENSE
RENAMED
|
File without changes
|
{ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/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.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/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.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_config_dashboards.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_config_entry_flow.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
|
{ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_voice_assistant.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/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.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp_dev.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/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
|