ha-mcp-dev 8.4.2.dev2534__tar.gz → 8.4.3.dev2542__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.2.dev2534/src/ha_mcp_dev.egg-info → ha_mcp_dev-8.4.3.dev2542}/PKG-INFO +4 -4
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/README.md +3 -3
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/pyproject.toml +1 -1
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/backup_manager.py +266 -29
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/config.py +27 -1
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/read_only.py +23 -0
- ha_mcp_dev-8.4.3.dev2542/src/ha_mcp/renamed_tools.py +103 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/server.py +22 -8
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/settings_ui/_persistence.py +62 -10
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/backup.py +1 -1
- ha_mcp_dev-8.4.3.dev2542/src/ha_mcp/tools/blueprint_sources.py +402 -0
- ha_mcp_dev-8.4.3.dev2542/src/ha_mcp/tools/blueprint_substitute.py +243 -0
- ha_mcp_dev-8.4.3.dev2542/src/ha_mcp/tools/blueprint_write.py +501 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/renamed_tool_middleware.py +9 -2
- ha_mcp_dev-8.4.3.dev2542/src/ha_mcp/tools/tools_blueprints.py +734 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/tools_bug_report.py +22 -3
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/tools_config_automations.py +129 -17
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/tools_config_scripts.py +103 -20
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/tools_filesystem.py +4 -3
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/tools_search.py +4 -1
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/util_helpers.py +3 -1
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/transforms/categorized_search.py +69 -21
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542/src/ha_mcp_dev.egg-info}/PKG-INFO +4 -4
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp_dev.egg-info/SOURCES.txt +3 -0
- ha_mcp_dev-8.4.2.dev2534/src/ha_mcp/renamed_tools.py +0 -43
- ha_mcp_dev-8.4.2.dev2534/src/ha_mcp/tools/tools_blueprints.py +0 -583
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/LICENSE +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/MANIFEST.in +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/setup.cfg +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_rich_compat.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/__init__.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/requirements.txt +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/LICENSE +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/MANIFEST.sha256 +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/VENDORED +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/__init__.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/__main__.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/asyncio/__init__.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/asyncio/client.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/asyncio/connection.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/asyncio/messages.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/asyncio/router.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/asyncio/server.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/auth.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/cli.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/client.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/connection.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/datastructures.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/exceptions.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/extensions/__init__.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/extensions/base.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/extensions/permessage_deflate.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/frames.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/headers.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/http11.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/imports.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/legacy/__init__.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/legacy/auth.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/legacy/client.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/legacy/exceptions.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/legacy/framing.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/legacy/handshake.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/legacy/http.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/legacy/protocol.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/legacy/server.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/protocol.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/proxy.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/py.typed +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/server.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/streams.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/sync/__init__.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/sync/client.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/sync/connection.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/sync/messages.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/sync/router.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/sync/server.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/sync/utils.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/trio/__init__.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/trio/client.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/trio/connection.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/trio/messages.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/trio/router.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/trio/server.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/trio/utils.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/typing.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/uri.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/utils.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_vendor/websockets/version.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/auth/oidc_compat.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/browser_landing.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/dashboard_screenshot/theme_guard.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/hacs_auto_refresh.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/llm_exposure.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/log_filters.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/policy/__init__.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/policy/approval_queue.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/policy/editing.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/policy/evaluator.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/policy/handlers.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/policy/middleware.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/policy/model.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/policy/persistence.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/policy/value_sources.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/redaction.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/resources/skills-vendor/evals/area-target-over-entity-list/case.yaml +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/resources/skills-vendor/evals/arrive-home-automation/case.yaml +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/resources/skills-vendor/evals/assist-shortcut-card/case.yaml +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/resources/skills-vendor/evals/avg-temperature/case.yaml +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/resources/skills-vendor/evals/backup-deletion-confirmation/case.yaml +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/resources/skills-vendor/evals/blueprint-authoring/case.yaml +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/resources/skills-vendor/evals/button-event-entity/case.yaml +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/resources/skills-vendor/evals/device-id-trigger-migration/case.yaml +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/resources/skills-vendor/evals/helper-flow-fields-not-platform-yaml/case.yaml +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/resources/skills-vendor/evals/media-player-song/case.yaml +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/resources/skills-vendor/evals/motion-light-automation-mode/case.yaml +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/resources/skills-vendor/evals/rename-entity-impact-analysis/case.yaml +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/resources/skills-vendor/evals/template-attributes-yaml-exception/case.yaml +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/resources/skills-vendor/evals/timer-finished-automation/case.yaml +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/resources/skills-vendor/evals/vacuum-last-clean/case.yaml +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/resources/skills-vendor/scripts/check_eval_cases.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/backups.md +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/settings_ui/__init__.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/settings_ui/_i18n.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/settings_ui/_locale_policy.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/settings_ui/_theme.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/settings_ui/locales/README.md +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/settings_ui/locales/cs.json +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/settings_ui/locales/de.json +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/settings_ui/locales/en.json +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/settings_ui/locales/eo.json +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/settings_ui/locales/es.json +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/settings_ui/locales/fr.json +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/settings_ui/locales/it.json +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/settings_ui/locales/ko.json +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/settings_ui/locales/nl.json +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/settings_ui/locales/pl.json +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/settings_ui/locales/ru.json +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/settings_ui/locales/sv.json +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/settings_ui/locales/tlh.json +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/settings_ui/locales/zh-Hans.json +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/settings_ui/settings.css +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/settings_ui/settings.html +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/settings_ui/settings.js +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/strict_bps.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/auto_backup.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/best_practice_result.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/best_practice_variables.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/bulk_selector.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/component_api.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/component_config_entries.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/component_config_reads.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/component_devices.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/component_registries.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/component_registry_lookup.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/config_entry_flow.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/config_entry_flow_form.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/config_entry_flow_menu.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/config_entry_flow_walker.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/config_entry_identity.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/config_entry_reconfigure.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/config_entry_reload_watch.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/error_log_parsing.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/hacs_registration.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/integration_reconfigure.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/log_common.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/log_sources.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/log_sources_supervisor.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/radio/__init__.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/radio/base.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/radio/matter.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/radio/thread.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/radio/zigbee.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/radio/zwave.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/smart_search/_base.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/smart_search/_config.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/smart_search/_graph.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/tools_assist_pipeline.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/tools_dev.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/tools_logs.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/tools_radio.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/tools_security_policy.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/tools_themes.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/tools/validation_middleware.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/transport_security.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/update_check.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/utils/config_write_lock.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/utils/device_registry_semantics.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/utils/entity_membership.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/utils/skill_loader.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/visibility/__init__.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/visibility/enforcement.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/visibility/model.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/visibility/persistence.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp/visibility/resolver.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/tests/__init__.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/tests/test_constants.py +0 -0
- {ha_mcp_dev-8.4.2.dev2534 → ha_mcp_dev-8.4.3.dev2542}/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
|
+
Version: 8.4.3.dev2542
|
|
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
|
|
@@ -40,7 +40,7 @@ Dynamic: license-file
|
|
|
40
40
|
<!-- mcp-name: io.github.homeassistant-ai/ha-mcp -->
|
|
41
41
|
|
|
42
42
|
<p align="center">
|
|
43
|
-
<img src="https://img.shields.io/badge/tools-
|
|
43
|
+
<img src="https://img.shields.io/badge/tools-87-blue" alt="95+ Tools">
|
|
44
44
|
<a href="https://github.com/homeassistant-ai/ha-mcp/releases"><img src="https://img.shields.io/github/v/release/homeassistant-ai/ha-mcp" alt="Release"></a>
|
|
45
45
|
<a href="https://github.com/homeassistant-ai/ha-mcp/actions/workflows/e2e-tests.yml"><img src="https://img.shields.io/github/actions/workflow/status/homeassistant-ai/ha-mcp/e2e-tests.yml?branch=master&label=E2E%20Tests" alt="E2E Tests"></a>
|
|
46
46
|
<a href="LICENSE.md"><img src="https://img.shields.io/github/license/homeassistant-ai/ha-mcp.svg" alt="License"></a>
|
|
@@ -247,7 +247,7 @@ Spend less time configuring, more time enjoying your smart home.
|
|
|
247
247
|
<details>
|
|
248
248
|
<!-- TOOLS_TABLE_START -->
|
|
249
249
|
|
|
250
|
-
<summary><b>Complete Tool List (
|
|
250
|
+
<summary><b>Complete Tool List (87 tools)</b></summary>
|
|
251
251
|
|
|
252
252
|
| Category | Tools |
|
|
253
253
|
|----------|-------|
|
|
@@ -255,7 +255,7 @@ Spend less time configuring, more time enjoying your smart home.
|
|
|
255
255
|
| **Areas & Floors** | `ha_list_floors_areas`, `ha_remove_area_or_floor`, `ha_set_area_or_floor` |
|
|
256
256
|
| **Assist** | `ha_manage_pipeline` |
|
|
257
257
|
| **Automations** | `ha_config_get_automation`, `ha_config_remove_automation`, `ha_config_set_automation` |
|
|
258
|
-
| **Blueprints** | `
|
|
258
|
+
| **Blueprints** | `ha_manage_blueprints` |
|
|
259
259
|
| **Calendar** | `ha_config_get_calendar_events`, `ha_config_remove_calendar_event`, `ha_config_set_calendar_event` |
|
|
260
260
|
| **Camera** | `ha_get_camera_image` |
|
|
261
261
|
| **Dashboard** | `ha_get_dashboard_screenshot` *(beta)* |
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<!-- mcp-name: io.github.homeassistant-ai/ha-mcp -->
|
|
9
9
|
|
|
10
10
|
<p align="center">
|
|
11
|
-
<img src="https://img.shields.io/badge/tools-
|
|
11
|
+
<img src="https://img.shields.io/badge/tools-87-blue" alt="95+ Tools">
|
|
12
12
|
<a href="https://github.com/homeassistant-ai/ha-mcp/releases"><img src="https://img.shields.io/github/v/release/homeassistant-ai/ha-mcp" alt="Release"></a>
|
|
13
13
|
<a href="https://github.com/homeassistant-ai/ha-mcp/actions/workflows/e2e-tests.yml"><img src="https://img.shields.io/github/actions/workflow/status/homeassistant-ai/ha-mcp/e2e-tests.yml?branch=master&label=E2E%20Tests" alt="E2E Tests"></a>
|
|
14
14
|
<a href="LICENSE.md"><img src="https://img.shields.io/github/license/homeassistant-ai/ha-mcp.svg" alt="License"></a>
|
|
@@ -215,7 +215,7 @@ Spend less time configuring, more time enjoying your smart home.
|
|
|
215
215
|
<details>
|
|
216
216
|
<!-- TOOLS_TABLE_START -->
|
|
217
217
|
|
|
218
|
-
<summary><b>Complete Tool List (
|
|
218
|
+
<summary><b>Complete Tool List (87 tools)</b></summary>
|
|
219
219
|
|
|
220
220
|
| Category | Tools |
|
|
221
221
|
|----------|-------|
|
|
@@ -223,7 +223,7 @@ Spend less time configuring, more time enjoying your smart home.
|
|
|
223
223
|
| **Areas & Floors** | `ha_list_floors_areas`, `ha_remove_area_or_floor`, `ha_set_area_or_floor` |
|
|
224
224
|
| **Assist** | `ha_manage_pipeline` |
|
|
225
225
|
| **Automations** | `ha_config_get_automation`, `ha_config_remove_automation`, `ha_config_set_automation` |
|
|
226
|
-
| **Blueprints** | `
|
|
226
|
+
| **Blueprints** | `ha_manage_blueprints` |
|
|
227
227
|
| **Calendar** | `ha_config_get_calendar_events`, `ha_config_remove_calendar_event`, `ha_config_set_calendar_event` |
|
|
228
228
|
| **Camera** | `ha_get_camera_image` |
|
|
229
229
|
| **Dashboard** | `ha_get_dashboard_screenshot` *(beta)* |
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ha-mcp-dev"
|
|
7
|
-
version = "8.4.
|
|
7
|
+
version = "8.4.3.dev2542"
|
|
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
|
|
@@ -40,6 +40,7 @@ from __future__ import annotations
|
|
|
40
40
|
|
|
41
41
|
import asyncio
|
|
42
42
|
import difflib
|
|
43
|
+
import hashlib
|
|
43
44
|
import logging
|
|
44
45
|
import os
|
|
45
46
|
import re
|
|
@@ -103,15 +104,24 @@ class MandatoryBackupError(Exception):
|
|
|
103
104
|
_TRACKER_SOFT_CAP = 10_000
|
|
104
105
|
_TRACKER_PRUNE_BATCH = 1_000
|
|
105
106
|
|
|
106
|
-
# Filename pattern: <domain>.<safe_entity_id>.<YYYYMMDD_HHMMSS
|
|
107
|
+
# Filename pattern: <domain>.<safe_entity_id>.<YYYYMMDD_HHMMSS>[_NN].yaml
|
|
107
108
|
# The middle ``.`` separators make the timestamp rsplit reliable even
|
|
108
|
-
# when entity_id contains dots (after sanitization, dots are kept).
|
|
109
|
+
# when entity_id contains dots (after sanitization, dots are kept). ``_NN``
|
|
110
|
+
# is the same-second sequence (see ``_unclaimed_target``): the timestamp is
|
|
111
|
+
# a second and the throttle defaults to 0, so a second capture of one entity
|
|
112
|
+
# inside that second takes the next free suffix rather than replacing the
|
|
113
|
+
# first. ``_`` sorts after ``.``, so a suffixed name stays newest-last.
|
|
109
114
|
_FILENAME_RE = re.compile(
|
|
110
115
|
r"^(?P<domain>[A-Za-z0-9_]+)\."
|
|
111
116
|
r"(?P<entity_id>[A-Za-z0-9._-]+)\."
|
|
112
|
-
r"(?P<ts>\d{8}_\d{6})
|
|
117
|
+
r"(?P<ts>\d{8}_\d{6})(?:_(?P<seq>\d{2}))?\.yaml$"
|
|
113
118
|
)
|
|
114
119
|
|
|
120
|
+
# Captures of one entity inside one second before the manager gives up on a
|
|
121
|
+
# free name. Two is the realistic case (a save then a delete of the same
|
|
122
|
+
# blueprint); the cap only bounds the ``exists()`` probe.
|
|
123
|
+
_MAX_SAME_SECOND = 100
|
|
124
|
+
|
|
115
125
|
# Domains whose snapshot ``config`` is raw text (file/YAML content) rather
|
|
116
126
|
# than a structured dict. They carry a ``kind: "text"`` marker in the
|
|
117
127
|
# snapshot payload and diff via a unified text diff instead of JSON-Patch
|
|
@@ -121,7 +131,19 @@ _FILENAME_RE = re.compile(
|
|
|
121
131
|
# (read_file), but restored via edit_yaml_config(action="replace_file") because
|
|
122
132
|
# write_file rejects config files. It backs the pre-restore safety snapshot and
|
|
123
133
|
# the legacy-store restore (#1579).
|
|
124
|
-
|
|
134
|
+
#
|
|
135
|
+
# ``blueprint_automation`` / ``blueprint_script`` snapshot the blueprint's raw
|
|
136
|
+
# YAML text (#2329): core stores blueprints as files, and ``blueprint/save``
|
|
137
|
+
# takes the same YAML string back, so the round trip is text on both ends.
|
|
138
|
+
_TEXT_DOMAINS = frozenset(
|
|
139
|
+
{
|
|
140
|
+
"file",
|
|
141
|
+
"yaml",
|
|
142
|
+
"yaml_file",
|
|
143
|
+
"blueprint_automation",
|
|
144
|
+
"blueprint_script",
|
|
145
|
+
}
|
|
146
|
+
)
|
|
125
147
|
|
|
126
148
|
# Pre-#1579 backups (``.ha_mcp_tools_backups/*.bak``) are surfaced through the
|
|
127
149
|
# same scope="edits" actions under a synthetic name ``legacy:<filename>``. The
|
|
@@ -163,17 +185,74 @@ class DomainHandler:
|
|
|
163
185
|
# ----------------------------- backup manager -------------------------------
|
|
164
186
|
|
|
165
187
|
|
|
188
|
+
def _entity_id_aliases(entity_id: str) -> list[str]:
|
|
189
|
+
"""Every filename stem a snapshot of ``entity_id`` may carry.
|
|
190
|
+
|
|
191
|
+
Sanitised ids gained a digest suffix, so snapshots written before that
|
|
192
|
+
sit under the bare sanitised name. Rotation and entity filtering match
|
|
193
|
+
both, otherwise those older files would never be counted again: they
|
|
194
|
+
would never be pruned, and would drop out of an entity-filtered listing.
|
|
195
|
+
"""
|
|
196
|
+
current = _safe_entity_id(entity_id)
|
|
197
|
+
legacy = _SAFE_ID_RE.sub("_", entity_id).lstrip(".") or "_"
|
|
198
|
+
return [current] if legacy == current else [current, legacy]
|
|
199
|
+
|
|
200
|
+
|
|
166
201
|
def _safe_entity_id(entity_id: str) -> str:
|
|
167
202
|
"""Sanitize an entity id for use in a filename.
|
|
168
203
|
|
|
169
204
|
Replaces any character outside ``[A-Za-z0-9._-]`` with ``_``. Path
|
|
170
205
|
separators get caught by this (the regex excludes both ``/`` and ``\\``).
|
|
171
206
|
Strips leading dots to prevent dotfile collisions.
|
|
207
|
+
|
|
208
|
+
Sanitizing is lossy, so ids that needed it get a short digest of the
|
|
209
|
+
ORIGINAL appended. Blueprint domains (#2329) key on paths, where
|
|
210
|
+
``user/motion.yaml`` and ``user_motion.yaml`` both clean to
|
|
211
|
+
``user_motion.yaml`` -- without the digest they share one snapshot
|
|
212
|
+
namespace, so captures in the same second overwrite each other and
|
|
213
|
+
rotation counts both histories as one, able to delete the only restore
|
|
214
|
+
point for a blueprint that was never written. Entity ids are already
|
|
215
|
+
within the safe set, so their filenames are unchanged and existing
|
|
216
|
+
snapshots stay discoverable.
|
|
172
217
|
"""
|
|
173
218
|
if not entity_id:
|
|
174
219
|
return "_"
|
|
175
220
|
cleaned = _SAFE_ID_RE.sub("_", entity_id).lstrip(".")
|
|
176
|
-
|
|
221
|
+
if not cleaned:
|
|
222
|
+
return "_"
|
|
223
|
+
if cleaned == entity_id:
|
|
224
|
+
return cleaned
|
|
225
|
+
digest = hashlib.sha256(entity_id.encode("utf-8")).hexdigest()[:8]
|
|
226
|
+
return f"{cleaned}-{digest}"
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _snapshot_order(path: Path) -> tuple[str, str, str]:
|
|
230
|
+
"""Sort key placing snapshots in capture order, oldest first.
|
|
231
|
+
|
|
232
|
+
Filenames cannot be compared directly: an id the sanitiser rewrote has
|
|
233
|
+
two stems, and the digest stem sorts before the bare one on every name
|
|
234
|
+
(``-`` < ``.``) regardless of timestamp, so by name every new capture
|
|
235
|
+
would age below every pre-digest file. The parsed timestamp and the
|
|
236
|
+
same-second sequence order them; the name only breaks exact ties.
|
|
237
|
+
"""
|
|
238
|
+
m = _FILENAME_RE.match(path.name)
|
|
239
|
+
if m is None:
|
|
240
|
+
return ("", "", path.name)
|
|
241
|
+
return (m.group("ts"), m.group("seq") or "", path.name)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _id_matches(payload_id: str | None, wanted: str) -> bool:
|
|
245
|
+
"""Whether a snapshot whose payload names ``payload_id`` is ``wanted``.
|
|
246
|
+
|
|
247
|
+
Two spellings select an entity: the id itself, and the digest stem
|
|
248
|
+
``_safe_entity_id`` derives from it — the stem a listing used to show and
|
|
249
|
+
a filename still carries. Both name exactly one entity. The bare
|
|
250
|
+
pre-digest stem is deliberately not accepted: it is the current stem of a
|
|
251
|
+
differently-named entity, which is the ambiguity this check exists for.
|
|
252
|
+
"""
|
|
253
|
+
if payload_id is None:
|
|
254
|
+
return False
|
|
255
|
+
return payload_id == wanted or _safe_entity_id(payload_id) == wanted
|
|
177
256
|
|
|
178
257
|
|
|
179
258
|
def _now_ts() -> str:
|
|
@@ -517,9 +596,7 @@ class BackupManager:
|
|
|
517
596
|
self, domain: str, entity_id: str, config: Any, tool_name: str | None
|
|
518
597
|
) -> Path:
|
|
519
598
|
safe = _safe_entity_id(entity_id)
|
|
520
|
-
|
|
521
|
-
filename = f"{domain}.{safe}.{ts}.yaml"
|
|
522
|
-
target = self._dir / filename
|
|
599
|
+
target = self._unclaimed_target(domain, safe, _now_ts())
|
|
523
600
|
payload: dict[str, Any] = {
|
|
524
601
|
"schema_version": SCHEMA_VERSION,
|
|
525
602
|
"domain": domain,
|
|
@@ -538,10 +615,76 @@ class BackupManager:
|
|
|
538
615
|
logger.info("Auto-backup: wrote %s", target.name)
|
|
539
616
|
return target
|
|
540
617
|
|
|
618
|
+
def _unclaimed_target(self, domain: str, safe: str, ts: str) -> Path:
|
|
619
|
+
"""First filename for ``ts`` that no snapshot of this entity holds yet.
|
|
620
|
+
|
|
621
|
+
The bare name is taken when it is free; each later capture inside the
|
|
622
|
+
same second appends ``_01``, ``_02``, ... so the atomic replace in
|
|
623
|
+
``_write_snapshot`` can never discard an earlier pre-write state.
|
|
624
|
+
``maybe_snapshot`` serialises captures per key, so the ``exists()``
|
|
625
|
+
probe is not racing another capture of the same entity.
|
|
626
|
+
"""
|
|
627
|
+
target = self._dir / f"{domain}.{safe}.{ts}.yaml"
|
|
628
|
+
for seq in range(1, _MAX_SAME_SECOND):
|
|
629
|
+
if not target.exists():
|
|
630
|
+
return target
|
|
631
|
+
target = self._dir / f"{domain}.{safe}.{ts}_{seq:02d}.yaml"
|
|
632
|
+
if not target.exists():
|
|
633
|
+
return target
|
|
634
|
+
raise OSError(
|
|
635
|
+
f"{_MAX_SAME_SECOND} snapshots of {domain}:{safe} inside one second"
|
|
636
|
+
)
|
|
637
|
+
|
|
638
|
+
def _payload_entity_id(self, path: Path) -> str | None:
|
|
639
|
+
"""The id the snapshot at ``path`` was taken for, from its payload.
|
|
640
|
+
|
|
641
|
+
Filename stems are ambiguous in both directions: the legacy stem for
|
|
642
|
+
``user/motion.yaml`` is ``user_motion.yaml``, which is also the
|
|
643
|
+
CURRENT stem of a differently-named blueprint. The payload keeps the
|
|
644
|
+
original id, so rotation and the entity filter behind ``list_snapshots``
|
|
645
|
+
/ ``delete_bulk`` ask it rather than trusting the name. A file that
|
|
646
|
+
cannot be read answers ``None`` and is treated as not ours: skipping it
|
|
647
|
+
wastes a rotation slot or hides it from one entity's listing, deleting
|
|
648
|
+
it could destroy another entity's only restore point.
|
|
649
|
+
|
|
650
|
+
Only the header is read. ``_write_snapshot`` emits ``entity_id`` before
|
|
651
|
+
``config`` (which is the whole automation, dashboard or file), so the
|
|
652
|
+
read stops at the ``config:`` line and never parses the body: every
|
|
653
|
+
listing row opens its file, and an unfiltered listing of hundreds of
|
|
654
|
+
whole-file snapshots has to stay a header read per row.
|
|
655
|
+
"""
|
|
656
|
+
try:
|
|
657
|
+
with path.open(encoding="utf-8") as handle:
|
|
658
|
+
header: list[str] = []
|
|
659
|
+
for line in handle:
|
|
660
|
+
if line.startswith("config:"):
|
|
661
|
+
break
|
|
662
|
+
header.append(line)
|
|
663
|
+
loaded = yaml.safe_load("".join(header))
|
|
664
|
+
except (OSError, yaml.YAMLError) as err:
|
|
665
|
+
logger.warning(
|
|
666
|
+
"Auto-backup: cannot identify %s, leaving it in place: %s",
|
|
667
|
+
path.name,
|
|
668
|
+
err,
|
|
669
|
+
)
|
|
670
|
+
return None
|
|
671
|
+
if not isinstance(loaded, dict):
|
|
672
|
+
return None
|
|
673
|
+
found = loaded.get("entity_id")
|
|
674
|
+
return found if isinstance(found, str) else None
|
|
675
|
+
|
|
676
|
+
def _snapshot_is_for(self, path: Path, entity_id: str) -> bool:
|
|
677
|
+
"""Whether ``path`` holds a snapshot of ``entity_id`` (see ``_id_matches``)."""
|
|
678
|
+
return _id_matches(self._payload_entity_id(path), entity_id)
|
|
679
|
+
|
|
541
680
|
def _rotate(self, domain: str, entity_id: str) -> None:
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
681
|
+
candidates: set[Path] = set()
|
|
682
|
+
for safe in _entity_id_aliases(entity_id):
|
|
683
|
+
candidates.update(self._dir.glob(f"{domain}.{safe}.*.yaml"))
|
|
684
|
+
files = sorted(
|
|
685
|
+
(p for p in candidates if self._snapshot_is_for(p, entity_id)),
|
|
686
|
+
key=_snapshot_order,
|
|
687
|
+
)
|
|
545
688
|
excess = len(files) - self.retain_per_entity
|
|
546
689
|
for old in files[: max(0, excess)]:
|
|
547
690
|
try:
|
|
@@ -566,30 +709,64 @@ class BackupManager:
|
|
|
566
709
|
# would otherwise never match a filter passed in original form.
|
|
567
710
|
# Sanitize the filter once up front so the per-file comparison is
|
|
568
711
|
# symmetric: filter and ``meta["entity_id"]`` both come from the
|
|
569
|
-
# same sanitization function.
|
|
570
|
-
|
|
712
|
+
# same sanitization function. The stem only narrows the candidates:
|
|
713
|
+
# sanitising is lossy in both directions (``user/motion.yaml``'s
|
|
714
|
+
# pre-digest stem IS ``user_motion.yaml``'s current stem), so each
|
|
715
|
+
# stem match is confirmed against the id the payload stores before it
|
|
716
|
+
# counts as this entity's — ``delete_bulk`` unlinks whatever is
|
|
717
|
+
# returned here, and the caller named one entity.
|
|
718
|
+
safe_filter = set(_entity_id_aliases(entity_id)) if entity_id else None
|
|
571
719
|
out: list[dict[str, Any]] = []
|
|
572
|
-
#
|
|
573
|
-
#
|
|
574
|
-
for path in sorted(self._dir.glob("*.yaml"), reverse=True):
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
if
|
|
579
|
-
continue
|
|
580
|
-
if safe_filter and meta["entity_id"] != safe_filter:
|
|
581
|
-
continue
|
|
582
|
-
try:
|
|
583
|
-
stat = path.stat()
|
|
584
|
-
except OSError:
|
|
720
|
+
# Newest first by capture time — see ``_snapshot_order`` for why the
|
|
721
|
+
# filename itself is not the key.
|
|
722
|
+
for path in sorted(self._dir.glob("*.yaml"), key=_snapshot_order, reverse=True):
|
|
723
|
+
row = self._listing_row(
|
|
724
|
+
path, domain=domain, safe_filter=safe_filter, entity_id=entity_id
|
|
725
|
+
)
|
|
726
|
+
if row is None:
|
|
585
727
|
continue
|
|
586
|
-
|
|
587
|
-
meta["mtime"] = stat.st_mtime
|
|
588
|
-
out.append(meta)
|
|
728
|
+
out.append(row)
|
|
589
729
|
if limit and len(out) >= limit:
|
|
590
730
|
break
|
|
591
731
|
return out
|
|
592
732
|
|
|
733
|
+
def _listing_row(
|
|
734
|
+
self,
|
|
735
|
+
path: Path,
|
|
736
|
+
*,
|
|
737
|
+
domain: str | None,
|
|
738
|
+
safe_filter: set[str] | None,
|
|
739
|
+
entity_id: str | None,
|
|
740
|
+
) -> dict[str, Any] | None:
|
|
741
|
+
"""One ``list_snapshots`` entry for ``path``, or ``None`` if filtered out."""
|
|
742
|
+
meta = self._parse_filename(path.name)
|
|
743
|
+
if meta is None:
|
|
744
|
+
return None
|
|
745
|
+
if domain and meta["domain"] != domain:
|
|
746
|
+
return None
|
|
747
|
+
if safe_filter and meta["entity_id"] not in safe_filter:
|
|
748
|
+
return None
|
|
749
|
+
# Every row reports the id its payload names, not its stem: a filter
|
|
750
|
+
# needs the stored id to settle the stem ambiguity anyway, and a stem
|
|
751
|
+
# shown to the caller — digest or pre-digest — is a value the filter
|
|
752
|
+
# would refuse or misroute, where the stored id is what the settings
|
|
753
|
+
# UI and ha_manage_backup can send straight back and get that entity's
|
|
754
|
+
# whole history. The read is header-only (``_payload_entity_id``); a
|
|
755
|
+
# file that cannot identify itself keeps its stem in an unfiltered
|
|
756
|
+
# listing and is left out of a filtered one.
|
|
757
|
+
payload_id = self._payload_entity_id(path)
|
|
758
|
+
if entity_id and not _id_matches(payload_id, entity_id):
|
|
759
|
+
return None
|
|
760
|
+
if payload_id is not None:
|
|
761
|
+
meta["entity_id"] = payload_id
|
|
762
|
+
try:
|
|
763
|
+
stat = path.stat()
|
|
764
|
+
except OSError:
|
|
765
|
+
return None
|
|
766
|
+
meta["size"] = stat.st_size
|
|
767
|
+
meta["mtime"] = stat.st_mtime
|
|
768
|
+
return meta
|
|
769
|
+
|
|
593
770
|
def _parse_filename(self, name: str) -> dict[str, Any] | None:
|
|
594
771
|
m = _FILENAME_RE.match(name)
|
|
595
772
|
if m is None:
|
|
@@ -2272,6 +2449,62 @@ async def _read_legacy_backup(client: Any, filename: str) -> dict[str, Any]:
|
|
|
2272
2449
|
return unwrap_service_response(result)
|
|
2273
2450
|
|
|
2274
2451
|
|
|
2452
|
+
# Blueprints (#2329) — ``ha_manage_blueprints`` can unlink a blueprint file that
|
|
2453
|
+
# core will not rebuild (``action="delete"``) or overwrite one
|
|
2454
|
+
# (``action="save"``), so the pre-write snapshot is the only way back.
|
|
2455
|
+
# ``entity_id`` is the blueprint path inside ``blueprints/<domain>/`` (e.g.
|
|
2456
|
+
# ``user/motion.yaml``) and the snapshot ``config`` is the raw YAML text,
|
|
2457
|
+
# restored through ``blueprint/save``.
|
|
2458
|
+
|
|
2459
|
+
|
|
2460
|
+
async def _fetch_blueprint(client: Any, path: str, domain: str) -> Any:
|
|
2461
|
+
"""Fetch a blueprint's YAML for snapshotting, best copy first.
|
|
2462
|
+
|
|
2463
|
+
Delegates to the shared tier ladder in ``tools.blueprint_sources`` — the
|
|
2464
|
+
same one ``ha_manage_blueprints(action="get")`` walks — but only its
|
|
2465
|
+
installed-file tiers: the embedded read, the component's ``blueprint_get``
|
|
2466
|
+
text, and the tools entry's ``read_file``. The ``source_url`` re-fetch
|
|
2467
|
+
``get`` may fall back to is deliberately NOT a snapshot source: it is what
|
|
2468
|
+
the author publishes now, so a restore from it could silently write
|
|
2469
|
+
different YAML than the delete or overwrite destroyed.
|
|
2470
|
+
|
|
2471
|
+
``None`` means there is nothing to snapshot (no installed-file tier could
|
|
2472
|
+
serve it); the decorator logs that skip and the write proceeds
|
|
2473
|
+
un-backed-up, which is the best-effort contract.
|
|
2474
|
+
"""
|
|
2475
|
+
from .tools.blueprint_sources import resolve_blueprint_source
|
|
2476
|
+
|
|
2477
|
+
found = await resolve_blueprint_source(client, domain, path, source_url=None)
|
|
2478
|
+
return found.text
|
|
2479
|
+
|
|
2480
|
+
|
|
2481
|
+
async def _restore_blueprint(client: Any, path: str, config: Any, domain: str) -> Any:
|
|
2482
|
+
"""Re-install a captured blueprint via ``blueprint/save``."""
|
|
2483
|
+
response = await client.send_websocket_message(
|
|
2484
|
+
{
|
|
2485
|
+
"type": "blueprint/save",
|
|
2486
|
+
"domain": domain,
|
|
2487
|
+
"path": path,
|
|
2488
|
+
"yaml": str(config),
|
|
2489
|
+
"allow_override": True,
|
|
2490
|
+
}
|
|
2491
|
+
)
|
|
2492
|
+
if not isinstance(response, dict) or not response.get("success"):
|
|
2493
|
+
error = response.get("error") if isinstance(response, dict) else response
|
|
2494
|
+
raise HomeAssistantError(f"blueprint/save restore failed for {path!r}: {error}")
|
|
2495
|
+
return response.get("result") or {}
|
|
2496
|
+
|
|
2497
|
+
|
|
2498
|
+
def _make_blueprint_handler(domain: str) -> DomainHandler:
|
|
2499
|
+
async def fetch(client: Any, entity_id: str) -> Any:
|
|
2500
|
+
return await _fetch_blueprint(client, entity_id, domain)
|
|
2501
|
+
|
|
2502
|
+
async def restore(client: Any, entity_id: str, config: Any) -> Any:
|
|
2503
|
+
return await _restore_blueprint(client, entity_id, config, domain)
|
|
2504
|
+
|
|
2505
|
+
return DomainHandler(domain=f"blueprint_{domain}", fetch=fetch, restore=restore)
|
|
2506
|
+
|
|
2507
|
+
|
|
2275
2508
|
def _make_helper_handler(helper_type: str) -> DomainHandler:
|
|
2276
2509
|
async def fetch(client: Any, entity_id: str) -> Any:
|
|
2277
2510
|
return await _fetch_helper(client, entity_id, helper_type)
|
|
@@ -2325,5 +2558,9 @@ def register_default_handlers(mgr: BackupManager, _client: Any) -> None:
|
|
|
2325
2558
|
# edit_yaml_config(replace_file) since write_file rejects config files.
|
|
2326
2559
|
# Backs the legacy-restore write path and its pre-restore safety snapshot.
|
|
2327
2560
|
mgr.register(DomainHandler("yaml_file", _fetch_file, _restore_yaml_file))
|
|
2561
|
+
# Blueprint files (#2329): the pre-write snapshot for
|
|
2562
|
+
# ha_manage_blueprints(action="delete" / "save").
|
|
2563
|
+
for blueprint_domain in ("automation", "script"):
|
|
2564
|
+
mgr.register(_make_blueprint_handler(blueprint_domain))
|
|
2328
2565
|
for helper_type in _KNOWN_HELPER_TYPES:
|
|
2329
2566
|
mgr.register(_make_helper_handler(helper_type))
|
|
@@ -1548,7 +1548,10 @@ _EMBEDDED_CONNECTION: dict[str, str | bool] = {}
|
|
|
1548
1548
|
|
|
1549
1549
|
|
|
1550
1550
|
def set_embedded_connection(
|
|
1551
|
-
url: str,
|
|
1551
|
+
url: str,
|
|
1552
|
+
token: str,
|
|
1553
|
+
verify_ssl: bool | None = None,
|
|
1554
|
+
config_dir: str | None = None,
|
|
1552
1555
|
) -> None:
|
|
1553
1556
|
"""Register the in-process HA connection for embedded mode.
|
|
1554
1557
|
|
|
@@ -1572,6 +1575,14 @@ def set_embedded_connection(
|
|
|
1572
1575
|
for 127.0.0.1, so verification on the loopback connection can only fail.
|
|
1573
1576
|
``None`` (the default, and what pre-#1890 components pass implicitly)
|
|
1574
1577
|
leaves ``Settings.verify_ssl`` alone.
|
|
1578
|
+
|
|
1579
|
+
``config_dir`` is Home Assistant's own configuration directory (#2329).
|
|
1580
|
+
Embedded mode is its only writer and it is read back through
|
|
1581
|
+
:func:`get_embedded_config_dir` alone — deliberately NOT a ``Settings``
|
|
1582
|
+
field, so no env var or override file can point it anywhere. It lets an
|
|
1583
|
+
in-process server read a blueprint's on-disk YAML directly instead of
|
|
1584
|
+
routing the read through the component, and grants nothing an external
|
|
1585
|
+
server could not already reach.
|
|
1575
1586
|
"""
|
|
1576
1587
|
_EMBEDDED_CONNECTION["url"] = url
|
|
1577
1588
|
_EMBEDDED_CONNECTION["token"] = token
|
|
@@ -1579,10 +1590,25 @@ def set_embedded_connection(
|
|
|
1579
1590
|
_EMBEDDED_CONNECTION.pop("verify_ssl", None)
|
|
1580
1591
|
else:
|
|
1581
1592
|
_EMBEDDED_CONNECTION["verify_ssl"] = verify_ssl
|
|
1593
|
+
if config_dir is None:
|
|
1594
|
+
_EMBEDDED_CONNECTION.pop("config_dir", None)
|
|
1595
|
+
else:
|
|
1596
|
+
_EMBEDDED_CONNECTION["config_dir"] = config_dir
|
|
1582
1597
|
if _settings is not None:
|
|
1583
1598
|
_apply_embedded_connection(_settings)
|
|
1584
1599
|
|
|
1585
1600
|
|
|
1601
|
+
def get_embedded_config_dir() -> str | None:
|
|
1602
|
+
"""Home Assistant's configuration directory, when running embedded.
|
|
1603
|
+
|
|
1604
|
+
``None`` outside embedded mode, and on an embedded install whose component
|
|
1605
|
+
predates the ``config_dir`` keyword. Consumers must treat it as an optional
|
|
1606
|
+
capability and fall back to their component / service path.
|
|
1607
|
+
"""
|
|
1608
|
+
config_dir = _EMBEDDED_CONNECTION.get("config_dir")
|
|
1609
|
+
return config_dir if isinstance(config_dir, str) and config_dir else None
|
|
1610
|
+
|
|
1611
|
+
|
|
1586
1612
|
def _reset_embedded_connection() -> None:
|
|
1587
1613
|
"""Drop the registered embedded connection (test seam).
|
|
1588
1614
|
|
|
@@ -153,6 +153,21 @@ def _updates_write(args: dict[str, Any]) -> str | None:
|
|
|
153
153
|
return f"action={action!r}"
|
|
154
154
|
|
|
155
155
|
|
|
156
|
+
def _blueprint_write(args: dict[str, Any]) -> str | None:
|
|
157
|
+
# Reads (allowed): listing installed blueprints and reading one's
|
|
158
|
+
# metadata/inputs/body. ``substitute`` joins them because it is a pure
|
|
159
|
+
# render — core builds the standalone config in memory and sends it back,
|
|
160
|
+
# touching neither the blueprint store nor any automation/script; writing
|
|
161
|
+
# the rendered config anywhere still needs ha_config_set_automation /
|
|
162
|
+
# ha_config_set_script, which read-only mode blocks. ``import`` (writes a
|
|
163
|
+
# blueprint file), ``save`` (overwrites one with caller-supplied YAML) and
|
|
164
|
+
# ``delete`` stay blocked, and a missing/unknown action fails closed.
|
|
165
|
+
action = args.get("action")
|
|
166
|
+
if action in ("list", "get", "substitute"):
|
|
167
|
+
return None
|
|
168
|
+
return f"action={action!r}"
|
|
169
|
+
|
|
170
|
+
|
|
156
171
|
def _theme_write(args: dict[str, Any]) -> str | None:
|
|
157
172
|
# Reads (allowed): listing installed themes and inspecting the screenshot
|
|
158
173
|
# engine account's saved per-user theme. The latter has no pure-read
|
|
@@ -259,6 +274,14 @@ READ_ONLY_EXEMPT_TOOLS: dict[str, ReadOnlyExemption] = {
|
|
|
259
274
|
_security_policy_write,
|
|
260
275
|
"reading the policy document (action='get')",
|
|
261
276
|
),
|
|
277
|
+
# Blueprint listing and reading exist only here — ha_get_blueprint was
|
|
278
|
+
# merged into this tool (issue #2329), so hiding it would remove the read
|
|
279
|
+
# surface entirely.
|
|
280
|
+
"ha_manage_blueprints": ReadOnlyExemption(
|
|
281
|
+
_blueprint_write,
|
|
282
|
+
"listing and reading blueprints (action='list' or 'get') and "
|
|
283
|
+
"rendering a standalone config (action='substitute')",
|
|
284
|
+
),
|
|
262
285
|
}
|
|
263
286
|
|
|
264
287
|
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Tools whose names moved, and the stored state that has to follow them.
|
|
2
|
+
|
|
3
|
+
Home Assistant 2026.2 renamed add-ons to apps, and this server's two add-on
|
|
4
|
+
tools moved with it. Issue #2329 then folded the two blueprint tools into one
|
|
5
|
+
``ha_manage_blueprints``. Neither is a breaking change for callers — clients
|
|
6
|
+
resolve by name at runtime, and ``RenamedToolAliasMiddleware`` keeps the old
|
|
7
|
+
name callable for one that has not re-listed yet.
|
|
8
|
+
|
|
9
|
+
What does not repair itself is state a user already stored under the old name:
|
|
10
|
+
a tool they disabled or pinned in the settings UI, a ``DISABLED_TOOLS`` entry,
|
|
11
|
+
a policy rule naming the tool. Those keys are read through the mapping here,
|
|
12
|
+
so the setting survives the rename instead of falling back to the default —
|
|
13
|
+
which for a disabled write tool means silently re-enabling it.
|
|
14
|
+
|
|
15
|
+
A consolidation adds two wrinkles a plain rename does not have. Two retired
|
|
16
|
+
names can now carry different stored states for one current tool, so the
|
|
17
|
+
readers pick the more restrictive one (``rename_retired_keys``'s ``prefer``).
|
|
18
|
+
And a call on a retired name carries the old signature, which the merged tool
|
|
19
|
+
cannot dispatch without the ``action`` it never had —
|
|
20
|
+
``adapt_retired_arguments`` fills that in.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from collections.abc import Callable, Mapping
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
# Retired tool name -> the name that serves it today.
|
|
29
|
+
RENAMED_TOOLS: dict[str, str] = {
|
|
30
|
+
"ha_get_addon": "ha_get_app",
|
|
31
|
+
"ha_manage_addon": "ha_manage_app",
|
|
32
|
+
# #2329: consolidated, not renamed — both retired tools answer to one
|
|
33
|
+
# action-dispatched tool. A call on either old name needs an ``action``
|
|
34
|
+
# (see ``adapt_retired_arguments``), and stored state for the two can
|
|
35
|
+
# disagree (see ``rename_retired_keys``).
|
|
36
|
+
"ha_get_blueprint": "ha_manage_blueprints",
|
|
37
|
+
"ha_import_blueprint": "ha_manage_blueprints",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def current_tool_name(name: str) -> str:
|
|
42
|
+
"""The name a tool answers to today, given any name it has had."""
|
|
43
|
+
return RENAMED_TOOLS.get(name, name)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def rename_retired_keys[T](
|
|
47
|
+
states: Mapping[str, T], *, prefer: Callable[[T, T], T] | None = None
|
|
48
|
+
) -> dict[str, T]:
|
|
49
|
+
"""Re-key a per-tool mapping onto the current tool names.
|
|
50
|
+
|
|
51
|
+
A value already stored under the current name wins: it was set against
|
|
52
|
+
the tool as it is now, while the retired key is inherited.
|
|
53
|
+
|
|
54
|
+
When two retired names were folded into one current tool and both carry
|
|
55
|
+
a value, ``prefer(a, b)`` picks the one to keep; readers pass the more
|
|
56
|
+
restrictive choice for their value type (a disabled tool must not come
|
|
57
|
+
back enabled because its sibling was merely pinned). Without ``prefer``
|
|
58
|
+
the later key in iteration order wins, which is only acceptable for
|
|
59
|
+
mappings that cannot collide.
|
|
60
|
+
"""
|
|
61
|
+
renamed: dict[str, T] = {}
|
|
62
|
+
for name, value in states.items():
|
|
63
|
+
if name not in RENAMED_TOOLS:
|
|
64
|
+
continue
|
|
65
|
+
current = current_tool_name(name)
|
|
66
|
+
if current in renamed and prefer is not None:
|
|
67
|
+
renamed[current] = prefer(renamed[current], value)
|
|
68
|
+
else:
|
|
69
|
+
renamed[current] = value
|
|
70
|
+
kept = {name: value for name, value in states.items() if name not in RENAMED_TOOLS}
|
|
71
|
+
return {**renamed, **kept}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _adapt_get_blueprint(arguments: dict[str, Any]) -> dict[str, Any]:
|
|
75
|
+
# ``ha_get_blueprint`` listed without a path and read one with it.
|
|
76
|
+
return {"action": "get" if arguments.get("path") else "list", **arguments}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _adapt_import_blueprint(arguments: dict[str, Any]) -> dict[str, Any]:
|
|
80
|
+
return {"action": "import", **arguments}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# Retired name -> how its call arguments map onto the current tool's. Only a
|
|
84
|
+
# consolidation needs one; a plain rename forwards the arguments unchanged.
|
|
85
|
+
_RETIRED_ARGUMENT_ADAPTERS: dict[str, Callable[[dict[str, Any]], dict[str, Any]]] = {
|
|
86
|
+
"ha_get_blueprint": _adapt_get_blueprint,
|
|
87
|
+
"ha_import_blueprint": _adapt_import_blueprint,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def adapt_retired_arguments(
|
|
92
|
+
name: str, arguments: Mapping[str, Any] | None
|
|
93
|
+
) -> dict[str, Any] | None:
|
|
94
|
+
"""Rewrite a retired tool's call arguments for the tool that serves it now.
|
|
95
|
+
|
|
96
|
+
``name`` is the name the caller used. Names without an adapter — current
|
|
97
|
+
names and plain renames — return the arguments untouched (``None`` stays
|
|
98
|
+
``None``). An explicit ``action`` in the arguments is respected.
|
|
99
|
+
"""
|
|
100
|
+
adapter = _RETIRED_ARGUMENT_ADAPTERS.get(name)
|
|
101
|
+
if adapter is None:
|
|
102
|
+
return dict(arguments) if arguments is not None else None
|
|
103
|
+
return adapter(dict(arguments or {}))
|