ha-mcp-dev 8.4.3.dev2602__tar.gz → 8.4.3.dev2608__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-8.4.3.dev2602/src/ha_mcp_dev.egg-info → ha_mcp_dev-8.4.3.dev2608}/PKG-INFO +2 -2
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/README.md +1 -1
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/pyproject.toml +1 -1
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/server.py +13 -8
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/component_api.py +95 -50
- ha_mcp_dev-8.4.3.dev2608/src/ha_mcp/tools/component_dashboard_edit.py +169 -0
- ha_mcp_dev-8.4.3.dev2608/src/ha_mcp/tools/dashboard_edit_errors.py +156 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_config_automations.py +62 -13
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_config_dashboards.py +454 -144
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/util_helpers.py +6 -0
- ha_mcp_dev-8.4.3.dev2608/src/ha_mcp/utils/dashboard_patch.py +149 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608/src/ha_mcp_dev.egg-info}/PKG-INFO +2 -2
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp_dev.egg-info/SOURCES.txt +3 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/LICENSE +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/MANIFEST.in +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/setup.cfg +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_rich_compat.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/__init__.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/requirements.txt +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/LICENSE +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/MANIFEST.sha256 +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/VENDORED +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/__init__.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/__main__.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/asyncio/__init__.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/asyncio/client.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/asyncio/connection.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/asyncio/messages.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/asyncio/router.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/asyncio/server.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/auth.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/cli.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/client.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/connection.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/datastructures.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/exceptions.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/extensions/__init__.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/extensions/base.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/extensions/permessage_deflate.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/frames.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/headers.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/http11.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/imports.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/legacy/__init__.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/legacy/auth.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/legacy/client.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/legacy/exceptions.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/legacy/framing.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/legacy/handshake.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/legacy/http.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/legacy/protocol.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/legacy/server.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/protocol.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/proxy.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/py.typed +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/server.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/streams.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/sync/__init__.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/sync/client.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/sync/connection.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/sync/messages.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/sync/router.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/sync/server.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/sync/utils.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/trio/__init__.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/trio/client.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/trio/connection.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/trio/messages.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/trio/router.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/trio/server.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/trio/utils.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/typing.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/uri.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/utils.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/version.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/auth/oidc_compat.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/backup_manager.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/browser_landing.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/dashboard_screenshot/theme_guard.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/ha_request_queue.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/hacs_auto_refresh.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/llm_exposure.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/log_filters.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/policy/__init__.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/policy/approval_queue.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/policy/editing.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/policy/evaluator.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/policy/handlers.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/policy/middleware.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/policy/model.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/policy/persistence.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/policy/value_sources.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/read_only.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/redaction.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/renamed_tools.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/evals/area-target-over-entity-list/case.yaml +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/evals/arrive-home-automation/case.yaml +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/evals/assist-shortcut-card/case.yaml +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/evals/avg-temperature/case.yaml +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/evals/backup-deletion-confirmation/case.yaml +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/evals/blueprint-authoring/case.yaml +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/evals/button-event-entity/case.yaml +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/evals/device-id-trigger-migration/case.yaml +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/evals/helper-flow-fields-not-platform-yaml/case.yaml +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/evals/media-player-song/case.yaml +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/evals/motion-light-automation-mode/case.yaml +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/evals/rename-entity-impact-analysis/case.yaml +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/evals/template-attributes-yaml-exception/case.yaml +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/evals/timer-finished-automation/case.yaml +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/evals/vacuum-last-clean/case.yaml +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/scripts/check_eval_cases.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/backups.md +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/__init__.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/_i18n.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/_locale_policy.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/_persistence.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/_theme.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/locales/README.md +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/locales/cs.json +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/locales/de.json +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/locales/en.json +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/locales/eo.json +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/locales/es.json +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/locales/fr.json +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/locales/it.json +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/locales/ko.json +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/locales/nl.json +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/locales/pl.json +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/locales/ru.json +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/locales/sv.json +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/locales/tlh.json +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/locales/zh-Hans.json +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/settings.css +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/settings.html +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/settings.js +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/strict_bps.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tool_dispatch.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/auto_backup.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/best_practice_result.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/best_practice_variables.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/blueprint_sources.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/blueprint_substitute.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/blueprint_write.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/bulk_selector.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/component_config_entries.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/component_config_reads.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/component_devices.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/component_registries.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/component_registry_lookup.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/config_entry_flow.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/config_entry_flow_form.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/config_entry_flow_menu.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/config_entry_flow_walker.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/config_entry_identity.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/config_entry_reconfigure.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/config_entry_reload_watch.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/error_log_parsing.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/hacs_registration.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/integration_reconfigure.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/log_common.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/log_sources.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/log_sources_fault.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/log_sources_supervisor.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/radio/__init__.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/radio/base.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/radio/matter.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/radio/thread.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/radio/zigbee.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/radio/zwave.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/renamed_tool_middleware.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/smart_search/_base.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/smart_search/_config.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/smart_search/_graph.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_assist_pipeline.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_dev.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_logs.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_radio.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_security_policy.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_themes.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/validation_middleware.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/transport_security.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/update_check.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/utils/config_write_lock.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/utils/device_registry_semantics.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/utils/entity_membership.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/utils/skill_loader.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/visibility/__init__.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/visibility/enforcement.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/visibility/model.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/visibility/persistence.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/visibility/resolver.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/tests/__init__.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/tests/test_constants.py +0 -0
- {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/tests/test_env_manager.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ha-mcp-dev
|
|
3
|
-
Version: 8.4.3.
|
|
3
|
+
Version: 8.4.3.dev2608
|
|
4
4
|
Summary: Home Assistant MCP Server - Complete control of Home Assistant through MCP
|
|
5
5
|
Author-email: Julien <github@qc-h.net>
|
|
6
6
|
License: MIT
|
|
@@ -239,7 +239,7 @@ Spend less time configuring, more time enjoying your smart home.
|
|
|
239
239
|
|----------|--------------|
|
|
240
240
|
| **🔍 Search** | Fuzzy entity search, deep config search, system overview |
|
|
241
241
|
| **🏠 Control** | Any service, bulk device control, real-time states |
|
|
242
|
-
| **🔧 Manage** | Automations, scripts, helpers, dashboards, areas, zones, groups, calendars, blueprints |
|
|
242
|
+
| **🔧 Manage** | Automations, scripts, helpers, [dashboards](docs/dashboard-edits.md), areas, zones, groups, calendars, blueprints |
|
|
243
243
|
| **📊 Monitor** | History, statistics, camera snapshots, automation traces, ZHA devices |
|
|
244
244
|
| **💾 System** | Backup/restore, updates, apps, device registry |
|
|
245
245
|
| **🔒 Safety** | Read Only Mode toggle, per-tool enable/disable, tool security policies (user approval), automatic edit backups |
|
|
@@ -207,7 +207,7 @@ Spend less time configuring, more time enjoying your smart home.
|
|
|
207
207
|
|----------|--------------|
|
|
208
208
|
| **🔍 Search** | Fuzzy entity search, deep config search, system overview |
|
|
209
209
|
| **🏠 Control** | Any service, bulk device control, real-time states |
|
|
210
|
-
| **🔧 Manage** | Automations, scripts, helpers, dashboards, areas, zones, groups, calendars, blueprints |
|
|
210
|
+
| **🔧 Manage** | Automations, scripts, helpers, [dashboards](docs/dashboard-edits.md), areas, zones, groups, calendars, blueprints |
|
|
211
211
|
| **📊 Monitor** | History, statistics, camera snapshots, automation traces, ZHA devices |
|
|
212
212
|
| **💾 System** | Backup/restore, updates, apps, device registry |
|
|
213
213
|
| **🔒 Safety** | Read Only Mode toggle, per-tool enable/disable, tool security policies (user approval), automatic edit backups |
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ha-mcp-dev"
|
|
7
|
-
version = "8.4.3.
|
|
7
|
+
version = "8.4.3.dev2608"
|
|
8
8
|
description = "Home Assistant MCP Server - Complete control of Home Assistant through MCP"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
# Upper bound widened to <3.15 so the package installs on Home Assistant's
|
|
@@ -727,7 +727,8 @@ class HomeAssistantSmartMCPServer:
|
|
|
727
727
|
"ha_config_get_automation), or `take_control_of_blueprint` "
|
|
728
728
|
"to convert a blueprint-backed automation into an editable "
|
|
729
729
|
"standalone one (the UI's Take control action). Omit "
|
|
730
|
-
"`identifier` to create a new automation
|
|
730
|
+
"`identifier` to create a new automation. Reusing an identifier targets "
|
|
731
|
+
"the same automation; changing its alias requires config_hash from a prior read.\n\n"
|
|
731
732
|
"For schema details, examples, and native-vs-template "
|
|
732
733
|
"guidance, see ha_get_skill_guide or your locally "
|
|
733
734
|
"installed skills."
|
|
@@ -806,18 +807,22 @@ class HomeAssistantSmartMCPServer:
|
|
|
806
807
|
"matched cards surgically. (3) get — no search params "
|
|
807
808
|
"returns the full Lovelace config plus a stable "
|
|
808
809
|
"`config_hash`. Use `url_path='default'` for the main "
|
|
809
|
-
"dashboard
|
|
810
|
+
"dashboard. For known JSON Pointer paths, use "
|
|
811
|
+
"ha_config_set_dashboard(patch=..., config_hash=...).\n\n"
|
|
810
812
|
"For card-type taxonomy and search workflow examples, see "
|
|
811
813
|
"ha_get_skill_guide."
|
|
812
814
|
),
|
|
813
815
|
"ha_config_set_dashboard": (
|
|
814
816
|
"Create or update a Home Assistant dashboard.\n\n"
|
|
815
|
-
"
|
|
816
|
-
"
|
|
817
|
-
"
|
|
818
|
-
"`
|
|
819
|
-
"
|
|
820
|
-
"
|
|
817
|
+
"Three modes: full `config` replacement for new dashboards or "
|
|
818
|
+
"restructures; `patch` for known JSON Pointer paths with literal "
|
|
819
|
+
"values (add/remove/replace/test, up to 100 operations; use `-` "
|
|
820
|
+
"to append to arrays); `python_transform` for loops and pattern-based "
|
|
821
|
+
"edits. Both edit modes require `config_hash` from "
|
|
822
|
+
"ha_config_get_dashboard. Choose one mode for content changes; "
|
|
823
|
+
"omit all three for metadata-only calls. With patch, change "
|
|
824
|
+
"sidebar metadata in a separate call. Use `url_path` of 'default' "
|
|
825
|
+
"or 'lovelace' for the built-in dashboard.\n\n"
|
|
821
826
|
"For card types, layout patterns, and python_transform "
|
|
822
827
|
"security rules, see "
|
|
823
828
|
"ha_get_skill_guide or your locally installed skills."
|
|
@@ -67,20 +67,18 @@ DEVICE_REGISTRY_CHILD_SEMANTICS = "device_registry_child_semantics"
|
|
|
67
67
|
# time, so a component installed / upgraded mid-session is re-probed and
|
|
68
68
|
# adopted instead of being pinned to "absent" for the whole process lifetime
|
|
69
69
|
# (the cache key is the process-lifetime REST client — see ``_CAPS_CACHE``).
|
|
70
|
-
#
|
|
70
|
+
# Valid positive entries never expire on this timer; they are dropped only by
|
|
71
71
|
# ``invalidate_caps`` (a supposedly-supported command coming back
|
|
72
72
|
# ``unknown_command``). This ABSENT-negative window covers a definitive
|
|
73
73
|
# "component responded, no usable surface" verdict (``unknown_command`` /
|
|
74
|
-
#
|
|
74
|
+
# unsupported schema_version). Inconclusive failures use the short window below.
|
|
75
75
|
_NEGATIVE_CACHE_TTL_S = 300.0
|
|
76
76
|
|
|
77
|
-
# A separate,
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
#
|
|
81
|
-
#
|
|
82
|
-
# socket recovers. Kept short precisely because the transport may come back at
|
|
83
|
-
# any moment — unlike the absent-negative, which reflects a stable answer.
|
|
77
|
+
# A separate, SHORTER window for inconclusive discovery: transport failures,
|
|
78
|
+
# info handler errors, unexpected faults, and malformed replies. Caching these
|
|
79
|
+
# briefly avoids repeating a broken probe on every tool call (issue #1813 Phase 2,
|
|
80
|
+
# review-5 M8), while allowing strict writers to recover promptly after discovery
|
|
81
|
+
# works again. These failures do not establish that a capability is absent.
|
|
84
82
|
_TRANSIENT_NEGATIVE_CACHE_TTL_S = 30.0
|
|
85
83
|
|
|
86
84
|
|
|
@@ -121,29 +119,47 @@ class ComponentCaps:
|
|
|
121
119
|
# Weak-keyed by client so the negotiated caps self-evict when the client is
|
|
122
120
|
# garbage-collected. The key is the process-lifetime REST client, NOT the WS
|
|
123
121
|
# connection: an HA restart drops the socket but reuses the same pooled client,
|
|
124
|
-
# so a positive entry persists for the whole process (dropped only by
|
|
122
|
+
# so a valid positive entry persists for the whole process (dropped only by
|
|
125
123
|
# ``invalidate_caps`` on an ``unknown_command``). A ``None`` value is a cached
|
|
126
124
|
# *negative* ("probed, no usable WS surface"); absence means "not yet probed".
|
|
127
125
|
# A negative carries an expiry in EXACTLY ONE of the two timestamp maps below —
|
|
128
126
|
# ``_NEGATIVE_CACHE_TS`` (definitive absent, long) or ``_TRANSIENT_NEGATIVE_TS``
|
|
129
|
-
# (
|
|
127
|
+
# (inconclusive failure, short) — so a component installed/upgraded mid-session, or one
|
|
130
128
|
# that just became reachable again, is eventually re-probed instead of pinned.
|
|
131
129
|
_CAPS_CACHE: weakref.WeakKeyDictionary[Any, ComponentCaps | None] = (
|
|
132
130
|
weakref.WeakKeyDictionary()
|
|
133
131
|
)
|
|
134
132
|
# Monotonic timestamp a definitive ABSENT-negative was stored, keyed by the same
|
|
135
|
-
# client.
|
|
133
|
+
# client. Inconclusive failures never use this window.
|
|
136
134
|
_NEGATIVE_CACHE_TS: weakref.WeakKeyDictionary[Any, float] = weakref.WeakKeyDictionary()
|
|
137
|
-
# Monotonic timestamp
|
|
135
|
+
# Monotonic timestamp an inconclusive probe failure was stored. Also expires
|
|
136
|
+
# malformed snapshots retained for permissive readers. Kept
|
|
138
137
|
# separate from ``_NEGATIVE_CACHE_TS`` so the two negative kinds carry different
|
|
139
138
|
# TTLs; a client is stamped in at most one of the two at a time (the store helpers
|
|
140
139
|
# clear the other).
|
|
141
140
|
_TRANSIENT_NEGATIVE_TS: weakref.WeakKeyDictionary[Any, float] = (
|
|
142
141
|
weakref.WeakKeyDictionary()
|
|
143
142
|
)
|
|
143
|
+
# Preserve failed discovery separately so write callers can refuse a downgrade
|
|
144
|
+
# even when a best-effort read populated the negative cache first. Store only
|
|
145
|
+
# text, not exceptions whose traceback would retain clients through weak keys.
|
|
146
|
+
_PROBE_FAILURES: weakref.WeakKeyDictionary[Any, str] = weakref.WeakKeyDictionary()
|
|
144
147
|
_CAPS_LOCKS: weakref.WeakKeyDictionary[Any, asyncio.Lock] = weakref.WeakKeyDictionary()
|
|
145
148
|
|
|
146
149
|
|
|
150
|
+
class ComponentDiscoveryError(RuntimeError):
|
|
151
|
+
"""Discovery failed without establishing which component commands are usable."""
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _checked_caps(
|
|
155
|
+
client: Any, caps: ComponentCaps | None, strict: bool
|
|
156
|
+
) -> ComponentCaps | None:
|
|
157
|
+
"""Keep read fallbacks while exposing failed discovery to write callers."""
|
|
158
|
+
if strict and (failure := _PROBE_FAILURES.get(client)):
|
|
159
|
+
raise ComponentDiscoveryError(failure)
|
|
160
|
+
return caps
|
|
161
|
+
|
|
162
|
+
|
|
147
163
|
def _get_caps_lock(client: Any) -> asyncio.Lock:
|
|
148
164
|
"""Per-client lock so concurrent first-callers probe ``info`` exactly once."""
|
|
149
165
|
lock = _CAPS_LOCKS.get(client)
|
|
@@ -156,28 +172,29 @@ def _get_caps_lock(client: Any) -> asyncio.Lock:
|
|
|
156
172
|
def _live_cache_entry(client: Any) -> tuple[bool, ComponentCaps | None]:
|
|
157
173
|
"""Return ``(hit, caps)`` for a still-valid cache entry, else ``(False, None)``.
|
|
158
174
|
|
|
159
|
-
A positive entry is a hit for the process lifetime.
|
|
175
|
+
A valid positive entry is a hit for the process lifetime. Malformed replies
|
|
176
|
+
retained for permissive readers expire like transient failures. A negative (``None``)
|
|
160
177
|
entry is a hit only within its window of when it was stored (monotonic clock):
|
|
161
178
|
``_NEGATIVE_CACHE_TTL_S`` for a definitive absent-negative, the shorter
|
|
162
|
-
``_TRANSIENT_NEGATIVE_CACHE_TTL_S`` for
|
|
179
|
+
``_TRANSIENT_NEGATIVE_CACHE_TTL_S`` for an inconclusive failure. Once the
|
|
163
180
|
relevant window lapses it reports a miss so the caller re-probes and can adopt a
|
|
164
181
|
component that appeared — or became reachable — mid-session.
|
|
165
182
|
"""
|
|
166
183
|
if client not in _CAPS_CACHE:
|
|
167
184
|
return False, None
|
|
168
185
|
cached = _CAPS_CACHE[client]
|
|
169
|
-
if cached is not None:
|
|
186
|
+
if cached is not None and client not in _PROBE_FAILURES:
|
|
170
187
|
return True, cached
|
|
171
188
|
now = _monotonic()
|
|
172
189
|
stored_at = _NEGATIVE_CACHE_TS.get(client)
|
|
173
190
|
if stored_at is not None and (now - stored_at) < _NEGATIVE_CACHE_TTL_S:
|
|
174
|
-
return True,
|
|
191
|
+
return True, cached
|
|
175
192
|
transient_at = _TRANSIENT_NEGATIVE_TS.get(client)
|
|
176
193
|
if (
|
|
177
194
|
transient_at is not None
|
|
178
195
|
and (now - transient_at) < _TRANSIENT_NEGATIVE_CACHE_TTL_S
|
|
179
196
|
):
|
|
180
|
-
return True,
|
|
197
|
+
return True, cached
|
|
181
198
|
return False, None
|
|
182
199
|
|
|
183
200
|
|
|
@@ -185,9 +202,10 @@ def _store_caps(client: Any, caps: ComponentCaps | None) -> None:
|
|
|
185
202
|
"""Cache a positive result or a definitive ABSENT-negative for ``client``.
|
|
186
203
|
|
|
187
204
|
A ``None`` here is the long-window absent-negative (``info`` responded
|
|
188
|
-
``unknown_command`` /
|
|
205
|
+
``unknown_command`` / unsupported schema). Clears any prior
|
|
189
206
|
transient-negative stamp so a client never carries both negative kinds.
|
|
190
207
|
"""
|
|
208
|
+
_PROBE_FAILURES.pop(client, None)
|
|
191
209
|
_CAPS_CACHE[client] = caps
|
|
192
210
|
_TRANSIENT_NEGATIVE_TS.pop(client, None)
|
|
193
211
|
if caps is None:
|
|
@@ -196,17 +214,16 @@ def _store_caps(client: Any, caps: ComponentCaps | None) -> None:
|
|
|
196
214
|
_NEGATIVE_CACHE_TS.pop(client, None)
|
|
197
215
|
|
|
198
216
|
|
|
199
|
-
def _store_transient_negative(client: Any) -> None:
|
|
200
|
-
"""Cache
|
|
217
|
+
def _store_transient_negative(client: Any, caps: ComponentCaps | None = None) -> None:
|
|
218
|
+
"""Cache an inconclusive probe for the short recovery window.
|
|
201
219
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
absent-negative stamp so a client never carries both negative kinds.
|
|
220
|
+
Read callers retain legacy fallback or a permissively parsed snapshot.
|
|
221
|
+
Strict writers reject the accompanying probe failure until the next probe
|
|
222
|
+
succeeds. Preserve parsed capabilities from malformed replies for readers,
|
|
223
|
+
while expiring them on the same short timer as failed probes. Clear any
|
|
224
|
+
prior absent-negative stamp so a client never carries both cache windows.
|
|
208
225
|
"""
|
|
209
|
-
_CAPS_CACHE[client] =
|
|
226
|
+
_CAPS_CACHE[client] = caps
|
|
210
227
|
_NEGATIVE_CACHE_TS.pop(client, None)
|
|
211
228
|
_TRANSIENT_NEGATIVE_TS[client] = _monotonic()
|
|
212
229
|
|
|
@@ -214,8 +231,8 @@ def _store_transient_negative(client: Any) -> None:
|
|
|
214
231
|
def _parse_caps(response: Any) -> ComponentCaps | None:
|
|
215
232
|
"""Map an ``ha_mcp_tools/info`` response into ``ComponentCaps``.
|
|
216
233
|
|
|
217
|
-
|
|
218
|
-
|
|
234
|
+
Parse permissively for existing read callers. A missing result returns
|
|
235
|
+
``None``; strict callers separately reject malformed discovery.
|
|
219
236
|
"""
|
|
220
237
|
result = response.get("result") if isinstance(response, dict) else None
|
|
221
238
|
if not isinstance(result, dict):
|
|
@@ -249,17 +266,25 @@ def _parse_caps(response: Any) -> ComponentCaps | None:
|
|
|
249
266
|
)
|
|
250
267
|
|
|
251
268
|
|
|
252
|
-
async def get_component_caps(
|
|
269
|
+
async def get_component_caps(
|
|
270
|
+
client: Any, *, strict: bool = False
|
|
271
|
+
) -> ComponentCaps | None:
|
|
253
272
|
"""Return the cached (or freshly probed) capabilities of the component.
|
|
254
273
|
|
|
255
274
|
One ``ha_mcp_tools/info`` probe per client, cached. ``None`` means "no
|
|
256
275
|
usable component WS surface" — the caller falls back to its legacy path.
|
|
257
276
|
|
|
277
|
+
With ``strict=True``, failed or malformed discovery raises
|
|
278
|
+
``ComponentDiscoveryError``, including failures cached by earlier read tools.
|
|
279
|
+
A definitive unknown command, unsupported schema, or valid capability list
|
|
280
|
+
without the requested command still permits legacy routing. The default
|
|
281
|
+
retains best-effort read behavior. Inconclusive failures expire after 30
|
|
282
|
+
seconds for both readers and writers.
|
|
283
|
+
|
|
258
284
|
Cache-on-failure semantics follow the error taxonomy:
|
|
259
285
|
|
|
260
|
-
- ``HomeAssistantCommandError``
|
|
261
|
-
component
|
|
262
|
-
negative is re-probed after ``_NEGATIVE_CACHE_TTL_S`` so a component
|
|
286
|
+
- ``HomeAssistantCommandError`` with ``unknown_command`` (old or absent
|
|
287
|
+
component): cache ``None`` with an expiry. The negative is re-probed after ``_NEGATIVE_CACHE_TTL_S`` so a component
|
|
263
288
|
installed / upgraded mid-session (the REST client — the cache key — is not
|
|
264
289
|
recreated on an HA restart) is eventually adopted instead of pinned absent.
|
|
265
290
|
- ``HomeAssistantConnectionError`` / ``HomeAssistantCommandTimeout`` (WS
|
|
@@ -268,12 +293,12 @@ async def get_component_caps(client: Any) -> ComponentCaps | None:
|
|
|
268
293
|
negative (``_TRANSIENT_NEGATIVE_CACHE_TTL_S``) so repeated calls on a
|
|
269
294
|
WS-broken install skip the slow connect and go straight to legacy, then
|
|
270
295
|
re-probe once the window lapses (self-healing). The consuming tool's legacy
|
|
271
|
-
path serves
|
|
296
|
+
path serves read requests meanwhile; strict writers refuse the edit.
|
|
272
297
|
- No credentials on the client (a bare test double with no ``base_url`` /
|
|
273
298
|
``token``): nothing to probe; return ``None`` without caching.
|
|
274
|
-
-
|
|
275
|
-
|
|
276
|
-
|
|
299
|
+
- Other command errors, malformed replies, or unexpected exceptions: cache
|
|
300
|
+
for the same short transient window. None establishes capability absence;
|
|
301
|
+
strict writers refuse the edit until a subsequent probe succeeds.
|
|
277
302
|
|
|
278
303
|
A probe whose ``schema_version`` is not ``SUPPORTED_SCHEMA_VERSION`` is
|
|
279
304
|
treated as no-caps (cached negative, logged once): the server can't trust
|
|
@@ -281,12 +306,12 @@ async def get_component_caps(client: Any) -> ComponentCaps | None:
|
|
|
281
306
|
"""
|
|
282
307
|
hit, caps = _live_cache_entry(client)
|
|
283
308
|
if hit:
|
|
284
|
-
return caps
|
|
309
|
+
return _checked_caps(client, caps, strict)
|
|
285
310
|
|
|
286
311
|
async with _get_caps_lock(client):
|
|
287
312
|
hit, caps = _live_cache_entry(client)
|
|
288
313
|
if hit:
|
|
289
|
-
return caps
|
|
314
|
+
return _checked_caps(client, caps, strict)
|
|
290
315
|
|
|
291
316
|
base_url = getattr(client, "base_url", None)
|
|
292
317
|
token = getattr(client, "token", None)
|
|
@@ -305,23 +330,29 @@ async def get_component_caps(client: Any) -> ComponentCaps | None:
|
|
|
305
330
|
url=base_url, token=token, verify_ssl=verify_ssl
|
|
306
331
|
)
|
|
307
332
|
response = await ws.send_command(INFO_COMMAND)
|
|
308
|
-
except HomeAssistantCommandError:
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
333
|
+
except HomeAssistantCommandError as exc:
|
|
334
|
+
if is_unknown_command(exc):
|
|
335
|
+
_store_caps(client, None)
|
|
336
|
+
else:
|
|
337
|
+
_store_transient_negative(client)
|
|
338
|
+
_PROBE_FAILURES[client] = f"{INFO_COMMAND} failed: {exc}"
|
|
339
|
+
return _checked_caps(client, None, strict)
|
|
313
340
|
except (HomeAssistantConnectionError, HomeAssistantCommandTimeout):
|
|
314
341
|
logger.debug(
|
|
315
342
|
"%s probe skipped: WS unavailable", INFO_COMMAND, exc_info=True
|
|
316
343
|
)
|
|
317
344
|
_store_transient_negative(client)
|
|
318
|
-
|
|
345
|
+
_PROBE_FAILURES[client] = (
|
|
346
|
+
f"{INFO_COMMAND} probe could not reach Home Assistant"
|
|
347
|
+
)
|
|
348
|
+
return _checked_caps(client, None, strict)
|
|
319
349
|
except Exception:
|
|
320
|
-
#
|
|
321
|
-
#
|
|
350
|
+
# Read callers retain best-effort fallback; strict write callers
|
|
351
|
+
# must not confuse an unexpected fault with capability absence.
|
|
322
352
|
logger.debug("%s probe failed unexpectedly", INFO_COMMAND, exc_info=True)
|
|
323
353
|
_store_transient_negative(client)
|
|
324
|
-
|
|
354
|
+
_PROBE_FAILURES[client] = f"{INFO_COMMAND} probe failed unexpectedly"
|
|
355
|
+
return _checked_caps(client, None, strict)
|
|
325
356
|
|
|
326
357
|
caps = _parse_caps(response)
|
|
327
358
|
if caps is not None and caps.schema_version != SUPPORTED_SCHEMA_VERSION:
|
|
@@ -333,7 +364,20 @@ async def get_component_caps(client: Any) -> ComponentCaps | None:
|
|
|
333
364
|
)
|
|
334
365
|
caps = None
|
|
335
366
|
_store_caps(client, caps)
|
|
336
|
-
|
|
367
|
+
result = response.get("result") if isinstance(response, dict) else None
|
|
368
|
+
if (
|
|
369
|
+
not isinstance(response, dict)
|
|
370
|
+
or response.get("success") is not True
|
|
371
|
+
or not isinstance(result, dict)
|
|
372
|
+
or type(result.get("schema_version")) is not int
|
|
373
|
+
or not isinstance(result.get("capabilities"), list)
|
|
374
|
+
or not all(isinstance(cap, str) for cap in result["capabilities"])
|
|
375
|
+
):
|
|
376
|
+
# Keep permissive parsing for existing read callers, but expire
|
|
377
|
+
# malformed positive snapshots too so strict callers can recover.
|
|
378
|
+
_store_transient_negative(client, caps)
|
|
379
|
+
_PROBE_FAILURES[client] = f"Malformed {INFO_COMMAND} response"
|
|
380
|
+
return _checked_caps(client, caps, strict)
|
|
337
381
|
|
|
338
382
|
|
|
339
383
|
def component_supports(caps: ComponentCaps | None, capability: str) -> bool:
|
|
@@ -358,6 +402,7 @@ def invalidate_caps(client: Any) -> None:
|
|
|
358
402
|
``unknown_command`` (e.g. the component was downgraded mid-session), so the
|
|
359
403
|
stale positive doesn't keep routing to a dead command.
|
|
360
404
|
"""
|
|
405
|
+
_PROBE_FAILURES.pop(client, None)
|
|
361
406
|
_CAPS_CACHE.pop(client, None)
|
|
362
407
|
_NEGATIVE_CACHE_TS.pop(client, None)
|
|
363
408
|
_TRANSIENT_NEGATIVE_TS.pop(client, None)
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Capability-negotiated dashboard writes with no retry after a sent command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Any, NoReturn
|
|
8
|
+
|
|
9
|
+
from ..client.rest_client import HomeAssistantCommandError, HomeAssistantCommandNotSent
|
|
10
|
+
from ..client.websocket_client import get_websocket_client
|
|
11
|
+
from .component_api import (
|
|
12
|
+
component_supports,
|
|
13
|
+
get_component_caps,
|
|
14
|
+
invalidate_caps,
|
|
15
|
+
is_unknown_command,
|
|
16
|
+
)
|
|
17
|
+
from .dashboard_edit_errors import raise_dashboard_edit_error
|
|
18
|
+
|
|
19
|
+
WS_DASHBOARD_EDIT = "ha_mcp_tools/dashboard_edit"
|
|
20
|
+
_LOGGER = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _invalid_success_fields(result: dict[str, Any]) -> list[str]:
|
|
24
|
+
"""Identify invalid consumed fields without copying dashboard values into logs."""
|
|
25
|
+
verified = result.get("post_write_verified")
|
|
26
|
+
config_hash = result.get("config_hash")
|
|
27
|
+
size = result.get("previous_config_size")
|
|
28
|
+
warnings = result.get("warnings", [])
|
|
29
|
+
checks = {
|
|
30
|
+
"success": result.get("success") is True,
|
|
31
|
+
"config": isinstance(result.get("config"), dict),
|
|
32
|
+
"config_hash": "config_hash" in result
|
|
33
|
+
and (isinstance(config_hash, str) if verified else config_hash is None),
|
|
34
|
+
"post_write_verified": isinstance(verified, bool),
|
|
35
|
+
"write_committed": isinstance(result.get("write_committed"), bool),
|
|
36
|
+
"previous_config_size": isinstance(size, int)
|
|
37
|
+
and not isinstance(size, bool)
|
|
38
|
+
and size >= 0,
|
|
39
|
+
"warnings": isinstance(warnings, list)
|
|
40
|
+
and all(isinstance(warning, str) for warning in warnings),
|
|
41
|
+
"unchanged": isinstance(result.get("unchanged", False), bool),
|
|
42
|
+
}
|
|
43
|
+
return [name for name, valid in checks.items() if not valid]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _raise_malformed_reply(
|
|
47
|
+
url_path: str | None, action: str, fields: list[str]
|
|
48
|
+
) -> NoReturn:
|
|
49
|
+
"""Diagnose protocol skew without logging the config, templates, or card values."""
|
|
50
|
+
details = ", ".join(fields)
|
|
51
|
+
_LOGGER.warning("Malformed dashboard_edit response; invalid fields: %s", details)
|
|
52
|
+
raise_dashboard_edit_error(
|
|
53
|
+
url_path,
|
|
54
|
+
"write_outcome_unknown",
|
|
55
|
+
f"Malformed response fields: {details}",
|
|
56
|
+
None,
|
|
57
|
+
action,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _validate_edit_result(
|
|
62
|
+
raw: Any, url_path: str | None, action: str, *, hash_supplied: bool = False
|
|
63
|
+
) -> dict[str, Any]:
|
|
64
|
+
"""An incomplete response cannot establish whether the write happened."""
|
|
65
|
+
if not isinstance(raw, dict):
|
|
66
|
+
_raise_malformed_reply(url_path, action, ["envelope"])
|
|
67
|
+
if raw.get("success") is not True:
|
|
68
|
+
_raise_malformed_reply(url_path, action, ["envelope.success"])
|
|
69
|
+
result = raw.get("result")
|
|
70
|
+
if not isinstance(result, dict):
|
|
71
|
+
_raise_malformed_reply(url_path, action, ["result"])
|
|
72
|
+
if result.get("success") is False:
|
|
73
|
+
error = result.get("error")
|
|
74
|
+
if not isinstance(error, dict) or not isinstance(error.get("code"), str):
|
|
75
|
+
_raise_malformed_reply(url_path, action, ["error.code"])
|
|
76
|
+
committed = result.get("write_committed")
|
|
77
|
+
if committed is not False and committed is not None:
|
|
78
|
+
_raise_malformed_reply(url_path, action, ["write_committed"])
|
|
79
|
+
raise_dashboard_edit_error(
|
|
80
|
+
url_path,
|
|
81
|
+
error["code"],
|
|
82
|
+
str(error.get("message", error["code"])),
|
|
83
|
+
committed,
|
|
84
|
+
action,
|
|
85
|
+
hash_supplied=hash_supplied,
|
|
86
|
+
)
|
|
87
|
+
invalid_fields = _invalid_success_fields(result)
|
|
88
|
+
if invalid_fields:
|
|
89
|
+
_raise_malformed_reply(url_path, action, invalid_fields)
|
|
90
|
+
return result
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _handle_command_failure(
|
|
94
|
+
exc: BaseException, client: Any, url_path: str | None, action: str
|
|
95
|
+
) -> None:
|
|
96
|
+
"""Only a missing command allows fallback; other failures terminate the edit."""
|
|
97
|
+
if isinstance(exc, HomeAssistantCommandNotSent):
|
|
98
|
+
raise_dashboard_edit_error(url_path, "write_not_sent", str(exc), False, action)
|
|
99
|
+
if isinstance(exc, Exception) and is_unknown_command(exc):
|
|
100
|
+
invalidate_caps(client)
|
|
101
|
+
return
|
|
102
|
+
# Core's schema/admin gates reject these before this command's prep runs.
|
|
103
|
+
# Other command errors can originate after dispatch and remain ambiguous.
|
|
104
|
+
if isinstance(exc, HomeAssistantCommandError) and exc.code in {
|
|
105
|
+
"unauthorized",
|
|
106
|
+
"invalid_format",
|
|
107
|
+
}:
|
|
108
|
+
raise_dashboard_edit_error(url_path, exc.code, str(exc), False, action)
|
|
109
|
+
_LOGGER.warning("Dashboard edit command failed after dispatch", exc_info=exc)
|
|
110
|
+
raise_dashboard_edit_error(
|
|
111
|
+
url_path,
|
|
112
|
+
"write_outcome_unknown",
|
|
113
|
+
str(exc) or "Dashboard edit interrupted before its response was received",
|
|
114
|
+
None,
|
|
115
|
+
action,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
async def edit_dashboard_via_component(
|
|
120
|
+
client: Any,
|
|
121
|
+
url_path: str | None,
|
|
122
|
+
*,
|
|
123
|
+
action: str = "set",
|
|
124
|
+
expected_hash: str | None = None,
|
|
125
|
+
config: dict[str, Any] | None = None,
|
|
126
|
+
patch: list[dict[str, Any]] | None = None,
|
|
127
|
+
) -> dict[str, Any] | None:
|
|
128
|
+
"""Return an authoritative edit result, or None for an unavailable command.
|
|
129
|
+
|
|
130
|
+
Legacy fallback requires confirmed absence of a compatible capability or
|
|
131
|
+
HA's definitive unknown_command. Never use the REST client's retrying
|
|
132
|
+
WebSocket bridge for a native write.
|
|
133
|
+
``action`` labels local error responses only; it is not a command argument.
|
|
134
|
+
"""
|
|
135
|
+
# Clients without connection credentials cannot probe the component.
|
|
136
|
+
if not getattr(client, "base_url", None) or not getattr(client, "token", None):
|
|
137
|
+
return None
|
|
138
|
+
try:
|
|
139
|
+
caps = await get_component_caps(client, strict=True)
|
|
140
|
+
if not component_supports(caps, "dashboard_edit"):
|
|
141
|
+
return None
|
|
142
|
+
ws = await get_websocket_client(
|
|
143
|
+
url=client.base_url,
|
|
144
|
+
token=client.token,
|
|
145
|
+
verify_ssl=getattr(client, "verify_ssl", None),
|
|
146
|
+
)
|
|
147
|
+
except Exception as exc:
|
|
148
|
+
_LOGGER.warning(
|
|
149
|
+
"Unable to prepare the dashboard edit connection", exc_info=True
|
|
150
|
+
)
|
|
151
|
+
raise_dashboard_edit_error(url_path, "load_failed", str(exc), False, action)
|
|
152
|
+
|
|
153
|
+
kwargs: dict[str, Any] = {"url_path": url_path}
|
|
154
|
+
if expected_hash is not None:
|
|
155
|
+
kwargs["expected_hash"] = expected_hash
|
|
156
|
+
if config is not None:
|
|
157
|
+
kwargs["config"] = config
|
|
158
|
+
if patch is not None:
|
|
159
|
+
kwargs["patch"] = patch
|
|
160
|
+
try:
|
|
161
|
+
raw = await ws.send_command(WS_DASHBOARD_EDIT, **kwargs)
|
|
162
|
+
except (asyncio.CancelledError, Exception) as exc:
|
|
163
|
+
# Pre-dispatch cancellation still propagates; after dispatch it cannot
|
|
164
|
+
# establish whether Core saved unless an explicit rejection says so.
|
|
165
|
+
_handle_command_failure(exc, client, url_path, action)
|
|
166
|
+
return None
|
|
167
|
+
return _validate_edit_result(
|
|
168
|
+
raw, url_path, action, hash_supplied=expected_hash is not None
|
|
169
|
+
)
|