ha-mcp-dev 7.6.0.dev675__tar.gz → 7.6.0.dev676__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.6.0.dev675/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.6.0.dev676}/PKG-INFO +1 -1
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/pyproject.toml +1 -1
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/best_practice_checker.py +15 -5
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_config_automations.py +142 -108
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_search.py +1 -1
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_service.py +4 -1
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/utils/domain_handlers.py +4 -1
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/LICENSE +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/README.md +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/setup.cfg +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/backup_manager.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/policy/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/policy/approval_queue.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/policy/evaluator.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/policy/handlers.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/policy/middleware.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/policy/model.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/policy/persistence.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/policy/value_sources.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/settings.css +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/settings.js +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/settings_ui.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/auto_backup.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/smart_search/_base.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/smart_search/_config.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/validation_middleware.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/utils/skill_loader.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/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.6.0.
|
|
7
|
+
version = "7.6.0.dev676"
|
|
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"
|
{ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/best_practice_checker.py
RENAMED
|
@@ -197,14 +197,24 @@ def check_automation_config(
|
|
|
197
197
|
|
|
198
198
|
warnings = BestPracticeCheckResult()
|
|
199
199
|
|
|
200
|
+
# Read the canonical 2024.10+ plural root keys, falling back to the singular
|
|
201
|
+
# aliases. The internal pipeline always pre-normalizes to plural, so the
|
|
202
|
+
# fallback is defensive for direct/public callers of check_automation_config
|
|
203
|
+
# (HA accepts both forms). Mirrors _check_triggers' platform/trigger tolerance.
|
|
200
204
|
# Condition templates
|
|
201
|
-
_check_condition_templates(
|
|
205
|
+
_check_condition_templates(
|
|
206
|
+
config.get("conditions", config.get("condition", [])), warnings, skill_prefix
|
|
207
|
+
)
|
|
202
208
|
|
|
203
209
|
# Action tree (wait_template + nested conditions + target templates)
|
|
204
|
-
_check_action_tree(
|
|
210
|
+
_check_action_tree(
|
|
211
|
+
config.get("actions", config.get("action", [])), warnings, skill_prefix
|
|
212
|
+
)
|
|
205
213
|
|
|
206
214
|
# Trigger templates + device_id
|
|
207
|
-
_check_triggers(
|
|
215
|
+
_check_triggers(
|
|
216
|
+
config.get("triggers", config.get("trigger", [])), warnings, skill_prefix
|
|
217
|
+
)
|
|
208
218
|
|
|
209
219
|
# Mode vs motion pattern
|
|
210
220
|
_check_mode_motion(config, warnings, skill_prefix)
|
|
@@ -738,7 +748,7 @@ def _check_mode_motion(
|
|
|
738
748
|
if mode != "single":
|
|
739
749
|
return
|
|
740
750
|
|
|
741
|
-
triggers = _as_list(config.get("trigger", []))
|
|
751
|
+
triggers = _as_list(config.get("triggers", config.get("trigger", [])))
|
|
742
752
|
has_motion = any(
|
|
743
753
|
isinstance(t, dict)
|
|
744
754
|
and any(
|
|
@@ -750,7 +760,7 @@ def _check_mode_motion(
|
|
|
750
760
|
if not has_motion:
|
|
751
761
|
return
|
|
752
762
|
|
|
753
|
-
if _has_delay_or_wait(config.get("action", [])):
|
|
763
|
+
if _has_delay_or_wait(config.get("actions", config.get("action", []))):
|
|
754
764
|
_emit(
|
|
755
765
|
warnings,
|
|
756
766
|
"Automation uses motion trigger with delay/wait but "
|
{ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_config_automations.py
RENAMED
|
@@ -82,56 +82,37 @@ NOT_VERIFIED_WARNING_PREFIX = (
|
|
|
82
82
|
)
|
|
83
83
|
|
|
84
84
|
|
|
85
|
-
def _normalize_automation_config(
|
|
86
|
-
config: Any,
|
|
87
|
-
parent_key: str | None = None,
|
|
88
|
-
in_choose_or_if: bool = False,
|
|
89
|
-
is_root: bool = True,
|
|
90
|
-
) -> Any:
|
|
85
|
+
def _normalize_automation_config(config: Any, is_root: bool = True) -> Any:
|
|
91
86
|
"""
|
|
92
|
-
Recursively normalize automation config field names to HA
|
|
93
|
-
|
|
94
|
-
Home Assistant
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
'
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
'condition' (singular).
|
|
87
|
+
Recursively normalize automation config field names to HA's canonical form.
|
|
88
|
+
|
|
89
|
+
Home Assistant's 2024.10+ canonical form uses the plural root list keys
|
|
90
|
+
('triggers', 'actions', 'conditions'); the singular forms ('trigger',
|
|
91
|
+
'action', 'condition') remain fully accepted as silent aliases. This tool
|
|
92
|
+
canonicalizes to the plural root forms so the config-API round-trip and the
|
|
93
|
+
downstream validators / best-practice checker all see one stable, modern
|
|
94
|
+
shape (HA accepts whichever we send).
|
|
95
|
+
|
|
96
|
+
Only the ROOT list keys are pluralized. The singular keys that act as type
|
|
97
|
+
discriminators or service calls inside trigger/condition/action items
|
|
98
|
+
('trigger:' = trigger type, 'condition:' = condition type, 'action:' =
|
|
99
|
+
service call) are semantically different and are left untouched, as is the
|
|
100
|
+
singular 'sequence' key inside choose/if options and scripts. The nested
|
|
101
|
+
'conditions' lists required inside choose/if and compound (or/and/not)
|
|
102
|
+
blocks are already plural and pass through unchanged (issue #498: never
|
|
103
|
+
rewrite these deeper keys).
|
|
110
104
|
|
|
111
105
|
Args:
|
|
112
106
|
config: Automation configuration (dict, list, or primitive)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
'conditions' (plural) to remain unchanged
|
|
116
|
-
is_root: Whether this is the root-level automation config dict.
|
|
117
|
-
Only root level gets 'triggers'->'trigger' and
|
|
118
|
-
'actions'->'action' normalization.
|
|
107
|
+
is_root: Whether this is the root-level automation config dict. Only the
|
|
108
|
+
root level gets the singular -> plural list-key normalization.
|
|
119
109
|
|
|
120
110
|
Returns:
|
|
121
|
-
Normalized configuration with
|
|
122
|
-
but preserving 'conditions' (plural) inside choose/if blocks and
|
|
123
|
-
compound condition blocks (or/and/not)
|
|
111
|
+
Normalized configuration with plural list field names at the root level.
|
|
124
112
|
"""
|
|
125
113
|
# Handle lists - recursively process each item
|
|
126
114
|
if isinstance(config, list):
|
|
127
|
-
|
|
128
|
-
is_option_list = parent_key in ("choose", "if")
|
|
129
|
-
return [
|
|
130
|
-
_normalize_automation_config(
|
|
131
|
-
item, parent_key, is_option_list, is_root=False
|
|
132
|
-
)
|
|
133
|
-
for item in config
|
|
134
|
-
]
|
|
115
|
+
return [_normalize_automation_config(item, is_root=False) for item in config]
|
|
135
116
|
|
|
136
117
|
# Handle primitives (strings, numbers, etc.)
|
|
137
118
|
if not isinstance(config, dict):
|
|
@@ -140,39 +121,32 @@ def _normalize_automation_config(
|
|
|
140
121
|
# Process dictionary
|
|
141
122
|
normalized = config.copy()
|
|
142
123
|
|
|
143
|
-
#
|
|
144
|
-
# that needs its nested 'conditions' key preserved
|
|
145
|
-
is_compound_condition_block = normalized.get("condition") in ("or", "and", "not")
|
|
146
|
-
|
|
147
|
-
# Build field mappings based on context
|
|
124
|
+
# Build field mappings (source alias -> canonical key).
|
|
148
125
|
field_mappings: dict[str, str] = {}
|
|
149
126
|
|
|
150
|
-
#
|
|
151
|
-
#
|
|
152
|
-
#
|
|
127
|
+
# Pluralize the root list keys to HA's 2024.10+ canonical form. ONLY at the
|
|
128
|
+
# root level: deeper in the tree 'trigger'/'action'/'condition' are type
|
|
129
|
+
# discriminators / service calls, not list keys, and must not be touched
|
|
130
|
+
# (e.g., 'action' inside a delay object -- see issue #498).
|
|
153
131
|
if is_root:
|
|
154
|
-
field_mappings["
|
|
155
|
-
field_mappings["
|
|
132
|
+
field_mappings["trigger"] = "triggers"
|
|
133
|
+
field_mappings["action"] = "actions"
|
|
134
|
+
field_mappings["condition"] = "conditions"
|
|
156
135
|
|
|
157
|
-
# 'sequences' -> 'sequence' is
|
|
136
|
+
# 'sequences' -> 'sequence': the canonical key is singular at any level.
|
|
158
137
|
field_mappings["sequences"] = "sequence"
|
|
159
138
|
|
|
160
|
-
#
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
if plural in normalized and singular not in normalized:
|
|
168
|
-
normalized[singular] = normalized.pop(plural)
|
|
169
|
-
elif plural in normalized and singular in normalized:
|
|
170
|
-
# Both exist - prefer singular, remove plural
|
|
171
|
-
del normalized[plural]
|
|
139
|
+
# Apply field mapping to current level, preferring the canonical key.
|
|
140
|
+
for src, dst in field_mappings.items():
|
|
141
|
+
if src in normalized and dst not in normalized:
|
|
142
|
+
normalized[dst] = normalized.pop(src)
|
|
143
|
+
elif src in normalized and dst in normalized:
|
|
144
|
+
# Both present -- prefer the canonical key, drop the alias.
|
|
145
|
+
del normalized[src]
|
|
172
146
|
|
|
173
147
|
# Recursively process all values in the dictionary
|
|
174
148
|
for key, value in normalized.items():
|
|
175
|
-
normalized[key] = _normalize_automation_config(value,
|
|
149
|
+
normalized[key] = _normalize_automation_config(value, is_root=False)
|
|
176
150
|
|
|
177
151
|
return normalized
|
|
178
152
|
|
|
@@ -181,21 +155,32 @@ def _normalize_trigger_keys(triggers: list[dict[str, Any]]) -> list[dict[str, An
|
|
|
181
155
|
"""
|
|
182
156
|
Normalize trigger objects for round-trip compatibility.
|
|
183
157
|
|
|
184
|
-
Home Assistant
|
|
185
|
-
|
|
158
|
+
Older Home Assistant configs (and some integrations) still emit triggers
|
|
159
|
+
keyed by the legacy 'platform'. This tool canonicalizes each trigger to the
|
|
160
|
+
modern 'trigger' key (HA 2024.10+) so its pipeline and round-trip output use
|
|
161
|
+
one stable, current shape; HA accepts either form on the SET side.
|
|
186
162
|
|
|
187
163
|
Args:
|
|
188
164
|
triggers: List of trigger configuration dicts
|
|
189
165
|
|
|
190
166
|
Returns:
|
|
191
|
-
List of triggers with '
|
|
167
|
+
List of triggers with 'trigger' key instead of 'platform' key
|
|
192
168
|
"""
|
|
193
169
|
normalized_triggers = []
|
|
194
170
|
for trigger in triggers:
|
|
171
|
+
# Defensive: a malformed (e.g. LLM-generated) item may not be a dict.
|
|
172
|
+
if not isinstance(trigger, dict):
|
|
173
|
+
normalized_triggers.append(trigger)
|
|
174
|
+
continue
|
|
195
175
|
normalized_trigger = trigger.copy()
|
|
196
|
-
# Convert '
|
|
197
|
-
|
|
198
|
-
|
|
176
|
+
# Convert legacy 'platform' to modern 'trigger'. If both are present,
|
|
177
|
+
# drop the legacy alias so HA's strict schema doesn't reject the config
|
|
178
|
+
# with "extra keys not allowed" (mirrors _normalize_automation_config).
|
|
179
|
+
if "platform" in normalized_trigger:
|
|
180
|
+
if "trigger" not in normalized_trigger:
|
|
181
|
+
normalized_trigger["trigger"] = normalized_trigger.pop("platform")
|
|
182
|
+
else:
|
|
183
|
+
del normalized_trigger["platform"]
|
|
199
184
|
normalized_triggers.append(normalized_trigger)
|
|
200
185
|
return normalized_triggers
|
|
201
186
|
|
|
@@ -246,8 +231,9 @@ def _normalize_config_for_roundtrip(config: dict[str, Any]) -> dict[str, Any]:
|
|
|
246
231
|
directly passed to ha_config_set_automation without modification.
|
|
247
232
|
|
|
248
233
|
Transformations:
|
|
249
|
-
1. Field names:
|
|
250
|
-
|
|
234
|
+
1. Field names: canonicalized to plural root keys (triggers/actions/conditions);
|
|
235
|
+
a stray `sequences` key is normalized to `sequence`.
|
|
236
|
+
2. Trigger keys: platform -> trigger (inside each trigger object)
|
|
251
237
|
|
|
252
238
|
Args:
|
|
253
239
|
config: Raw automation configuration from HA API
|
|
@@ -255,16 +241,46 @@ def _normalize_config_for_roundtrip(config: dict[str, Any]) -> dict[str, Any]:
|
|
|
255
241
|
Returns:
|
|
256
242
|
Normalized configuration compatible with SET API
|
|
257
243
|
"""
|
|
258
|
-
# First normalize field names (
|
|
244
|
+
# First normalize field names (singular -> plural at the root level)
|
|
259
245
|
normalized = _normalize_automation_config(config)
|
|
260
246
|
|
|
261
|
-
# Then normalize trigger keys (
|
|
262
|
-
if "
|
|
263
|
-
normalized["
|
|
247
|
+
# Then normalize trigger keys (legacy 'platform' -> modern 'trigger')
|
|
248
|
+
if "triggers" in normalized and isinstance(normalized["triggers"], list):
|
|
249
|
+
normalized["triggers"] = _normalize_trigger_keys(normalized["triggers"])
|
|
264
250
|
|
|
265
251
|
return cast(dict[str, Any], normalized)
|
|
266
252
|
|
|
267
253
|
|
|
254
|
+
def _detect_conflicting_root_keys(config: Any) -> list[str]:
|
|
255
|
+
"""Warn when a config carries BOTH a singular alias and its canonical plural
|
|
256
|
+
root key with *different* values.
|
|
257
|
+
|
|
258
|
+
``_normalize_automation_config`` keeps the canonical plural and silently drops
|
|
259
|
+
the singular alias, so a caller that set, say, ``config['trigger']`` on a
|
|
260
|
+
config that already has ``triggers`` would have that change discarded. The
|
|
261
|
+
config is malformed (a caller should send one form), but surfacing the
|
|
262
|
+
conflict beats dropping data silently.
|
|
263
|
+
"""
|
|
264
|
+
if not isinstance(config, dict):
|
|
265
|
+
return []
|
|
266
|
+
warnings: list[str] = []
|
|
267
|
+
for singular, plural in (
|
|
268
|
+
("trigger", "triggers"),
|
|
269
|
+
("action", "actions"),
|
|
270
|
+
("condition", "conditions"),
|
|
271
|
+
):
|
|
272
|
+
if (
|
|
273
|
+
singular in config
|
|
274
|
+
and plural in config
|
|
275
|
+
and config[singular] != config[plural]
|
|
276
|
+
):
|
|
277
|
+
warnings.append(
|
|
278
|
+
f"Config contains both '{singular}' and '{plural}' with different "
|
|
279
|
+
f"values; using the canonical '{plural}' and ignoring '{singular}'."
|
|
280
|
+
)
|
|
281
|
+
return warnings
|
|
282
|
+
|
|
283
|
+
|
|
268
284
|
def _strip_redundant_identifier_echo(
|
|
269
285
|
result: dict[str, Any],
|
|
270
286
|
*,
|
|
@@ -420,8 +436,8 @@ class AutomationConfigTools:
|
|
|
420
436
|
config: Annotated[
|
|
421
437
|
dict[str, Any] | None,
|
|
422
438
|
Field(
|
|
423
|
-
description="Complete automation configuration with required fields: 'alias', '
|
|
424
|
-
"Optional: 'description', '
|
|
439
|
+
description="Complete automation configuration with required fields: 'alias', 'triggers', 'actions'. "
|
|
440
|
+
"Optional: 'description', 'conditions', 'mode', 'max', 'initial_state', 'variables'. "
|
|
425
441
|
"Mutually exclusive with python_transform.",
|
|
426
442
|
default=None,
|
|
427
443
|
),
|
|
@@ -442,8 +458,8 @@ class AutomationConfigTools:
|
|
|
442
458
|
"Requires identifier and config_hash for validation. "
|
|
443
459
|
"WARNING: Expressions with infinite loops will hang the server. "
|
|
444
460
|
"Examples: "
|
|
445
|
-
"Simple: python_transform=\"config['
|
|
446
|
-
"Pattern: python_transform=\"for a in config['
|
|
461
|
+
"Simple: python_transform=\"config['actions'][0]['data']['brightness'] = 255\" "
|
|
462
|
+
"Pattern: python_transform=\"for a in config['actions']: "
|
|
447
463
|
"if a.get('alias') == 'My Step': a['data']['value'] = 100\" "
|
|
448
464
|
"\n\n" + get_security_documentation(),
|
|
449
465
|
),
|
|
@@ -527,10 +543,11 @@ class AutomationConfigTools:
|
|
|
527
543
|
|
|
528
544
|
IMPORTANT: python_transform requires 'identifier' and 'config_hash' from ha_config_get_automation().
|
|
529
545
|
|
|
530
|
-
PYTHON TRANSFORM EXAMPLES
|
|
531
|
-
|
|
532
|
-
-
|
|
533
|
-
-
|
|
546
|
+
PYTHON TRANSFORM EXAMPLES (operate on the fetched config, which uses HA's
|
|
547
|
+
canonical plural root keys 'triggers'/'actions'/'conditions'):
|
|
548
|
+
- Update action: python_transform="config['actions'][0]['data']['brightness'] = 255"
|
|
549
|
+
- Add trigger: python_transform="config['triggers'].append({'trigger': 'state', 'entity_id': 'binary_sensor.motion', 'to': 'on'})"
|
|
550
|
+
- Remove last action: python_transform="config['actions'].pop()"
|
|
534
551
|
|
|
535
552
|
Creates a new automation (if identifier omitted) or updates existing automation with provided configuration.
|
|
536
553
|
|
|
@@ -541,8 +558,8 @@ class AutomationConfigTools:
|
|
|
541
558
|
|
|
542
559
|
REQUIRED FIELDS (Regular Automations):
|
|
543
560
|
- alias: Human-readable automation name
|
|
544
|
-
-
|
|
545
|
-
-
|
|
561
|
+
- triggers: List of triggers (time, state, event, etc.)
|
|
562
|
+
- actions: List of actions to execute
|
|
546
563
|
|
|
547
564
|
REQUIRED FIELDS (Blueprint Automations):
|
|
548
565
|
- alias: Human-readable automation name
|
|
@@ -553,7 +570,7 @@ class AutomationConfigTools:
|
|
|
553
570
|
OPTIONAL CONFIG FIELDS (Regular Automations):
|
|
554
571
|
- description: Detailed description of the user's intent (RECOMMENDED: helps safely modify implementation later)
|
|
555
572
|
- category: Category ID for organization (use ha_config_get_category to list, ha_config_set_category to create)
|
|
556
|
-
-
|
|
573
|
+
- conditions: Additional conditions that must be met
|
|
557
574
|
- mode: 'single' (default), 'restart', 'queued', 'parallel'
|
|
558
575
|
- max: Maximum concurrent executions (for queued/parallel modes)
|
|
559
576
|
- initial_state: Whether automation starts enabled (true/false)
|
|
@@ -565,19 +582,19 @@ class AutomationConfigTools:
|
|
|
565
582
|
ha_config_set_automation(config={
|
|
566
583
|
"alias": "Morning Lights",
|
|
567
584
|
"description": "Turn on bedroom lights at 7 AM to help wake up",
|
|
568
|
-
"
|
|
569
|
-
"
|
|
585
|
+
"triggers": [{"trigger": "time", "at": "07:00:00"}],
|
|
586
|
+
"actions": [{"action": "light.turn_on", "target": {"area_id": "bedroom"}}]
|
|
570
587
|
})
|
|
571
588
|
|
|
572
589
|
Motion-activated lighting — `for:` on the off-transition replaces action-delay:
|
|
573
590
|
ha_config_set_automation(config={
|
|
574
591
|
"alias": "Motion Light",
|
|
575
|
-
"
|
|
576
|
-
{"
|
|
577
|
-
{"
|
|
592
|
+
"triggers": [
|
|
593
|
+
{"trigger": "state", "entity_id": "binary_sensor.motion", "to": "on", "id": "motion_on"},
|
|
594
|
+
{"trigger": "state", "entity_id": "binary_sensor.motion", "to": "off",
|
|
578
595
|
"for": {"minutes": 5}, "id": "motion_off"}
|
|
579
596
|
],
|
|
580
|
-
"
|
|
597
|
+
"actions": [
|
|
581
598
|
{"choose": [
|
|
582
599
|
{"conditions": [
|
|
583
600
|
{"condition": "trigger", "id": "motion_on"},
|
|
@@ -595,8 +612,8 @@ class AutomationConfigTools:
|
|
|
595
612
|
identifier="automation.morning_routine",
|
|
596
613
|
config={
|
|
597
614
|
"alias": "Updated Morning Routine",
|
|
598
|
-
"
|
|
599
|
-
"
|
|
615
|
+
"triggers": [{"trigger": "time", "at": "06:30:00"}],
|
|
616
|
+
"actions": [
|
|
600
617
|
{"action": "light.turn_on", "target": {"area_id": "bedroom"}},
|
|
601
618
|
{"action": "climate.set_temperature", "target": {"entity_id": "climate.bedroom"}, "data": {"temperature": 22}}
|
|
602
619
|
]
|
|
@@ -714,7 +731,12 @@ class AutomationConfigTools:
|
|
|
714
731
|
config_category = config_dict.pop("category", None)
|
|
715
732
|
effective_category = category if category is not None else config_category
|
|
716
733
|
|
|
717
|
-
#
|
|
734
|
+
# Detect conflicting singular+plural root keys BEFORE normalization
|
|
735
|
+
# drops the singular alias (surface rather than silently discard).
|
|
736
|
+
conflict_warnings = _detect_conflicting_root_keys(config_dict)
|
|
737
|
+
|
|
738
|
+
# Normalize field names to HA's canonical plural root keys
|
|
739
|
+
# (trigger -> triggers, action -> actions, condition -> conditions).
|
|
718
740
|
config_dict = _normalize_automation_config(config_dict)
|
|
719
741
|
|
|
720
742
|
# Optional hash check for full config updates
|
|
@@ -735,6 +757,7 @@ class AutomationConfigTools:
|
|
|
735
757
|
bp_warnings,
|
|
736
758
|
validation_meta,
|
|
737
759
|
MandatoryBPS,
|
|
760
|
+
conflict_warnings,
|
|
738
761
|
)
|
|
739
762
|
|
|
740
763
|
except ToolError as te:
|
|
@@ -750,7 +773,7 @@ class AutomationConfigTools:
|
|
|
750
773
|
error_text = str(e)
|
|
751
774
|
suggestions = [
|
|
752
775
|
"Check automation configuration format",
|
|
753
|
-
"Ensure required fields: alias,
|
|
776
|
+
"Ensure required fields: alias, triggers, actions",
|
|
754
777
|
"Use entity_id format: automation.morning_routine or unique_id",
|
|
755
778
|
"Use ha_search(domain_filter='automation') to find automations",
|
|
756
779
|
"Use ha_get_skill_guide for automation examples",
|
|
@@ -844,6 +867,11 @@ class AutomationConfigTools:
|
|
|
844
867
|
transform_category = transformed_config.pop("category", None)
|
|
845
868
|
effective_category = category if category is not None else transform_category
|
|
846
869
|
|
|
870
|
+
# Detect conflicting singular+plural root keys (e.g. a transform that set
|
|
871
|
+
# the singular 'trigger' on a fetched plural config) before normalization
|
|
872
|
+
# drops the singular alias.
|
|
873
|
+
conflict_warnings = _detect_conflicting_root_keys(transformed_config)
|
|
874
|
+
|
|
847
875
|
transformed_config = _normalize_automation_config(transformed_config)
|
|
848
876
|
self._validate_required_fields(transformed_config, identifier)
|
|
849
877
|
bp_warnings = _check_best_practices(transformed_config)
|
|
@@ -851,6 +879,8 @@ class AutomationConfigTools:
|
|
|
851
879
|
result = await self._client.upsert_automation_config(
|
|
852
880
|
transformed_config, identifier
|
|
853
881
|
)
|
|
882
|
+
for warning in conflict_warnings:
|
|
883
|
+
result.setdefault("warnings", []).append(warning)
|
|
854
884
|
refetched = await self._get_automation_config_internal(identifier)
|
|
855
885
|
new_config_hash = refetched[1]
|
|
856
886
|
|
|
@@ -895,10 +925,14 @@ class AutomationConfigTools:
|
|
|
895
925
|
bp_warnings: BestPracticeCheckResult,
|
|
896
926
|
validation_meta: dict[str, Any],
|
|
897
927
|
MandatoryBPS: bool,
|
|
928
|
+
conflict_warnings: list[str] | None = None,
|
|
898
929
|
) -> dict[str, Any]:
|
|
899
930
|
"""Execute config-replacement mode and return the tool response."""
|
|
900
931
|
result = await self._client.upsert_automation_config(config_dict, identifier)
|
|
901
932
|
|
|
933
|
+
for warning in conflict_warnings or []:
|
|
934
|
+
result.setdefault("warnings", []).append(warning)
|
|
935
|
+
|
|
902
936
|
if result.get("entity_not_verified"):
|
|
903
937
|
result.setdefault("warnings", []).append(
|
|
904
938
|
f"{NOT_VERIFIED_WARNING_PREFIX} "
|
|
@@ -1098,13 +1132,13 @@ class AutomationConfigTools:
|
|
|
1098
1132
|
config_dict: dict[str, Any], identifier: str | None
|
|
1099
1133
|
) -> None:
|
|
1100
1134
|
"""Raise if an empty-trigger config wraps scene.create (common model misroute)."""
|
|
1101
|
-
trigger_value = config_dict.get("
|
|
1135
|
+
trigger_value = config_dict.get("triggers")
|
|
1102
1136
|
trigger_empty = trigger_value is None or (
|
|
1103
1137
|
isinstance(trigger_value, list) and not trigger_value
|
|
1104
1138
|
)
|
|
1105
1139
|
if not trigger_empty:
|
|
1106
1140
|
return
|
|
1107
|
-
actions_list = coerce_to_list(config_dict.get("
|
|
1141
|
+
actions_list = coerce_to_list(config_dict.get("actions"))
|
|
1108
1142
|
scene_create_indices = [
|
|
1109
1143
|
i for i, a in enumerate(actions_list) if _action_contains_scene_create(a)
|
|
1110
1144
|
]
|
|
@@ -1134,7 +1168,7 @@ class AutomationConfigTools:
|
|
|
1134
1168
|
@staticmethod
|
|
1135
1169
|
def _validate_condition_platform(config_dict: dict[str, Any]) -> None:
|
|
1136
1170
|
"""Raise if any condition uses 'platform' (trigger syntax) instead of 'condition'."""
|
|
1137
|
-
for idx, cond in enumerate(coerce_to_list(config_dict.get("
|
|
1171
|
+
for idx, cond in enumerate(coerce_to_list(config_dict.get("conditions"))):
|
|
1138
1172
|
if not isinstance(cond, dict):
|
|
1139
1173
|
continue
|
|
1140
1174
|
if "platform" in cond and "condition" not in cond:
|
|
@@ -1148,7 +1182,7 @@ class AutomationConfigTools:
|
|
|
1148
1182
|
suggestions=[
|
|
1149
1183
|
f"Replace 'platform' with 'condition': "
|
|
1150
1184
|
f"{{'condition': '{cond['platform']}', ...}}",
|
|
1151
|
-
"Triggers use '
|
|
1185
|
+
"Triggers use 'trigger'; conditions use 'condition'.",
|
|
1152
1186
|
],
|
|
1153
1187
|
context={"condition_index": idx, "found_key": "platform"},
|
|
1154
1188
|
)
|
|
@@ -1161,12 +1195,12 @@ class AutomationConfigTools:
|
|
|
1161
1195
|
"""Validate required fields and prevent duplicate creation."""
|
|
1162
1196
|
if "use_blueprint" in config_dict:
|
|
1163
1197
|
required_fields = ["alias"]
|
|
1164
|
-
# Strip empty
|
|
1165
|
-
for field in ["
|
|
1198
|
+
# Strip empty triggers/actions/conditions arrays that would override blueprint
|
|
1199
|
+
for field in ["triggers", "actions", "conditions"]:
|
|
1166
1200
|
if field in config_dict and config_dict[field] == []:
|
|
1167
1201
|
del config_dict[field]
|
|
1168
1202
|
else:
|
|
1169
|
-
required_fields = ["alias", "
|
|
1203
|
+
required_fields = ["alias", "triggers", "actions"]
|
|
1170
1204
|
|
|
1171
1205
|
missing_fields = [f for f in required_fields if f not in config_dict]
|
|
1172
1206
|
if missing_fields:
|
|
@@ -1174,7 +1208,7 @@ class AutomationConfigTools:
|
|
|
1174
1208
|
# script — point them at ha_config_set_script instead of the generic
|
|
1175
1209
|
# missing-fields error.
|
|
1176
1210
|
if "sequence" in config_dict and (
|
|
1177
|
-
"
|
|
1211
|
+
"triggers" in missing_fields or "actions" in missing_fields
|
|
1178
1212
|
):
|
|
1179
1213
|
context: dict[str, Any] = {"missing_fields": missing_fields}
|
|
1180
1214
|
if identifier:
|
|
@@ -1185,11 +1219,11 @@ class AutomationConfigTools:
|
|
|
1185
1219
|
message=f"Missing required fields: {', '.join(missing_fields)}",
|
|
1186
1220
|
details=(
|
|
1187
1221
|
"Config contains 'sequence', which belongs to scripts. "
|
|
1188
|
-
"Automations use '
|
|
1222
|
+
"Automations use 'triggers' and 'actions'; scripts use 'sequence'."
|
|
1189
1223
|
),
|
|
1190
1224
|
suggestions=[
|
|
1191
1225
|
"Did you mean ha_config_set_script? Scripts use 'sequence' directly.",
|
|
1192
|
-
"For an automation, replace 'sequence' with '
|
|
1226
|
+
"For an automation, replace 'sequence' with 'actions' and add 'triggers'.",
|
|
1193
1227
|
],
|
|
1194
1228
|
context=context,
|
|
1195
1229
|
)
|
|
@@ -2331,7 +2331,7 @@ def register_search_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
2331
2331
|
default=None,
|
|
2332
2332
|
description=(
|
|
2333
2333
|
"Return only the specified keys from each entity's attributes dict "
|
|
2334
|
-
'(e.g. ["brightness", "
|
|
2334
|
+
'(e.g. ["brightness", "color_temp_kelvin"] for lights). '
|
|
2335
2335
|
"None = full attributes (default). "
|
|
2336
2336
|
"Unknown keys are silently dropped. "
|
|
2337
2337
|
'Requires "attributes" to be present in fields= (or fields=None).'
|
|
@@ -81,7 +81,10 @@ _STATE_CHANGING_SERVICES = {
|
|
|
81
81
|
"set_temperature",
|
|
82
82
|
"set_hvac_mode",
|
|
83
83
|
"set_fan_mode",
|
|
84
|
-
|
|
84
|
+
# fan.set_speed was removed in the HA percentage migration (gone in 2026.6);
|
|
85
|
+
# its state-changing successors are set_percentage / set_preset_mode.
|
|
86
|
+
"set_percentage",
|
|
87
|
+
"set_preset_mode",
|
|
85
88
|
"select_option",
|
|
86
89
|
"set_value",
|
|
87
90
|
"set_datetime",
|
|
@@ -74,7 +74,10 @@ DOMAIN_HANDLERS = {
|
|
|
74
74
|
},
|
|
75
75
|
"fan": {
|
|
76
76
|
"valid_actions": ["on", "off", "toggle", "set"],
|
|
77
|
-
|
|
77
|
+
# HA removed the legacy `speed` param / `fan.set_speed` service in the
|
|
78
|
+
# 2021-2022 percentage migration (absent in 2026.6). Use percentage
|
|
79
|
+
# (fan.set_percentage) and preset_mode (fan.set_preset_mode).
|
|
80
|
+
"parameters": ["percentage", "preset_mode", "direction"],
|
|
78
81
|
"quick_actions": ["toggle", "speed_up", "speed_down"],
|
|
79
82
|
"state_attributes": ["percentage", "preset_mode"],
|
|
80
83
|
"supports_speed": True,
|
|
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.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/dashboard_screenshot/__init__.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/dashboard_screenshot/capture.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/dashboard_screenshot/provision.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
|
{ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/resources/skills-vendor/AGENTS.md
RENAMED
|
File without changes
|
{ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/resources/skills-vendor/CLAUDE.md
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/resources/skills-vendor/LICENSE
RENAMED
|
File without changes
|
{ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/resources/skills-vendor/README.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/smart_search/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/smart_search/_config.py
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/smart_search/_entities.py
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/smart_search/_overview.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/smart_search/_scenes.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/smart_search/_scoring.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
|
{ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_config_dashboards.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_config_entry_flow.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_config_helpers.py
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_config_scripts.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_dashboard_screenshot.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
|
{ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/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.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/validation_middleware.py
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/transforms/categorized_search.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/transforms/lite_docstrings.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/utils/kill_signal_diagnostics.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp_dev.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/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
|