ha-mcp-dev 8.4.0.dev2483__tar.gz → 8.4.0.dev2485__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.0.dev2483/src/ha_mcp_dev.egg-info → ha_mcp_dev-8.4.0.dev2485}/PKG-INFO +1 -1
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/pyproject.toml +1 -1
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/__init__.py +2 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/errors.py +30 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/device_control.py +84 -24
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/tools_service.py +205 -12
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/util_helpers.py +24 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/LICENSE +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/MANIFEST.in +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/README.md +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/setup.cfg +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_rich_compat.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/__init__.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/requirements.txt +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/LICENSE +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/MANIFEST.sha256 +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/VENDORED +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/__init__.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/__main__.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/asyncio/__init__.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/asyncio/client.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/asyncio/connection.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/asyncio/messages.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/asyncio/router.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/asyncio/server.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/auth.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/cli.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/client.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/connection.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/datastructures.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/exceptions.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/extensions/__init__.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/extensions/base.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/extensions/permessage_deflate.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/frames.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/headers.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/http11.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/imports.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/legacy/__init__.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/legacy/auth.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/legacy/client.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/legacy/exceptions.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/legacy/framing.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/legacy/handshake.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/legacy/http.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/legacy/protocol.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/legacy/server.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/protocol.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/proxy.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/py.typed +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/server.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/streams.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/sync/__init__.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/sync/client.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/sync/connection.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/sync/messages.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/sync/router.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/sync/server.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/sync/utils.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/trio/__init__.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/trio/client.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/trio/connection.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/trio/messages.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/trio/router.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/trio/server.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/trio/utils.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/typing.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/uri.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/utils.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/version.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/auth/oidc_compat.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/backup_manager.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/browser_landing.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/dashboard_screenshot/theme_guard.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/hacs_auto_refresh.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/llm_exposure.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/log_filters.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/policy/__init__.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/policy/approval_queue.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/policy/editing.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/policy/evaluator.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/policy/handlers.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/policy/middleware.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/policy/model.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/policy/persistence.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/policy/value_sources.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/read_only.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/redaction.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/renamed_tools.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/resources/skills-vendor/evals/area-target-over-entity-list/case.yaml +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/resources/skills-vendor/evals/arrive-home-automation/case.yaml +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/resources/skills-vendor/evals/assist-shortcut-card/case.yaml +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/resources/skills-vendor/evals/avg-temperature/case.yaml +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/resources/skills-vendor/evals/backup-deletion-confirmation/case.yaml +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/resources/skills-vendor/evals/blueprint-authoring/case.yaml +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/resources/skills-vendor/evals/button-event-entity/case.yaml +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/resources/skills-vendor/evals/device-id-trigger-migration/case.yaml +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/resources/skills-vendor/evals/helper-flow-fields-not-platform-yaml/case.yaml +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/resources/skills-vendor/evals/media-player-song/case.yaml +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/resources/skills-vendor/evals/motion-light-automation-mode/case.yaml +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/resources/skills-vendor/evals/rename-entity-impact-analysis/case.yaml +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/resources/skills-vendor/evals/template-attributes-yaml-exception/case.yaml +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/resources/skills-vendor/evals/timer-finished-automation/case.yaml +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/resources/skills-vendor/evals/vacuum-last-clean/case.yaml +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/resources/skills-vendor/scripts/check_eval_cases.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/backups.md +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/settings_ui/__init__.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/settings_ui/_i18n.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/settings_ui/_locale_policy.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/settings_ui/_persistence.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/settings_ui/_theme.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/settings_ui/locales/README.md +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/settings_ui/locales/cs.json +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/settings_ui/locales/de.json +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/settings_ui/locales/en.json +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/settings_ui/locales/eo.json +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/settings_ui/locales/es.json +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/settings_ui/locales/fr.json +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/settings_ui/locales/it.json +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/settings_ui/locales/ko.json +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/settings_ui/locales/nl.json +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/settings_ui/locales/pl.json +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/settings_ui/locales/ru.json +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/settings_ui/locales/sv.json +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/settings_ui/locales/tlh.json +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/settings_ui/locales/zh-Hans.json +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/settings_ui/settings.css +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/settings_ui/settings.html +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/settings_ui/settings.js +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/strict_bps.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/auto_backup.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/best_practice_result.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/best_practice_variables.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/bulk_selector.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/component_api.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/component_config_entries.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/component_config_reads.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/component_devices.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/component_registries.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/component_registry_lookup.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/config_entry_flow.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/config_entry_flow_form.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/config_entry_flow_menu.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/config_entry_flow_walker.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/config_entry_identity.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/config_entry_reconfigure.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/config_entry_reload_watch.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/error_log_parsing.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/hacs_registration.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/integration_reconfigure.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/log_common.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/log_sources.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/log_sources_supervisor.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/radio/__init__.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/radio/base.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/radio/matter.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/radio/thread.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/radio/zigbee.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/radio/zwave.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/renamed_tool_middleware.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/smart_search/_base.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/smart_search/_config.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/smart_search/_graph.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/tools_assist_pipeline.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/tools_dev.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/tools_logs.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/tools_radio.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/tools_security_policy.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/tools_themes.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/tools/validation_middleware.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/transport_security.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/update_check.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/utils/config_write_lock.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/utils/entity_membership.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/utils/skill_loader.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/visibility/__init__.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/visibility/enforcement.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/visibility/model.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/visibility/persistence.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/visibility/resolver.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/tests/__init__.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/tests/test_constants.py +0 -0
- {ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/tests/test_env_manager.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ha-mcp-dev"
|
|
7
|
-
version = "8.4.0.
|
|
7
|
+
version = "8.4.0.dev2485"
|
|
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
|
|
@@ -24,6 +24,7 @@ from .errors import (
|
|
|
24
24
|
create_config_error,
|
|
25
25
|
create_connection_error,
|
|
26
26
|
create_entity_not_found_error,
|
|
27
|
+
create_entity_unavailable_after_dispatch_error,
|
|
27
28
|
create_error_response,
|
|
28
29
|
create_service_error,
|
|
29
30
|
create_timeout_error,
|
|
@@ -45,6 +46,7 @@ __all__ = [
|
|
|
45
46
|
"create_connection_error",
|
|
46
47
|
"create_auth_error",
|
|
47
48
|
"create_entity_not_found_error",
|
|
49
|
+
"create_entity_unavailable_after_dispatch_error",
|
|
48
50
|
"create_service_error",
|
|
49
51
|
"create_validation_error",
|
|
50
52
|
"create_config_error",
|
|
@@ -405,6 +405,36 @@ def create_entity_not_found_error(
|
|
|
405
405
|
)
|
|
406
406
|
|
|
407
407
|
|
|
408
|
+
def create_entity_unavailable_after_dispatch_error(entity_id: str) -> dict[str, Any]:
|
|
409
|
+
"""Create an ENTITY_UNAVAILABLE error for a target whose command already dispatched.
|
|
410
|
+
|
|
411
|
+
Distinct from a bare "unavailable" report: by the time this fires, the
|
|
412
|
+
confirmation wait has already lapsed, which only happens AFTER the
|
|
413
|
+
service call reached Home Assistant. The caller must not read this as
|
|
414
|
+
"the command never went out" and blindly retry a non-idempotent service
|
|
415
|
+
(e.g. ``toggle``) — that would double-apply a write that may have already
|
|
416
|
+
landed. ``context["dispatched"]`` makes that explicit for a caller that
|
|
417
|
+
only inspects structured fields rather than the message.
|
|
418
|
+
"""
|
|
419
|
+
return create_error_response(
|
|
420
|
+
ErrorCode.ENTITY_UNAVAILABLE,
|
|
421
|
+
f"Entity '{entity_id}' exists but is currently unavailable",
|
|
422
|
+
details=(
|
|
423
|
+
f"The service call for '{entity_id}' was already dispatched to Home "
|
|
424
|
+
"Assistant -- it may have taken effect even though the entity's "
|
|
425
|
+
"state could not be confirmed within the timeout."
|
|
426
|
+
),
|
|
427
|
+
suggestions=[
|
|
428
|
+
"The command was already sent -- check the entity's current state "
|
|
429
|
+
+ "with ha_get_state before retrying",
|
|
430
|
+
"Avoid re-issuing a non-idempotent service (e.g. toggle) that could "
|
|
431
|
+
+ "double-apply if the original command did land",
|
|
432
|
+
"Check if the device is powered on and connected",
|
|
433
|
+
],
|
|
434
|
+
context={"entity_id": entity_id, "dispatched": True},
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
|
|
408
438
|
def create_service_error(
|
|
409
439
|
domain: str,
|
|
410
440
|
service: str,
|
|
@@ -23,7 +23,11 @@ from ..client.rest_client import (
|
|
|
23
23
|
from ..client.websocket_client import get_websocket_client
|
|
24
24
|
from ..client.websocket_listener import start_websocket_listener
|
|
25
25
|
from ..config import get_global_settings
|
|
26
|
-
from ..errors import
|
|
26
|
+
from ..errors import (
|
|
27
|
+
ErrorCode,
|
|
28
|
+
create_entity_unavailable_after_dispatch_error,
|
|
29
|
+
create_error_response,
|
|
30
|
+
)
|
|
27
31
|
from ..utils.domain_handlers import get_domain_handler
|
|
28
32
|
from ..utils.operation_manager import (
|
|
29
33
|
fail_pending_operation,
|
|
@@ -43,7 +47,7 @@ from .helpers import (
|
|
|
43
47
|
safe_progress,
|
|
44
48
|
)
|
|
45
49
|
from .tools_service import BulkControlOperation
|
|
46
|
-
from .util_helpers import _SERVICE_TO_STATE
|
|
50
|
+
from .util_helpers import _SERVICE_TO_STATE, is_single_entity_target
|
|
47
51
|
|
|
48
52
|
logger = logging.getLogger(__name__)
|
|
49
53
|
|
|
@@ -1016,9 +1020,18 @@ class DeviceControlTools:
|
|
|
1016
1020
|
by ``_build_bulk_response`` exactly as the legacy dispatch result is; an op
|
|
1017
1021
|
whose ``async_call`` raised under the batch (``error`` set / not dispatched)
|
|
1018
1022
|
maps to a structured ``SERVICE_CALL_FAILED`` error and is NOT re-dispatched
|
|
1019
|
-
(D9). When ``validate_first`` (the default) is set and
|
|
1020
|
-
|
|
1021
|
-
|
|
1023
|
+
(D9). When ``validate_first`` (the default) is set and ``entity_id`` is a
|
|
1024
|
+
single literal entity ID (see ``is_single_entity_target`` — a comma-joined
|
|
1025
|
+
multi-target or magic broadcast target like ``"all"`` is skipped entirely,
|
|
1026
|
+
since its transition row proves nothing about whether the possibly-
|
|
1027
|
+
successful dispatch landed), a captured pre-state that is null (the entity
|
|
1028
|
+
does not exist) maps to a structured ``ENTITY_NOT_FOUND`` failure — parity
|
|
1029
|
+
with the legacy per-op validation. A captured pre-state of ``"unavailable"``
|
|
1030
|
+
maps to a distinct ``ENTITY_UNAVAILABLE`` failure, but only once BOTH the op
|
|
1031
|
+
is unconfirmed AND the post-dispatch state still shows unavailable — unlike
|
|
1032
|
+
the not-found branch, an unavailable target can legitimately reconnect and
|
|
1033
|
+
confirm mid-dispatch, so this check is doubly gated rather than symmetric
|
|
1034
|
+
with it.
|
|
1022
1035
|
"""
|
|
1023
1036
|
service_call = {
|
|
1024
1037
|
"domain": row["domain"],
|
|
@@ -1039,31 +1052,78 @@ class DeviceControlTools:
|
|
|
1039
1052
|
# exist (HA no-ops the dispatch, then the wait stalls to partial). The legacy
|
|
1040
1053
|
# path returns a structured ENTITY_NOT_FOUND per-op failure — match it from
|
|
1041
1054
|
# the pre-state already in the transition (no extra hops), rather than
|
|
1042
|
-
# counting a phantom entity as a successful command.
|
|
1043
|
-
#
|
|
1044
|
-
#
|
|
1045
|
-
#
|
|
1046
|
-
|
|
1047
|
-
|
|
1055
|
+
# counting a phantom entity as a successful command. I2 (length-drift)
|
|
1056
|
+
# guards the BATCH-level op-count, not this: an individual op can still
|
|
1057
|
+
# legitimately carry an EMPTY transitions list (dispatched, but post-
|
|
1058
|
+
# dispatch formatting raised — see _dispatched_unconfirmed_bulk_result) at
|
|
1059
|
+
# the correct array length, and that proves nothing about existence. So
|
|
1060
|
+
# look for a transition row at all first; only a present row with a null
|
|
1061
|
+
# old_state means a genuinely absent entity.
|
|
1062
|
+
if (
|
|
1063
|
+
op.get("validate_first", True)
|
|
1064
|
+
and row.get("entity_ids")
|
|
1065
|
+
and is_single_entity_target(entity_id)
|
|
1066
|
+
):
|
|
1067
|
+
transition = next(
|
|
1048
1068
|
(
|
|
1049
|
-
t
|
|
1069
|
+
t
|
|
1050
1070
|
for t in transitions
|
|
1051
1071
|
if isinstance(t, dict) and t.get("entity_id") == entity_id
|
|
1052
1072
|
),
|
|
1053
1073
|
None,
|
|
1054
1074
|
)
|
|
1055
|
-
if
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
"
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1075
|
+
if transition is not None:
|
|
1076
|
+
old_state = transition.get("old_state")
|
|
1077
|
+
if old_state is None:
|
|
1078
|
+
err = create_error_response(
|
|
1079
|
+
ErrorCode.ENTITY_NOT_FOUND,
|
|
1080
|
+
f"Entity not found: {entity_id}",
|
|
1081
|
+
suggestions=[
|
|
1082
|
+
"Use ha_search to find the correct entity",
|
|
1083
|
+
"Check the entity is not disabled in Home Assistant",
|
|
1084
|
+
],
|
|
1085
|
+
context={"entity_id": entity_id, "action": action},
|
|
1086
|
+
)
|
|
1087
|
+
err["service_call"] = service_call
|
|
1088
|
+
return err
|
|
1089
|
+
if (
|
|
1090
|
+
isinstance(old_state, dict)
|
|
1091
|
+
and old_state.get("state") == "unavailable"
|
|
1092
|
+
):
|
|
1093
|
+
new_state = transition.get("new_state")
|
|
1094
|
+
if new_state is None and not component_op.get("confirmed"):
|
|
1095
|
+
# Removed during the dispatch -- a more specific outcome
|
|
1096
|
+
# than "still unavailable", which would falsely assert it
|
|
1097
|
+
# still exists.
|
|
1098
|
+
err = create_error_response(
|
|
1099
|
+
ErrorCode.ENTITY_NOT_FOUND,
|
|
1100
|
+
f"Entity not found: {entity_id}",
|
|
1101
|
+
suggestions=[
|
|
1102
|
+
"Use ha_search to find the correct entity",
|
|
1103
|
+
"Check the entity is not disabled in Home Assistant",
|
|
1104
|
+
],
|
|
1105
|
+
context={"entity_id": entity_id, "action": action},
|
|
1106
|
+
)
|
|
1107
|
+
err["service_call"] = service_call
|
|
1108
|
+
return err
|
|
1109
|
+
if (
|
|
1110
|
+
not component_op.get("confirmed")
|
|
1111
|
+
and isinstance(new_state, dict)
|
|
1112
|
+
and new_state.get("state") == "unavailable"
|
|
1113
|
+
):
|
|
1114
|
+
# Gated on NOT confirmed AND still unavailable per the
|
|
1115
|
+
# best-available post-dispatch read: unlike a nonexistent
|
|
1116
|
+
# entity, an "unavailable" one legitimately stays in the
|
|
1117
|
+
# component's confirmation wait and can reconnect and
|
|
1118
|
+
# transition mid-dispatch (see _confirmable_entity_ids) —
|
|
1119
|
+
# a confirmed op, or one whose new_state shows it
|
|
1120
|
+
# reconnected to some OTHER state (just not the expected
|
|
1121
|
+
# hint), must never be reported as this failure just
|
|
1122
|
+
# because its PRE-state was unavailable.
|
|
1123
|
+
err = create_entity_unavailable_after_dispatch_error(entity_id)
|
|
1124
|
+
err["action"] = action
|
|
1125
|
+
err["service_call"] = service_call
|
|
1126
|
+
return err
|
|
1067
1127
|
final_state = next(
|
|
1068
1128
|
(
|
|
1069
1129
|
t["new_state"].get("state")
|
|
@@ -14,6 +14,7 @@ from fastmcp.tools import tool
|
|
|
14
14
|
from pydantic import ConfigDict, Field, SkipValidation, TypeAdapter, ValidationError
|
|
15
15
|
|
|
16
16
|
from ..client.rest_client import (
|
|
17
|
+
HomeAssistantAPIError,
|
|
17
18
|
HomeAssistantClient,
|
|
18
19
|
HomeAssistantCommandError,
|
|
19
20
|
HomeAssistantCommandNotSent,
|
|
@@ -23,6 +24,8 @@ from ..client.websocket_client import get_websocket_client
|
|
|
23
24
|
from ..errors import (
|
|
24
25
|
ErrorCode,
|
|
25
26
|
create_connection_error,
|
|
27
|
+
create_entity_not_found_error,
|
|
28
|
+
create_entity_unavailable_after_dispatch_error,
|
|
26
29
|
create_error_response,
|
|
27
30
|
create_validation_error,
|
|
28
31
|
)
|
|
@@ -53,6 +56,7 @@ from .util_helpers import (
|
|
|
53
56
|
BLOCKED_WS_WRITE_COMMANDS,
|
|
54
57
|
JSON_STRING_COERCION,
|
|
55
58
|
compact_service_result,
|
|
59
|
+
is_single_entity_target,
|
|
56
60
|
parse_json_param,
|
|
57
61
|
parse_string_list_param,
|
|
58
62
|
project_entity_record,
|
|
@@ -924,8 +928,8 @@ class ServiceTools:
|
|
|
924
928
|
response["warnings"].append(_COMPONENT_RESPONSE_UNCONFIRMED_WARNING)
|
|
925
929
|
return response
|
|
926
930
|
|
|
927
|
-
async def
|
|
928
|
-
"""
|
|
931
|
+
async def _validate_entity_before_wait(self, entity_id: str | None) -> str | None:
|
|
932
|
+
"""Fetch entity_id's pre-call state, failing fast on a settled bad target.
|
|
929
933
|
|
|
930
934
|
``entity_id`` stays optional in the signature to match the caller's
|
|
931
935
|
own ``str | None`` (a service call may target zero entities); the
|
|
@@ -936,18 +940,54 @@ class ServiceTools:
|
|
|
936
940
|
not in a form the type checker can see through. Narrowing here
|
|
937
941
|
instead of trusting the caller keeps ``get_entity_state`` (which
|
|
938
942
|
genuinely requires a ``str``) honestly typed.
|
|
943
|
+
|
|
944
|
+
A clean 404 (the entity does not exist) settles the question with
|
|
945
|
+
certainty before any service dispatch or 10s state-change wait, so
|
|
946
|
+
raise immediately instead of silently degrading into a wait that can
|
|
947
|
+
never confirm. Any other fetch failure is NOT conclusive -- it
|
|
948
|
+
returns ``None`` and lets the call proceed as before, since the
|
|
949
|
+
entity may well exist and this could be a transient blip.
|
|
950
|
+
|
|
951
|
+
Deliberately NOT fast-failed here: a fetched ``"unavailable"``
|
|
952
|
+
state. Unlike a 404, it does not prove the target can never respond
|
|
953
|
+
-- dispatching the service is exactly what can reconnect it (e.g. a
|
|
954
|
+
device that wakes on the very command being sent), and failing here
|
|
955
|
+
would mean that command never reaches Home Assistant at all. The
|
|
956
|
+
entity's initial state is still returned so ``_verify_state_change``
|
|
957
|
+
can classify a genuine confirmation lapse as ENTITY_UNAVAILABLE only
|
|
958
|
+
if a live re-check shows it is STILL unavailable afterward -- mirrors
|
|
959
|
+
the component path's post-dispatch-state check.
|
|
960
|
+
|
|
961
|
+
A comma-separated ``entity_id`` ("light.a,light.b") or a Home
|
|
962
|
+
Assistant magic broadcast target (``entity_id="all"`` /
|
|
963
|
+
``ENTITY_MATCH_ALL``, or ``"none"``) is a valid target for the
|
|
964
|
+
service-call payload, but the single-entity ``/api/states/<id>``
|
|
965
|
+
endpoint has no way to resolve either -- a 404 there proves nothing
|
|
966
|
+
about whether the underlying call can succeed, so the fast-fail is
|
|
967
|
+
skipped entirely for one (see ``is_single_entity_target``; matches
|
|
968
|
+
the component route's own comma exclusion in
|
|
969
|
+
``_maybe_component_call_service``).
|
|
939
970
|
"""
|
|
940
971
|
if entity_id is None:
|
|
941
972
|
return None
|
|
973
|
+
is_single_target = is_single_entity_target(entity_id)
|
|
942
974
|
try:
|
|
943
975
|
state_data = await self._client.get_entity_state(entity_id)
|
|
944
|
-
|
|
976
|
+
except HomeAssistantAPIError as e:
|
|
977
|
+
if is_single_target and e.status_code == 404:
|
|
978
|
+
raise_tool_error(create_entity_not_found_error(entity_id))
|
|
979
|
+
logger.debug(
|
|
980
|
+
f"Could not fetch initial state for {entity_id}: {e} — state verification may be degraded"
|
|
981
|
+
)
|
|
982
|
+
return None
|
|
945
983
|
except Exception as e:
|
|
946
984
|
logger.debug(
|
|
947
985
|
f"Could not fetch initial state for {entity_id}: {e} — state verification may be degraded"
|
|
948
986
|
)
|
|
949
987
|
return None
|
|
950
988
|
|
|
989
|
+
return state_data.get("state") if state_data else None
|
|
990
|
+
|
|
951
991
|
async def _verify_state_change(
|
|
952
992
|
self,
|
|
953
993
|
entity_id: str,
|
|
@@ -955,7 +995,17 @@ class ServiceTools:
|
|
|
955
995
|
initial_state: str | None,
|
|
956
996
|
response: dict[str, Any],
|
|
957
997
|
) -> None:
|
|
958
|
-
"""Wait for and verify entity state change after a service call, updating response in place.
|
|
998
|
+
"""Wait for and verify entity state change after a service call, updating response in place.
|
|
999
|
+
|
|
1000
|
+
An entity that was ``"unavailable"`` at dispatch time was NOT failed
|
|
1001
|
+
fast (see ``_validate_entity_before_wait``) -- it may have reconnected
|
|
1002
|
+
once the command actually reached it. Only once the wait genuinely
|
|
1003
|
+
lapses does a fresh live re-check decide whether it's still
|
|
1004
|
+
unavailable (report the precise ``ENTITY_UNAVAILABLE``) or reconnected
|
|
1005
|
+
to some state other than the hint (leave the existing generic
|
|
1006
|
+
partial/timeout wording) -- mirrors the component path's
|
|
1007
|
+
post-dispatch-state check.
|
|
1008
|
+
"""
|
|
959
1009
|
try:
|
|
960
1010
|
expected = _SERVICE_TO_STATE.get(service)
|
|
961
1011
|
new_state = await wait_for_state_change(
|
|
@@ -967,15 +1017,58 @@ class ServiceTools:
|
|
|
967
1017
|
)
|
|
968
1018
|
if new_state:
|
|
969
1019
|
response["verified_state"] = new_state.get("state")
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1020
|
+
return
|
|
1021
|
+
if initial_state == "unavailable" and await self._still_unavailable(
|
|
1022
|
+
entity_id
|
|
1023
|
+
):
|
|
1024
|
+
raise_tool_error(
|
|
1025
|
+
create_entity_unavailable_after_dispatch_error(entity_id)
|
|
973
1026
|
)
|
|
1027
|
+
response.setdefault("warnings", []).append(
|
|
1028
|
+
"Service executed but state change could not be verified within timeout."
|
|
1029
|
+
)
|
|
1030
|
+
except ToolError:
|
|
1031
|
+
raise
|
|
974
1032
|
except Exception as e:
|
|
975
1033
|
response.setdefault("warnings", []).append(
|
|
976
1034
|
f"Service executed but state verification failed: {e}"
|
|
977
1035
|
)
|
|
978
1036
|
|
|
1037
|
+
async def _still_unavailable(self, entity_id: str) -> bool:
|
|
1038
|
+
"""Live re-check: True only when a successful fetch explicitly shows
|
|
1039
|
+
``"unavailable"``.
|
|
1040
|
+
|
|
1041
|
+
A 404 here means the entity was REMOVED since the initial fetch — a
|
|
1042
|
+
more specific outcome than "still unavailable", so it raises
|
|
1043
|
+
ENTITY_NOT_FOUND directly rather than being folded into it. Any other
|
|
1044
|
+
fetch failure (network blip, an expired token surfacing as
|
|
1045
|
+
``HomeAssistantAuthError`` -- a sibling of ``HomeAssistantAPIError``,
|
|
1046
|
+
not a subclass, so it lands in the generic branch below) or a
|
|
1047
|
+
response with no usable ``state`` is inconclusive, not proof either
|
|
1048
|
+
way, so it returns ``False`` and lets the caller fall through to the
|
|
1049
|
+
generic verification-failed wording instead of confidently asserting
|
|
1050
|
+
an unproven "still unavailable". Logged at WARNING (matching the
|
|
1051
|
+
sibling re-check in ``_validate_entity_before_wait`` and
|
|
1052
|
+
``wait_for_state_change`` in util_helpers.py) so an operational
|
|
1053
|
+
failure here -- an expired token, in particular -- leaves a
|
|
1054
|
+
server-side trace instead of silently reading as a generic timeout.
|
|
1055
|
+
"""
|
|
1056
|
+
try:
|
|
1057
|
+
current = await self._client.get_entity_state(entity_id)
|
|
1058
|
+
except HomeAssistantAPIError as e:
|
|
1059
|
+
if e.status_code == 404:
|
|
1060
|
+
raise_tool_error(create_entity_not_found_error(entity_id))
|
|
1061
|
+
logger.warning(
|
|
1062
|
+
f"Post-timeout unavailable re-check for {entity_id} failed: {e} — treating as inconclusive"
|
|
1063
|
+
)
|
|
1064
|
+
return False
|
|
1065
|
+
except Exception as e:
|
|
1066
|
+
logger.warning(
|
|
1067
|
+
f"Post-timeout unavailable re-check for {entity_id} failed: {e} — treating as inconclusive"
|
|
1068
|
+
)
|
|
1069
|
+
return False
|
|
1070
|
+
return isinstance(current, dict) and current.get("state") == "unavailable"
|
|
1071
|
+
|
|
979
1072
|
@staticmethod
|
|
980
1073
|
def _split_return_response_envelope(
|
|
981
1074
|
result: Any, *, return_response: bool
|
|
@@ -1465,6 +1558,81 @@ class ServiceTools:
|
|
|
1465
1558
|
return new_state.get("state")
|
|
1466
1559
|
return None
|
|
1467
1560
|
|
|
1561
|
+
@staticmethod
|
|
1562
|
+
def _raise_for_missing_or_unavailable_target(
|
|
1563
|
+
transitions: list[Any], entity_id: str
|
|
1564
|
+
) -> None:
|
|
1565
|
+
"""Report why a confirmation never landed, once it's certain it never will.
|
|
1566
|
+
|
|
1567
|
+
Only reached when the component result was NOT confirmed. No-op
|
|
1568
|
+
(returns) unless ``entity_id`` is a single literal entity ID (see
|
|
1569
|
+
``is_single_entity_target``): a comma-joined multi-target or a Home
|
|
1570
|
+
Assistant magic broadcast target (``"all"`` / ``"none"``) is never a
|
|
1571
|
+
literal entity in the state machine, so its transition row carries a
|
|
1572
|
+
null ``old_state`` unconditionally -- that proves nothing about
|
|
1573
|
+
whether the (possibly successful) broadcast landed, unlike a genuine
|
|
1574
|
+
single-entity miss.
|
|
1575
|
+
|
|
1576
|
+
For a real single-entity target: the transition's ``old_state`` is
|
|
1577
|
+
``None`` for an entity absent from the state machine at pre-dispatch
|
|
1578
|
+
time — the component excludes it from the wait entirely (it
|
|
1579
|
+
structurally cannot ever confirm), so an unconfirmed result with a
|
|
1580
|
+
null ``old_state`` is unambiguous: raise ENTITY_NOT_FOUND immediately.
|
|
1581
|
+
A real state dict whose ``state`` is ``"unavailable"`` stayed IN the
|
|
1582
|
+
component's confirmation wait (unlike a nonexistent target, it can
|
|
1583
|
+
legitimately reconnect and transition mid-dispatch — the caller only
|
|
1584
|
+
reaches this branch after that chance has already lapsed); what
|
|
1585
|
+
happened to it by ``new_state`` (the best-available POST-dispatch
|
|
1586
|
+
read: ``_post_state`` re-reads live if the listener's exact
|
|
1587
|
+
expected-state event never arrived) then decides the outcome:
|
|
1588
|
+
|
|
1589
|
+
* ``new_state`` is ``None`` — the entity was REMOVED during the
|
|
1590
|
+
dispatch, a more specific outcome than "still unavailable" (which
|
|
1591
|
+
would falsely assert it still exists), so raise ENTITY_NOT_FOUND.
|
|
1592
|
+
* ``new_state`` still shows ``"unavailable"`` — raise
|
|
1593
|
+
ENTITY_UNAVAILABLE.
|
|
1594
|
+
* ``new_state`` shows anything else — the target reconnected but
|
|
1595
|
+
settled on some state other than the hint (e.g. a lock that
|
|
1596
|
+
reached "locking", not yet "locked"), demonstrably no longer
|
|
1597
|
+
unavailable even though this op never confirmed. Reporting it as
|
|
1598
|
+
ENTITY_UNAVAILABLE from the stale pre-state alone would discard
|
|
1599
|
+
that real result, so no-op instead.
|
|
1600
|
+
|
|
1601
|
+
No-op also for any other unconfirmed target — a real, available
|
|
1602
|
+
entity whose confirming event simply never arrived, OR no transition
|
|
1603
|
+
row at all (``_dispatched_unconfirmed_result``'s empty
|
|
1604
|
+
``transitions`` after a post-dispatch formatting failure — the write
|
|
1605
|
+
already landed, so an absent row proves nothing about existence) —
|
|
1606
|
+
leaving the caller's existing generic partial/timeout wording for
|
|
1607
|
+
those genuine-ambiguity cases.
|
|
1608
|
+
"""
|
|
1609
|
+
if not is_single_entity_target(entity_id):
|
|
1610
|
+
return
|
|
1611
|
+
transition = next(
|
|
1612
|
+
(
|
|
1613
|
+
t
|
|
1614
|
+
for t in transitions
|
|
1615
|
+
if isinstance(t, dict) and t.get("entity_id") == entity_id
|
|
1616
|
+
),
|
|
1617
|
+
None,
|
|
1618
|
+
)
|
|
1619
|
+
if transition is None:
|
|
1620
|
+
return
|
|
1621
|
+
old_state = transition.get("old_state")
|
|
1622
|
+
if old_state is None:
|
|
1623
|
+
raise_tool_error(create_entity_not_found_error(entity_id))
|
|
1624
|
+
if not (
|
|
1625
|
+
isinstance(old_state, dict) and old_state.get("state") == "unavailable"
|
|
1626
|
+
):
|
|
1627
|
+
return
|
|
1628
|
+
new_state = transition.get("new_state")
|
|
1629
|
+
if new_state is None:
|
|
1630
|
+
# Removed during the dispatch -- a more specific outcome than
|
|
1631
|
+
# "still unavailable", which would falsely assert it still exists.
|
|
1632
|
+
raise_tool_error(create_entity_not_found_error(entity_id))
|
|
1633
|
+
if isinstance(new_state, dict) and new_state.get("state") == "unavailable":
|
|
1634
|
+
raise_tool_error(create_entity_unavailable_after_dispatch_error(entity_id))
|
|
1635
|
+
|
|
1468
1636
|
def _build_component_call_response(
|
|
1469
1637
|
self,
|
|
1470
1638
|
component_result: dict[str, Any],
|
|
@@ -1492,6 +1660,21 @@ class ServiceTools:
|
|
|
1492
1660
|
timeout path produces (``_build_timeout_response``).
|
|
1493
1661
|
"""
|
|
1494
1662
|
transitions = component_result.get("transitions") or []
|
|
1663
|
+
# A nonexistent target's pre-dispatch read excludes it from the wait
|
|
1664
|
+
# entirely, so it can never reach ``confirmed``; an "unavailable" one
|
|
1665
|
+
# stays eligible and CAN reach it (a genuine reconnect-and-confirm) —
|
|
1666
|
+
# only an UNCONFIRMED result is checked here, before building a generic
|
|
1667
|
+
# partial/timeout response, using the same ``old_state``/``new_state``
|
|
1668
|
+
# signal ``device_control.py``'s bulk-op mapping already reads from
|
|
1669
|
+
# this identical transitions shape. A genuine confirmation lapse on a
|
|
1670
|
+
# real, available target (old_state present, not "unavailable") falls
|
|
1671
|
+
# through unchanged to the existing partial-success wording below.
|
|
1672
|
+
if (
|
|
1673
|
+
should_wait
|
|
1674
|
+
and entity_id is not None
|
|
1675
|
+
and not component_result.get("confirmed")
|
|
1676
|
+
):
|
|
1677
|
+
self._raise_for_missing_or_unavailable_target(transitions, entity_id)
|
|
1495
1678
|
# The transition new_states are State.as_dict() records — the same shape the
|
|
1496
1679
|
# legacy REST POST returns — so the existing projection helpers apply
|
|
1497
1680
|
# unchanged (compact filters to the target, drops metadata / heavy lists).
|
|
@@ -1538,9 +1721,13 @@ class ServiceTools:
|
|
|
1538
1721
|
)
|
|
1539
1722
|
if should_wait:
|
|
1540
1723
|
if component_result.get("partial"):
|
|
1541
|
-
#
|
|
1542
|
-
#
|
|
1543
|
-
#
|
|
1724
|
+
# Reached only when _raise_for_missing_or_unavailable_target above
|
|
1725
|
+
# did NOT raise -- i.e. a genuine confirmation lapse on a real,
|
|
1726
|
+
# available target whose confirming state_changed simply never
|
|
1727
|
+
# arrived within the wait. success stays True here (the same
|
|
1728
|
+
# partial-success contract the legacy timeout path reports);
|
|
1729
|
+
# unlike this generic case, a target proven nonexistent or still
|
|
1730
|
+
# unavailable is a raised ToolError, not a partial success.
|
|
1544
1731
|
response["partial"] = True
|
|
1545
1732
|
response.setdefault("warnings", []).append(
|
|
1546
1733
|
"Service executed but state change could not be verified "
|
|
@@ -1819,10 +2006,16 @@ class ServiceTools:
|
|
|
1819
2006
|
return component_response
|
|
1820
2007
|
|
|
1821
2008
|
# Legacy REST path (component absent, or it never dispatched): capture
|
|
1822
|
-
# initial state before the call for the WS-subscribe verification.
|
|
2009
|
+
# initial state before the call for the WS-subscribe verification. A
|
|
2010
|
+
# settled 404 (single-entity target definitely does not exist) raises
|
|
2011
|
+
# here, BEFORE the dispatch below, so a known-invalid call never
|
|
2012
|
+
# reaches the service POST or the 10s confirmation wait. An
|
|
2013
|
+
# "unavailable" target is NOT fast-failed here -- dispatching is
|
|
2014
|
+
# exactly what might reconnect it; see _verify_state_change for its
|
|
2015
|
+
# post-dispatch classification instead.
|
|
1823
2016
|
initial_state = None
|
|
1824
2017
|
if should_wait:
|
|
1825
|
-
initial_state = await self.
|
|
2018
|
+
initial_state = await self._validate_entity_before_wait(entity_id)
|
|
1826
2019
|
|
|
1827
2020
|
result = await self._client.call_service(
|
|
1828
2021
|
domain, service, service_data, return_response=return_response_bool
|
|
@@ -53,6 +53,30 @@ _SERVICE_TO_STATE: dict[str, str] = {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
|
|
56
|
+
def is_single_entity_target(entity_id: str | None) -> bool:
|
|
57
|
+
"""True when ``entity_id`` looks like exactly one real, literal entity ID.
|
|
58
|
+
|
|
59
|
+
False for two shapes a not-found/unavailable lookup miss cannot settle
|
|
60
|
+
anything about:
|
|
61
|
+
|
|
62
|
+
* A comma-separated multi-target list ("light.a,light.b") — a valid
|
|
63
|
+
service-call payload the single-entity ``/api/states/<id>`` endpoint
|
|
64
|
+
has no syntax for, so its 404 on the literal joined string proves
|
|
65
|
+
nothing about the individual targets.
|
|
66
|
+
* A Home Assistant magic broadcast target — ``ENTITY_MATCH_ALL`` ("all")
|
|
67
|
+
or ``ENTITY_MATCH_NONE`` ("none"), both defined in
|
|
68
|
+
``homeassistant/const.py`` and neither a literal entity in the state
|
|
69
|
+
machine, so a states-endpoint miss for either is expected and proves
|
|
70
|
+
nothing about whether the underlying service call can succeed.
|
|
71
|
+
|
|
72
|
+
A normal entity ID always has a ``domain.object_id`` shape (contains a
|
|
73
|
+
dot); the absence of one is the general, defensive signal used here
|
|
74
|
+
rather than hardcoding just "all"/"none" by name, so any other future
|
|
75
|
+
magic target is covered the same way.
|
|
76
|
+
"""
|
|
77
|
+
return entity_id is not None and "," not in entity_id and "." in entity_id
|
|
78
|
+
|
|
79
|
+
|
|
56
80
|
def websocket_error_message(error: Any) -> str:
|
|
57
81
|
"""Extract a readable message from a Home Assistant websocket error."""
|
|
58
82
|
if isinstance(error, dict):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/MANIFEST.sha256
RENAMED
|
File without changes
|
{ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/VENDORED
RENAMED
|
File without changes
|
{ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/__init__.py
RENAMED
|
File without changes
|
{ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/__main__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/client.py
RENAMED
|
File without changes
|
{ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/connection.py
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/exceptions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/frames.py
RENAMED
|
File without changes
|
{ha_mcp_dev-8.4.0.dev2483 → ha_mcp_dev-8.4.0.dev2485}/src/ha_mcp/_vendor/websockets/headers.py
RENAMED
|
File without changes
|