ha-mcp-dev 8.3.0.dev2374__tar.gz → 8.3.0.dev2379__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.3.0.dev2374/src/ha_mcp_dev.egg-info → ha_mcp_dev-8.3.0.dev2379}/PKG-INFO +1 -1
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/pyproject.toml +1 -1
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/device_control.py +168 -13
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/smart_search/_base.py +8 -1
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/smart_search/_entities.py +220 -40
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_search.py +15 -3
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_service.py +109 -8
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/utils/operation_manager.py +3 -3
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/visibility/enforcement.py +22 -1
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/LICENSE +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/MANIFEST.in +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/README.md +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/setup.cfg +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_rich_compat.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/requirements.txt +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/LICENSE +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/MANIFEST.sha256 +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/VENDORED +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/__main__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/asyncio/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/asyncio/client.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/asyncio/connection.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/asyncio/messages.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/asyncio/router.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/asyncio/server.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/auth.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/cli.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/client.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/connection.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/datastructures.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/exceptions.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/extensions/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/extensions/base.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/extensions/permessage_deflate.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/frames.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/headers.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/http11.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/imports.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/legacy/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/legacy/auth.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/legacy/client.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/legacy/exceptions.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/legacy/framing.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/legacy/handshake.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/legacy/http.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/legacy/protocol.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/legacy/server.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/protocol.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/proxy.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/py.typed +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/server.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/streams.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/sync/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/sync/client.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/sync/connection.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/sync/messages.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/sync/router.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/sync/server.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/sync/utils.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/trio/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/trio/client.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/trio/connection.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/trio/messages.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/trio/router.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/trio/server.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/trio/utils.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/typing.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/uri.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/utils.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/version.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/auth/oidc_compat.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/backup_manager.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/browser_landing.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/dashboard_screenshot/theme_guard.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/hacs_auto_refresh.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/llm_exposure.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/policy/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/policy/approval_queue.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/policy/editing.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/policy/evaluator.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/policy/handlers.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/policy/middleware.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/policy/model.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/policy/persistence.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/policy/value_sources.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/read_only.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/redaction.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/renamed_tools.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/_i18n.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/_persistence.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/_theme.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/locales/README.md +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/locales/cs.json +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/locales/de.json +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/locales/en.json +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/locales/eo.json +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/locales/es.json +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/locales/fr.json +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/locales/it.json +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/locales/nl.json +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/locales/pl.json +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/locales/ru.json +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/locales/sv.json +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/locales/zh-Hans.json +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/settings.css +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/settings.html +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/settings.js +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/strict_bps.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/auto_backup.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/best_practice_result.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/best_practice_variables.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/component_api.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/component_config_entries.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/component_config_reads.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/component_devices.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/component_registries.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/component_registry_lookup.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/config_entry_flow.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/config_entry_flow_form.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/config_entry_flow_menu.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/config_entry_flow_walker.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/config_entry_identity.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/config_entry_reconfigure.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/config_entry_reload_watch.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/error_log_parsing.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/hacs_registration.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/integration_reconfigure.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/radio/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/radio/base.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/radio/matter.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/radio/thread.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/radio/zigbee.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/radio/zwave.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/renamed_tool_middleware.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/smart_search/_config.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_assist_pipeline.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_dev.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_radio.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_security_policy.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_themes.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/validation_middleware.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/transport_security.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/update_check.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/utils/config_write_lock.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/utils/skill_loader.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/visibility/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/visibility/model.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/visibility/persistence.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/visibility/resolver.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/tests/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/tests/test_constants.py +0 -0
- {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/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.3.0.
|
|
7
|
+
version = "8.3.0.dev2379"
|
|
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
|
|
@@ -8,6 +8,7 @@ and async operation verification through WebSocket monitoring.
|
|
|
8
8
|
import asyncio
|
|
9
9
|
import json
|
|
10
10
|
import logging
|
|
11
|
+
import math
|
|
11
12
|
import time
|
|
12
13
|
from typing import Any, ClassVar
|
|
13
14
|
|
|
@@ -88,7 +89,7 @@ class DeviceControlTools:
|
|
|
88
89
|
entity_id: str,
|
|
89
90
|
action: str,
|
|
90
91
|
parameters: dict[str, Any] | None = None,
|
|
91
|
-
timeout_seconds:
|
|
92
|
+
timeout_seconds: float = 10,
|
|
92
93
|
validate_first: bool = True,
|
|
93
94
|
) -> dict[str, Any]:
|
|
94
95
|
"""
|
|
@@ -347,7 +348,13 @@ class DeviceControlTools:
|
|
|
347
348
|
}
|
|
348
349
|
|
|
349
350
|
_DOMAIN_PARAMS: ClassVar[dict[str, list[str]]] = {
|
|
350
|
-
"light": [
|
|
351
|
+
"light": [
|
|
352
|
+
"brightness",
|
|
353
|
+
"brightness_pct",
|
|
354
|
+
"color_temp_kelvin",
|
|
355
|
+
"rgb_color",
|
|
356
|
+
"effect",
|
|
357
|
+
],
|
|
351
358
|
"climate": ["temperature", "target_temp_high", "target_temp_low", "hvac_mode"],
|
|
352
359
|
"cover": ["position", "tilt_position"],
|
|
353
360
|
"media_player": ["volume_level", "media_content_id", "media_content_type"],
|
|
@@ -462,7 +469,7 @@ class DeviceControlTools:
|
|
|
462
469
|
return expected if expected else None
|
|
463
470
|
|
|
464
471
|
async def get_device_operation_status(
|
|
465
|
-
self, operation_id: str, timeout_seconds:
|
|
472
|
+
self, operation_id: str, timeout_seconds: float = 10
|
|
466
473
|
) -> dict[str, Any]:
|
|
467
474
|
"""Check status of a device operation, waiting up to ``timeout_seconds`` for completion.
|
|
468
475
|
|
|
@@ -495,9 +502,10 @@ class DeviceControlTools:
|
|
|
495
502
|
if operation.status.value == "pending" and timeout_seconds > 0:
|
|
496
503
|
deadline = time.monotonic() + timeout_seconds
|
|
497
504
|
while operation.status.value == "pending":
|
|
498
|
-
|
|
505
|
+
remaining = deadline - time.monotonic()
|
|
506
|
+
if remaining <= 0:
|
|
499
507
|
break
|
|
500
|
-
await asyncio.sleep(0.2)
|
|
508
|
+
await asyncio.sleep(min(0.2, remaining))
|
|
501
509
|
refreshed = get_operation_from_memory(operation_id)
|
|
502
510
|
if refreshed is None:
|
|
503
511
|
raise_tool_error(
|
|
@@ -597,8 +605,52 @@ class DeviceControlTools:
|
|
|
597
605
|
}
|
|
598
606
|
|
|
599
607
|
@staticmethod
|
|
608
|
+
def _normalize_bulk_parameters(
|
|
609
|
+
value: Any,
|
|
610
|
+
) -> tuple[dict[str, Any] | None, ErrorCode | None]:
|
|
611
|
+
"""Normalize optional bulk parameters and identify invalid values."""
|
|
612
|
+
if isinstance(value, str):
|
|
613
|
+
try:
|
|
614
|
+
value = json.loads(value)
|
|
615
|
+
except json.JSONDecodeError:
|
|
616
|
+
return None, ErrorCode.VALIDATION_INVALID_JSON
|
|
617
|
+
if value is not None and not isinstance(value, dict):
|
|
618
|
+
return None, ErrorCode.VALIDATION_INVALID_PARAMETER
|
|
619
|
+
return value, None
|
|
620
|
+
|
|
621
|
+
@staticmethod
|
|
622
|
+
def _normalize_bulk_timeout(value: Any) -> tuple[float | None, bool]:
|
|
623
|
+
"""Normalize an optional non-negative finite timeout."""
|
|
624
|
+
if value is None:
|
|
625
|
+
return None, True
|
|
626
|
+
try:
|
|
627
|
+
timeout = float(value)
|
|
628
|
+
except (TypeError, ValueError):
|
|
629
|
+
return None, False
|
|
630
|
+
return (
|
|
631
|
+
(timeout, True)
|
|
632
|
+
if math.isfinite(timeout) and timeout >= 0
|
|
633
|
+
else (None, False)
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
@staticmethod
|
|
637
|
+
def _normalized_bulk_operation(
|
|
638
|
+
operation: dict[str, Any],
|
|
639
|
+
parameters: dict[str, Any] | None,
|
|
640
|
+
timeout: float | None,
|
|
641
|
+
) -> dict[str, Any]:
|
|
642
|
+
"""Return one normalized valid bulk operation."""
|
|
643
|
+
normalized = dict(operation)
|
|
644
|
+
if parameters is not None:
|
|
645
|
+
normalized["parameters"] = parameters
|
|
646
|
+
if timeout is not None:
|
|
647
|
+
normalized["timeout_seconds"] = timeout
|
|
648
|
+
return normalized
|
|
649
|
+
|
|
650
|
+
@classmethod
|
|
600
651
|
def _validate_bulk_operations(
|
|
601
|
-
|
|
652
|
+
cls,
|
|
653
|
+
operations: list[Any],
|
|
602
654
|
skipped_operations: list[dict[str, Any]],
|
|
603
655
|
) -> list[tuple[int, dict[str, Any], str, str]]:
|
|
604
656
|
valid: list[tuple[int, dict[str, Any], str, str]] = []
|
|
@@ -614,6 +666,33 @@ class DeviceControlTools:
|
|
|
614
666
|
skipped_operations.append(err_response)
|
|
615
667
|
continue
|
|
616
668
|
|
|
669
|
+
allowed_keys = {
|
|
670
|
+
"entity_id",
|
|
671
|
+
"action",
|
|
672
|
+
"parameters",
|
|
673
|
+
"timeout_seconds",
|
|
674
|
+
"validate_first",
|
|
675
|
+
}
|
|
676
|
+
obsolete_keys = sorted(set(op) - allowed_keys)
|
|
677
|
+
if obsolete_keys:
|
|
678
|
+
key_list = ", ".join(obsolete_keys)
|
|
679
|
+
error = (
|
|
680
|
+
f"Operation at index {i} contains unsupported key(s): {key_list}"
|
|
681
|
+
)
|
|
682
|
+
logger.warning(f"Bulk control: {error}")
|
|
683
|
+
err_response = create_error_response(
|
|
684
|
+
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
685
|
+
error,
|
|
686
|
+
suggestions=[
|
|
687
|
+
"Use entity_id and action; put action data inside parameters"
|
|
688
|
+
],
|
|
689
|
+
context={"index": i, "unsupported_keys": obsolete_keys},
|
|
690
|
+
)
|
|
691
|
+
err_response["index"] = i
|
|
692
|
+
err_response["operation"] = op
|
|
693
|
+
skipped_operations.append(err_response)
|
|
694
|
+
continue
|
|
695
|
+
|
|
617
696
|
entity_id = op.get("entity_id")
|
|
618
697
|
action = op.get("action")
|
|
619
698
|
missing = [f for f in ("entity_id", "action") if not op.get(f)]
|
|
@@ -627,13 +706,85 @@ class DeviceControlTools:
|
|
|
627
706
|
err_response["index"] = i
|
|
628
707
|
err_response["operation"] = op
|
|
629
708
|
skipped_operations.append(err_response)
|
|
630
|
-
|
|
631
|
-
|
|
709
|
+
continue
|
|
710
|
+
|
|
711
|
+
if not isinstance(entity_id, str) or not isinstance(action, str):
|
|
712
|
+
error = f"Operation at index {i} requires string entity_id and action fields"
|
|
713
|
+
logger.warning(f"Bulk control: {error}")
|
|
714
|
+
err_response = create_error_response(
|
|
715
|
+
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
716
|
+
error,
|
|
717
|
+
context={"index": i},
|
|
718
|
+
)
|
|
719
|
+
err_response["index"] = i
|
|
720
|
+
err_response["operation"] = op
|
|
721
|
+
skipped_operations.append(err_response)
|
|
722
|
+
continue
|
|
723
|
+
|
|
724
|
+
parameters, parameter_error = cls._normalize_bulk_parameters(
|
|
725
|
+
op.get("parameters")
|
|
726
|
+
)
|
|
727
|
+
if parameter_error is not None:
|
|
728
|
+
error = (
|
|
729
|
+
f"Operation at index {i} has invalid JSON parameters"
|
|
730
|
+
if parameter_error == ErrorCode.VALIDATION_INVALID_JSON
|
|
731
|
+
else f"Operation at index {i} parameters must be a JSON object"
|
|
732
|
+
)
|
|
733
|
+
logger.warning(f"Bulk control: {error}")
|
|
734
|
+
err_response = create_error_response(
|
|
735
|
+
parameter_error,
|
|
736
|
+
error,
|
|
737
|
+
context={"index": i},
|
|
738
|
+
)
|
|
739
|
+
err_response["index"] = i
|
|
740
|
+
err_response["operation"] = op
|
|
741
|
+
skipped_operations.append(err_response)
|
|
742
|
+
continue
|
|
743
|
+
|
|
744
|
+
timeout = None
|
|
745
|
+
timeout_valid = "timeout_seconds" not in op
|
|
746
|
+
if not timeout_valid:
|
|
747
|
+
timeout, timeout_valid = cls._normalize_bulk_timeout(
|
|
748
|
+
op["timeout_seconds"]
|
|
749
|
+
)
|
|
750
|
+
timeout_valid = timeout_valid and timeout is not None
|
|
751
|
+
if not timeout_valid:
|
|
752
|
+
error = (
|
|
753
|
+
f"Operation at index {i} timeout_seconds must be a "
|
|
754
|
+
"non-negative number"
|
|
755
|
+
)
|
|
756
|
+
logger.warning(f"Bulk control: {error}")
|
|
757
|
+
err_response = create_error_response(
|
|
758
|
+
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
759
|
+
error,
|
|
760
|
+
context={"index": i},
|
|
761
|
+
)
|
|
762
|
+
err_response["index"] = i
|
|
763
|
+
err_response["operation"] = op
|
|
764
|
+
skipped_operations.append(err_response)
|
|
765
|
+
continue
|
|
766
|
+
|
|
767
|
+
if "validate_first" in op and type(op["validate_first"]) is not bool:
|
|
768
|
+
error = f"Operation at index {i} validate_first must be a boolean"
|
|
769
|
+
logger.warning(f"Bulk control: {error}")
|
|
770
|
+
err_response = create_error_response(
|
|
771
|
+
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
772
|
+
error,
|
|
773
|
+
context={"index": i},
|
|
774
|
+
)
|
|
775
|
+
err_response["index"] = i
|
|
776
|
+
err_response["operation"] = op
|
|
777
|
+
skipped_operations.append(err_response)
|
|
778
|
+
continue
|
|
779
|
+
|
|
780
|
+
normalized_op = cls._normalized_bulk_operation(op, parameters, timeout)
|
|
781
|
+
valid.append((i, normalized_op, entity_id, action))
|
|
782
|
+
|
|
632
783
|
return valid
|
|
633
784
|
|
|
634
785
|
async def bulk_device_control(
|
|
635
786
|
self,
|
|
636
|
-
operations: list[
|
|
787
|
+
operations: list[Any],
|
|
637
788
|
parallel: bool = True,
|
|
638
789
|
ctx: Context | None = None,
|
|
639
790
|
) -> dict[str, Any]:
|
|
@@ -672,6 +823,10 @@ class DeviceControlTools:
|
|
|
672
823
|
f"{len(skipped_operations)} skipped, "
|
|
673
824
|
f"mode={'parallel' if parallel else 'sequential'}",
|
|
674
825
|
)
|
|
826
|
+
if not valid_operations:
|
|
827
|
+
return self._build_bulk_response(
|
|
828
|
+
operations, results, operation_ids, skipped_operations, parallel
|
|
829
|
+
)
|
|
675
830
|
|
|
676
831
|
# Route through the component's bulk_call_service capability when
|
|
677
832
|
# advertised (D5a): one register-before-fire batch frame confirms every
|
|
@@ -904,7 +1059,7 @@ class DeviceControlTools:
|
|
|
904
1059
|
|
|
905
1060
|
async def _bulk_via_component(
|
|
906
1061
|
self,
|
|
907
|
-
operations: list[
|
|
1062
|
+
operations: list[Any],
|
|
908
1063
|
valid_operations: list[tuple[int, dict[str, Any], str, str]],
|
|
909
1064
|
skipped_operations: list[dict[str, Any]],
|
|
910
1065
|
parallel: bool,
|
|
@@ -1111,7 +1266,7 @@ class DeviceControlTools:
|
|
|
1111
1266
|
|
|
1112
1267
|
def _build_ambiguous_bulk_response(
|
|
1113
1268
|
self,
|
|
1114
|
-
operations: list[
|
|
1269
|
+
operations: list[Any],
|
|
1115
1270
|
valid_operations: list[tuple[int, dict[str, Any], str, str]],
|
|
1116
1271
|
rows: list[dict[str, Any]],
|
|
1117
1272
|
skipped_operations: list[dict[str, Any]],
|
|
@@ -1246,7 +1401,7 @@ class DeviceControlTools:
|
|
|
1246
1401
|
|
|
1247
1402
|
def _build_bulk_response(
|
|
1248
1403
|
self,
|
|
1249
|
-
operations: list[
|
|
1404
|
+
operations: list[Any],
|
|
1250
1405
|
results: list[dict[str, Any]],
|
|
1251
1406
|
operation_ids: list[str],
|
|
1252
1407
|
skipped_operations: list[dict[str, Any]],
|
|
@@ -1293,7 +1448,7 @@ class DeviceControlTools:
|
|
|
1293
1448
|
return response
|
|
1294
1449
|
|
|
1295
1450
|
async def get_bulk_operation_status(
|
|
1296
|
-
self, operation_ids: list[str], timeout_seconds:
|
|
1451
|
+
self, operation_ids: list[str], timeout_seconds: float = 10
|
|
1297
1452
|
) -> dict[str, Any]:
|
|
1298
1453
|
"""
|
|
1299
1454
|
Check status of multiple operations.
|
{ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/smart_search/_base.py
RENAMED
|
@@ -40,7 +40,14 @@ class _SearchBase:
|
|
|
40
40
|
logger.debug(f"Could not fetch {label}: {result}")
|
|
41
41
|
cause = str(result) or type(result).__name__
|
|
42
42
|
elif isinstance(result, dict) and result.get("success"):
|
|
43
|
-
|
|
43
|
+
payload = result.get("result", [])
|
|
44
|
+
if isinstance(payload, list):
|
|
45
|
+
registry = payload
|
|
46
|
+
else:
|
|
47
|
+
cause = (
|
|
48
|
+
"malformed result payload: expected list, "
|
|
49
|
+
f"got {type(payload).__name__}"
|
|
50
|
+
)
|
|
44
51
|
elif isinstance(result, dict):
|
|
45
52
|
cause = str(result.get("error") or "request failed")
|
|
46
53
|
else:
|
{ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/smart_search/_entities.py
RENAMED
|
@@ -4,7 +4,7 @@ import asyncio
|
|
|
4
4
|
import logging
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
|
-
from ...utils.fuzzy_search import calculate_partial_ratio
|
|
7
|
+
from ...utils.fuzzy_search import calculate_partial_ratio, calculate_ratio
|
|
8
8
|
from ...visibility.resolver import (
|
|
9
9
|
device_registry_needed_for_visibility,
|
|
10
10
|
load_hidden_set,
|
|
@@ -405,9 +405,10 @@ class EntitySearchMixin(_SearchBase):
|
|
|
405
405
|
"""
|
|
406
406
|
Get entities grouped by area/room using the HA registries for accurate area resolution.
|
|
407
407
|
|
|
408
|
-
Uses entity
|
|
409
|
-
|
|
410
|
-
|
|
408
|
+
Uses the entity, device, area, and floor registries to determine which
|
|
409
|
+
area each entity belongs to. Resolution tries an exact area first,
|
|
410
|
+
then an exact or unambiguous close-spelling floor (expanded to every
|
|
411
|
+
area on that floor), and finally falls back to fuzzy area matching.
|
|
411
412
|
|
|
412
413
|
Args:
|
|
413
414
|
area_query: Area/room name (or alias) to search for
|
|
@@ -419,6 +420,10 @@ class EntitySearchMixin(_SearchBase):
|
|
|
419
420
|
|
|
420
421
|
Returns:
|
|
421
422
|
Dictionary with area-grouped entities
|
|
423
|
+
|
|
424
|
+
Raises:
|
|
425
|
+
ToolError: If mandatory Home Assistant state retrieval fails or an
|
|
426
|
+
unexpected search error occurs.
|
|
422
427
|
"""
|
|
423
428
|
try:
|
|
424
429
|
# Fetch all registries and states in parallel
|
|
@@ -433,6 +438,9 @@ class EntitySearchMixin(_SearchBase):
|
|
|
433
438
|
self.client.send_websocket_message(
|
|
434
439
|
{"type": "config/device_registry/list"}
|
|
435
440
|
),
|
|
441
|
+
self.client.send_websocket_message(
|
|
442
|
+
{"type": "config/floor_registry/list"}
|
|
443
|
+
),
|
|
436
444
|
return_exceptions=True,
|
|
437
445
|
)
|
|
438
446
|
|
|
@@ -465,11 +473,22 @@ class EntitySearchMixin(_SearchBase):
|
|
|
465
473
|
area_registry = self._parse_area_registry(results[1], registry_warnings)
|
|
466
474
|
entity_reg_map = self._parse_entity_reg_map(results[2], registry_warnings)
|
|
467
475
|
device_area_map = self._parse_device_area_map(results[3], registry_warnings)
|
|
476
|
+
floor_result = results[4]
|
|
477
|
+
floor_registry_available = (
|
|
478
|
+
isinstance(floor_result, dict)
|
|
479
|
+
and floor_result.get("success") is True
|
|
480
|
+
and isinstance(floor_result.get("result"), list)
|
|
481
|
+
)
|
|
482
|
+
floor_registry = self._parse_floor_registry(floor_result, registry_warnings)
|
|
468
483
|
degraded_warnings = registry_warnings + visibility_warnings
|
|
469
484
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
485
|
+
matched_area_ids, resolution_warnings = self._resolve_area_query(
|
|
486
|
+
area_registry,
|
|
487
|
+
floor_registry,
|
|
488
|
+
area_query,
|
|
489
|
+
floor_registry_available=floor_registry_available,
|
|
490
|
+
)
|
|
491
|
+
degraded_warnings.extend(resolution_warnings)
|
|
473
492
|
if not matched_area_ids:
|
|
474
493
|
return merge_visibility_warnings(
|
|
475
494
|
{
|
|
@@ -536,6 +555,18 @@ class EntitySearchMixin(_SearchBase):
|
|
|
536
555
|
area_registry[area_id] = area
|
|
537
556
|
return area_registry
|
|
538
557
|
|
|
558
|
+
@classmethod
|
|
559
|
+
def _parse_floor_registry(
|
|
560
|
+
cls, result: Any, warnings: list[str] | None = None
|
|
561
|
+
) -> dict[str, dict[str, Any]]:
|
|
562
|
+
"""Parse the floor registry into ``floor_id -> floor info``."""
|
|
563
|
+
floor_registry: dict[str, dict[str, Any]] = {}
|
|
564
|
+
for floor in cls._extract_registry_list(result, "floor registry", warnings):
|
|
565
|
+
floor_id = floor.get("floor_id", "")
|
|
566
|
+
if floor_id:
|
|
567
|
+
floor_registry[floor_id] = floor
|
|
568
|
+
return floor_registry
|
|
569
|
+
|
|
539
570
|
@classmethod
|
|
540
571
|
def _parse_entity_reg_map(
|
|
541
572
|
cls, result: Any, warnings: list[str] | None = None
|
|
@@ -565,54 +596,203 @@ class EntitySearchMixin(_SearchBase):
|
|
|
565
596
|
return device_area_map
|
|
566
597
|
|
|
567
598
|
@staticmethod
|
|
599
|
+
def _match_exact_registry_ids(
|
|
600
|
+
registry: dict[str, dict[str, Any]],
|
|
601
|
+
id_key: str,
|
|
602
|
+
query_lower: str,
|
|
603
|
+
) -> set[str]:
|
|
604
|
+
"""Return exact case-insensitive id, name, or alias matches."""
|
|
605
|
+
matches: set[str] = set()
|
|
606
|
+
for registry_id, info in registry.items():
|
|
607
|
+
item_id = str(info.get(id_key) or registry_id)
|
|
608
|
+
name = str(info.get("name") or "")
|
|
609
|
+
aliases = info.get("aliases", []) or []
|
|
610
|
+
if (
|
|
611
|
+
query_lower == item_id.lower()
|
|
612
|
+
or query_lower == name.lower()
|
|
613
|
+
or any(
|
|
614
|
+
query_lower == alias.lower()
|
|
615
|
+
for alias in aliases
|
|
616
|
+
if isinstance(alias, str)
|
|
617
|
+
)
|
|
618
|
+
):
|
|
619
|
+
matches.add(registry_id)
|
|
620
|
+
return matches
|
|
621
|
+
|
|
622
|
+
@classmethod
|
|
623
|
+
def _resolve_area_query(
|
|
624
|
+
cls,
|
|
625
|
+
area_registry: dict[str, dict[str, Any]],
|
|
626
|
+
floor_registry: dict[str, dict[str, Any]],
|
|
627
|
+
area_query: str,
|
|
628
|
+
*,
|
|
629
|
+
floor_registry_available: bool,
|
|
630
|
+
) -> tuple[set[str], list[str]]:
|
|
631
|
+
"""Resolve exact area first, then exact/fuzzy floor, then fuzzy area."""
|
|
632
|
+
query_lower = area_query.lower().strip()
|
|
633
|
+
exact_area_ids = cls._match_exact_registry_ids(
|
|
634
|
+
area_registry, "area_id", query_lower
|
|
635
|
+
)
|
|
636
|
+
exact_floor_ids = cls._match_exact_registry_ids(
|
|
637
|
+
floor_registry, "floor_id", query_lower
|
|
638
|
+
)
|
|
639
|
+
if exact_area_ids:
|
|
640
|
+
warnings = []
|
|
641
|
+
if exact_floor_ids:
|
|
642
|
+
warnings.append(
|
|
643
|
+
f"'{area_query}' matches both an area and a floor; "
|
|
644
|
+
"the exact area match was used."
|
|
645
|
+
)
|
|
646
|
+
return exact_area_ids, warnings
|
|
647
|
+
|
|
648
|
+
if exact_floor_ids:
|
|
649
|
+
area_ids, warning = cls._expand_floor_ids(
|
|
650
|
+
area_registry,
|
|
651
|
+
floor_registry,
|
|
652
|
+
exact_floor_ids,
|
|
653
|
+
area_query,
|
|
654
|
+
fuzzy_match=False,
|
|
655
|
+
)
|
|
656
|
+
return area_ids, [warning]
|
|
657
|
+
|
|
658
|
+
if not floor_registry_available:
|
|
659
|
+
# Without the floor registry, fuzzy area matching could silently
|
|
660
|
+
# recreate the original floor->partial-area misresolution.
|
|
661
|
+
return set(), []
|
|
662
|
+
|
|
663
|
+
fuzzy_floor_ids, floor_score = cls._match_fuzzy_registry_ids(
|
|
664
|
+
floor_registry, "floor_id", query_lower
|
|
665
|
+
)
|
|
666
|
+
_, area_score = cls._match_fuzzy_registry_ids(
|
|
667
|
+
area_registry, "area_id", query_lower
|
|
668
|
+
)
|
|
669
|
+
if len(fuzzy_floor_ids) > 1 and floor_score > area_score:
|
|
670
|
+
floor_names = sorted(
|
|
671
|
+
str(
|
|
672
|
+
floor_registry[floor_id].get("name")
|
|
673
|
+
or floor_registry[floor_id].get("floor_id")
|
|
674
|
+
or floor_id
|
|
675
|
+
)
|
|
676
|
+
for floor_id in fuzzy_floor_ids
|
|
677
|
+
)
|
|
678
|
+
return set(), [
|
|
679
|
+
f"'{area_query}' ambiguously matches multiple floors: "
|
|
680
|
+
f"{', '.join(floor_names)}. Use an exact floor name or ID."
|
|
681
|
+
]
|
|
682
|
+
if len(fuzzy_floor_ids) == 1 and floor_score > area_score:
|
|
683
|
+
area_ids, warning = cls._expand_floor_ids(
|
|
684
|
+
area_registry,
|
|
685
|
+
floor_registry,
|
|
686
|
+
fuzzy_floor_ids,
|
|
687
|
+
area_query,
|
|
688
|
+
fuzzy_match=True,
|
|
689
|
+
)
|
|
690
|
+
return area_ids, [warning]
|
|
691
|
+
return cls._match_area_ids(area_registry, query_lower), []
|
|
692
|
+
|
|
693
|
+
@staticmethod
|
|
694
|
+
def _match_fuzzy_registry_ids(
|
|
695
|
+
registry: dict[str, dict[str, Any]],
|
|
696
|
+
id_key: str,
|
|
697
|
+
query_lower: str,
|
|
698
|
+
) -> tuple[set[str], int]:
|
|
699
|
+
"""Return best whole-string registry matches and their score.
|
|
700
|
+
|
|
701
|
+
Whole-string similarity distinguishes a typo in a floor name from an
|
|
702
|
+
area name that merely contains the floor name as one component.
|
|
703
|
+
"""
|
|
704
|
+
best_ids: set[str] = set()
|
|
705
|
+
best_score = 0
|
|
706
|
+
for registry_id, info in registry.items():
|
|
707
|
+
values = [
|
|
708
|
+
str(info.get(id_key) or registry_id),
|
|
709
|
+
str(info.get("name") or ""),
|
|
710
|
+
*(
|
|
711
|
+
alias
|
|
712
|
+
for alias in (info.get("aliases", []) or [])
|
|
713
|
+
if isinstance(alias, str)
|
|
714
|
+
),
|
|
715
|
+
]
|
|
716
|
+
score = max(
|
|
717
|
+
(
|
|
718
|
+
calculate_ratio(query_lower, value.lower())
|
|
719
|
+
for value in values
|
|
720
|
+
if value
|
|
721
|
+
),
|
|
722
|
+
default=0,
|
|
723
|
+
)
|
|
724
|
+
if score > best_score:
|
|
725
|
+
best_ids = {registry_id}
|
|
726
|
+
best_score = score
|
|
727
|
+
elif score == best_score:
|
|
728
|
+
best_ids.add(registry_id)
|
|
729
|
+
return (best_ids, best_score) if best_score >= 80 else (set(), best_score)
|
|
730
|
+
|
|
731
|
+
@staticmethod
|
|
732
|
+
def _expand_floor_ids(
|
|
733
|
+
area_registry: dict[str, dict[str, Any]],
|
|
734
|
+
floor_registry: dict[str, dict[str, Any]],
|
|
735
|
+
floor_ids: set[str],
|
|
736
|
+
area_query: str,
|
|
737
|
+
*,
|
|
738
|
+
fuzzy_match: bool,
|
|
739
|
+
) -> tuple[set[str], str]:
|
|
740
|
+
"""Expand floor IDs to their areas and build the model-facing warning."""
|
|
741
|
+
area_ids = {
|
|
742
|
+
area_id
|
|
743
|
+
for area_id, area in area_registry.items()
|
|
744
|
+
if area.get("floor_id") in floor_ids
|
|
745
|
+
}
|
|
746
|
+
floor_names = [
|
|
747
|
+
str(
|
|
748
|
+
floor_registry[floor_id].get("name")
|
|
749
|
+
or floor_registry[floor_id].get("floor_id")
|
|
750
|
+
or ""
|
|
751
|
+
)
|
|
752
|
+
for floor_id in sorted(floor_ids)
|
|
753
|
+
]
|
|
754
|
+
floor_label = ", ".join(name for name in floor_names if name)
|
|
755
|
+
floor_suffix = f" ({floor_label})" if floor_label else ""
|
|
756
|
+
match_suffix = " from a close spelling" if fuzzy_match else ""
|
|
757
|
+
warning = (
|
|
758
|
+
f"'{area_query}' is a floor, not an area — expanded to "
|
|
759
|
+
f"{len(area_ids)} area(s) on the matched floor(s){floor_suffix}"
|
|
760
|
+
f"{match_suffix}."
|
|
761
|
+
)
|
|
762
|
+
return area_ids, warning
|
|
763
|
+
|
|
764
|
+
@classmethod
|
|
568
765
|
def _match_area_ids(
|
|
569
|
-
area_registry: dict[str, dict[str, Any]], area_query_lower: str
|
|
766
|
+
cls, area_registry: dict[str, dict[str, Any]], area_query_lower: str
|
|
570
767
|
) -> set[str]:
|
|
571
|
-
"""Resolve the query to
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
honor a literal area_id from ``ha_list_floors_areas`` — a query like
|
|
576
|
-
``"bedroom_kids"`` would otherwise also fuzzy-match its parent
|
|
577
|
-
``"bedroom"`` (partial_ratio=100) and aggregate sibling areas' entities.
|
|
578
|
-
Aliases (per-area registry, used by HA voice config) mirror the
|
|
579
|
-
entity-side enrichment in smart_entity_search.
|
|
768
|
+
"""Resolve the query to area IDs: exact id/name/alias, then fuzzy.
|
|
769
|
+
|
|
770
|
+
Exact matches delegate to the shared registry matcher so area and floor
|
|
771
|
+
precedence use identical case-insensitive semantics.
|
|
580
772
|
"""
|
|
581
|
-
exact_area_ids
|
|
582
|
-
|
|
773
|
+
exact_area_ids = cls._match_exact_registry_ids(
|
|
774
|
+
area_registry, "area_id", area_query_lower
|
|
775
|
+
)
|
|
776
|
+
if exact_area_ids:
|
|
777
|
+
return exact_area_ids
|
|
583
778
|
|
|
779
|
+
fuzzy_area_ids: set[str] = set()
|
|
584
780
|
for area_id, area_info in area_registry.items():
|
|
585
|
-
area_name = area_info.get("name"
|
|
781
|
+
area_name = str(area_info.get("name") or "")
|
|
586
782
|
area_aliases = area_info.get("aliases", []) or []
|
|
587
|
-
# Exact match on area_id, name, or any alias (case-insensitive)
|
|
588
|
-
if (
|
|
589
|
-
area_query_lower == area_id.lower()
|
|
590
|
-
or area_query_lower == area_name.lower()
|
|
591
|
-
or any(
|
|
592
|
-
area_query_lower == a.lower()
|
|
593
|
-
for a in area_aliases
|
|
594
|
-
if isinstance(a, str)
|
|
595
|
-
)
|
|
596
|
-
):
|
|
597
|
-
exact_area_ids.add(area_id)
|
|
598
|
-
continue
|
|
599
|
-
# Fuzzy match on area name, id, or any alias
|
|
600
783
|
name_score = calculate_partial_ratio(area_query_lower, area_name.lower())
|
|
601
784
|
id_score = calculate_partial_ratio(area_query_lower, area_id.lower())
|
|
602
785
|
alias_score = max(
|
|
603
786
|
(
|
|
604
|
-
calculate_partial_ratio(area_query_lower,
|
|
605
|
-
for
|
|
606
|
-
if isinstance(
|
|
787
|
+
calculate_partial_ratio(area_query_lower, alias.lower())
|
|
788
|
+
for alias in area_aliases
|
|
789
|
+
if isinstance(alias, str)
|
|
607
790
|
),
|
|
608
791
|
default=0,
|
|
609
792
|
)
|
|
610
793
|
if max(name_score, id_score, alias_score) >= 80:
|
|
611
794
|
fuzzy_area_ids.add(area_id)
|
|
612
|
-
|
|
613
|
-
# Exact matches win — fuzzy aggregation only runs when no area_query is
|
|
614
|
-
# itself an area_id / name / alias.
|
|
615
|
-
return exact_area_ids or fuzzy_area_ids
|
|
795
|
+
return fuzzy_area_ids
|
|
616
796
|
|
|
617
797
|
@staticmethod
|
|
618
798
|
def _resolve_entity_areas(
|
|
@@ -321,6 +321,13 @@ def _finalize_partial_state(
|
|
|
321
321
|
if partial_local:
|
|
322
322
|
response["partial"] = True
|
|
323
323
|
response["errors"].extend(errors_local)
|
|
324
|
+
local_reason = "; ".join(
|
|
325
|
+
f"{error['surface']}: {error['error']}" for error in errors_local
|
|
326
|
+
)
|
|
327
|
+
existing_reason = response.get("partial_reason")
|
|
328
|
+
response["partial_reason"] = (
|
|
329
|
+
f"{existing_reason}; {local_reason}" if existing_reason else local_reason
|
|
330
|
+
)
|
|
324
331
|
|
|
325
332
|
|
|
326
333
|
def _compute_eligibility(
|
|
@@ -1691,8 +1698,10 @@ class SearchTools:
|
|
|
1691
1698
|
Field(
|
|
1692
1699
|
default=None,
|
|
1693
1700
|
description=(
|
|
1694
|
-
"Narrow entity-registry results to an area (id or
|
|
1695
|
-
"
|
|
1701
|
+
"Narrow entity-registry results to an area (id, name, or alias), "
|
|
1702
|
+
"an exact floor (id, name, or alias), or an unambiguous "
|
|
1703
|
+
"close-spelling floor match; a floor match expands to all areas "
|
|
1704
|
+
"on that floor. Does not affect configuration search."
|
|
1696
1705
|
),
|
|
1697
1706
|
),
|
|
1698
1707
|
] = None,
|
|
@@ -2255,7 +2264,10 @@ class SearchTools:
|
|
|
2255
2264
|
str | None,
|
|
2256
2265
|
Field(
|
|
2257
2266
|
default=None,
|
|
2258
|
-
description=
|
|
2267
|
+
description=(
|
|
2268
|
+
"Limit to an area, or an exact/close floor name to expand "
|
|
2269
|
+
"to every area on that floor."
|
|
2270
|
+
),
|
|
2259
2271
|
),
|
|
2260
2272
|
] = None,
|
|
2261
2273
|
limit: Annotated[
|