ha-mcp-dev 8.3.0.dev2379__tar.gz → 8.3.0.dev2385__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.dev2379/src/ha_mcp_dev.egg-info → ha_mcp_dev-8.3.0.dev2385}/PKG-INFO +1 -1
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/pyproject.toml +1 -1
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/settings_ui/locales/cs.json +1 -1
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/settings_ui/locales/de.json +1 -1
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/settings_ui/locales/eo.json +1 -1
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/settings_ui/locales/es.json +1 -1
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/settings_ui/locales/fr.json +1 -1
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/settings_ui/locales/it.json +1 -1
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/settings_ui/locales/nl.json +1 -1
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/settings_ui/locales/pl.json +1 -1
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/settings_ui/locales/ru.json +1 -1
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/settings_ui/locales/sv.json +1 -1
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/settings_ui/locales/zh-Hans.json +1 -1
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/device_control.py +166 -109
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/smart_search/_base.py +6 -1
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/smart_search/_entities.py +22 -10
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/tools_search.py +10 -7
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/tools_service.py +26 -6
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/LICENSE +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/MANIFEST.in +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/README.md +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/setup.cfg +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_rich_compat.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/requirements.txt +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/LICENSE +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/MANIFEST.sha256 +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/VENDORED +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/__main__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/asyncio/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/asyncio/client.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/asyncio/connection.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/asyncio/messages.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/asyncio/router.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/asyncio/server.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/auth.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/cli.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/client.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/connection.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/datastructures.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/exceptions.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/extensions/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/extensions/base.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/extensions/permessage_deflate.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/frames.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/headers.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/http11.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/imports.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/legacy/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/legacy/auth.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/legacy/client.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/legacy/exceptions.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/legacy/framing.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/legacy/handshake.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/legacy/http.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/legacy/protocol.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/legacy/server.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/protocol.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/proxy.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/py.typed +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/server.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/streams.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/sync/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/sync/client.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/sync/connection.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/sync/messages.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/sync/router.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/sync/server.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/sync/utils.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/trio/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/trio/client.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/trio/connection.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/trio/messages.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/trio/router.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/trio/server.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/trio/utils.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/typing.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/uri.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/utils.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_vendor/websockets/version.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/auth/oidc_compat.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/backup_manager.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/browser_landing.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/dashboard_screenshot/theme_guard.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/hacs_auto_refresh.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/llm_exposure.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/policy/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/policy/approval_queue.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/policy/editing.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/policy/evaluator.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/policy/handlers.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/policy/middleware.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/policy/model.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/policy/persistence.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/policy/value_sources.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/read_only.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/redaction.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/renamed_tools.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/settings_ui/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/settings_ui/_i18n.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/settings_ui/_persistence.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/settings_ui/_theme.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/settings_ui/locales/README.md +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/settings_ui/locales/en.json +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/settings_ui/settings.css +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/settings_ui/settings.html +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/settings_ui/settings.js +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/strict_bps.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/auto_backup.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/best_practice_result.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/best_practice_variables.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/component_api.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/component_config_entries.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/component_config_reads.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/component_devices.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/component_registries.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/component_registry_lookup.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/config_entry_flow.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/config_entry_flow_form.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/config_entry_flow_menu.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/config_entry_flow_walker.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/config_entry_identity.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/config_entry_reconfigure.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/config_entry_reload_watch.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/error_log_parsing.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/hacs_registration.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/integration_reconfigure.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/radio/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/radio/base.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/radio/matter.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/radio/thread.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/radio/zigbee.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/radio/zwave.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/renamed_tool_middleware.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/smart_search/_config.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/tools_assist_pipeline.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/tools_dev.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/tools_radio.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/tools_security_policy.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/tools_themes.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/validation_middleware.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/transport_security.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/update_check.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/utils/config_write_lock.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/utils/skill_loader.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/visibility/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/visibility/enforcement.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/visibility/model.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/visibility/persistence.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/visibility/resolver.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/tests/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/tests/test_constants.py +0 -0
- {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/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.dev2385"
|
|
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
|
{ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/settings_ui/locales/cs.json
RENAMED
|
@@ -470,7 +470,7 @@
|
|
|
470
470
|
"tools": {
|
|
471
471
|
"ha_bulk_control": {
|
|
472
472
|
"title": "Hromadné ovládání",
|
|
473
|
-
"description": "
|
|
473
|
+
"description": "Správa více akcí entit v jednom požadavku."
|
|
474
474
|
},
|
|
475
475
|
"ha_call_event": {
|
|
476
476
|
"title": "Vyvolat událost",
|
{ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/settings_ui/locales/de.json
RENAMED
|
@@ -501,7 +501,7 @@
|
|
|
501
501
|
"tools": {
|
|
502
502
|
"ha_bulk_control": {
|
|
503
503
|
"title": "Massensteuerung",
|
|
504
|
-
"description": "
|
|
504
|
+
"description": "Verwalte mehrere Entitätsaktionen in einer Anfrage."
|
|
505
505
|
},
|
|
506
506
|
"ha_call_event": {
|
|
507
507
|
"title": "Ereignis aufrufen",
|
{ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/settings_ui/locales/eo.json
RENAMED
|
@@ -470,7 +470,7 @@
|
|
|
470
470
|
"tools": {
|
|
471
471
|
"ha_bulk_control": {
|
|
472
472
|
"title": "Amasa Kontrolo",
|
|
473
|
-
"description": "
|
|
473
|
+
"description": "Mastrumi plurajn entajn agojn en unu peto."
|
|
474
474
|
},
|
|
475
475
|
"ha_call_event": {
|
|
476
476
|
"title": "Voki Eventon",
|
{ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/settings_ui/locales/es.json
RENAMED
|
@@ -501,7 +501,7 @@
|
|
|
501
501
|
"tools": {
|
|
502
502
|
"ha_bulk_control": {
|
|
503
503
|
"title": "Control en bloque",
|
|
504
|
-
"description": "
|
|
504
|
+
"description": "Gestiona múltiples acciones de entidades en una sola solicitud."
|
|
505
505
|
},
|
|
506
506
|
"ha_call_event": {
|
|
507
507
|
"title": "Lanzar evento",
|
{ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/settings_ui/locales/fr.json
RENAMED
|
@@ -501,7 +501,7 @@
|
|
|
501
501
|
"tools": {
|
|
502
502
|
"ha_bulk_control": {
|
|
503
503
|
"title": "Contrôle groupé",
|
|
504
|
-
"description": "
|
|
504
|
+
"description": "Gérer plusieurs actions d'entité en une seule requête."
|
|
505
505
|
},
|
|
506
506
|
"ha_call_event": {
|
|
507
507
|
"title": "Déclencher un événement",
|
{ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/settings_ui/locales/it.json
RENAMED
|
@@ -501,7 +501,7 @@
|
|
|
501
501
|
"tools": {
|
|
502
502
|
"ha_bulk_control": {
|
|
503
503
|
"title": "Controllo in blocco",
|
|
504
|
-
"description": "
|
|
504
|
+
"description": "Gestisci più azioni di entità in una singola richiesta."
|
|
505
505
|
},
|
|
506
506
|
"ha_call_event": {
|
|
507
507
|
"title": "Invia un evento",
|
{ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/settings_ui/locales/nl.json
RENAMED
|
@@ -470,7 +470,7 @@
|
|
|
470
470
|
"tools": {
|
|
471
471
|
"ha_bulk_control": {
|
|
472
472
|
"title": "Bulkbediening",
|
|
473
|
-
"description": "
|
|
473
|
+
"description": "Beheer meerdere entiteitsacties in één verzoek."
|
|
474
474
|
},
|
|
475
475
|
"ha_call_event": {
|
|
476
476
|
"title": "Roep Gebeurtenis aan",
|
{ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/settings_ui/locales/pl.json
RENAMED
|
@@ -470,7 +470,7 @@
|
|
|
470
470
|
"tools": {
|
|
471
471
|
"ha_bulk_control": {
|
|
472
472
|
"title": "Sterowanie zbiorcze",
|
|
473
|
-
"description": "
|
|
473
|
+
"description": "Zarządzaj wieloma akcjami encji w jednym żądaniu."
|
|
474
474
|
},
|
|
475
475
|
"ha_call_event": {
|
|
476
476
|
"title": "Wywołaj zdarzenie",
|
{ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/settings_ui/locales/ru.json
RENAMED
|
@@ -501,7 +501,7 @@
|
|
|
501
501
|
"tools": {
|
|
502
502
|
"ha_bulk_control": {
|
|
503
503
|
"title": "Массовое управление",
|
|
504
|
-
"description": "Управление
|
|
504
|
+
"description": "Управление действиями нескольких сущностей в одном запросе."
|
|
505
505
|
},
|
|
506
506
|
"ha_call_event": {
|
|
507
507
|
"title": "Вызвать событие",
|
{ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/settings_ui/locales/sv.json
RENAMED
|
@@ -470,7 +470,7 @@
|
|
|
470
470
|
"tools": {
|
|
471
471
|
"ha_bulk_control": {
|
|
472
472
|
"title": "Masskontroll",
|
|
473
|
-
"description": "
|
|
473
|
+
"description": "Hantera flera entitetsåtgärder i en enda begäran."
|
|
474
474
|
},
|
|
475
475
|
"ha_call_event": {
|
|
476
476
|
"title": "Anropa Händelse",
|
|
@@ -42,10 +42,23 @@ from .helpers import (
|
|
|
42
42
|
safe_info,
|
|
43
43
|
safe_progress,
|
|
44
44
|
)
|
|
45
|
+
from .tools_service import BulkControlOperation
|
|
45
46
|
from .util_helpers import _SERVICE_TO_STATE
|
|
46
47
|
|
|
47
48
|
logger = logging.getLogger(__name__)
|
|
48
49
|
|
|
50
|
+
# The bulk row contract is declared once, on the advertised transport schema.
|
|
51
|
+
# Reading its keys back here keeps the runtime batch validator from drifting
|
|
52
|
+
# out of sync with what models are told they may send.
|
|
53
|
+
_BULK_OPERATION_KEYS: frozenset[str] = frozenset(BulkControlOperation.__annotations__)
|
|
54
|
+
|
|
55
|
+
# The row shape models get wrong most often is a service-style row, so this
|
|
56
|
+
# leads every suggestion list a rejected bulk batch produces.
|
|
57
|
+
_BULK_ROW_SHAPE_SUGGESTION = (
|
|
58
|
+
"Use entity_id and action (e.g. action='off', not service='turn_off'); "
|
|
59
|
+
"send no domain or service keys and put action data in parameters"
|
|
60
|
+
)
|
|
61
|
+
|
|
49
62
|
# The ha_mcp_tools/bulk_call_service WS command (Phase 3, D5a): the BATCH write
|
|
50
63
|
# capability. When the component advertises ``bulk_call_service`` the consumer
|
|
51
64
|
# resolves every op's domain/service server-side (D6), sends one register-before-fire
|
|
@@ -347,6 +360,10 @@ class DeviceControlTools:
|
|
|
347
360
|
"set": "set_temperature",
|
|
348
361
|
}
|
|
349
362
|
|
|
363
|
+
# Silent allowlist: _add_domain_params copies only these keys into the
|
|
364
|
+
# service call and drops everything else without an error, and a domain
|
|
365
|
+
# absent from this table loses every parameter. Extend it when adding
|
|
366
|
+
# support for a new parameter, or the parameter vanishes at dispatch.
|
|
350
367
|
_DOMAIN_PARAMS: ClassVar[dict[str, list[str]]] = {
|
|
351
368
|
"light": [
|
|
352
369
|
"brightness",
|
|
@@ -474,10 +491,10 @@ class DeviceControlTools:
|
|
|
474
491
|
"""Check status of a device operation, waiting up to ``timeout_seconds`` for completion.
|
|
475
492
|
|
|
476
493
|
Polls the in-memory operation registry (mutated by the WebSocket
|
|
477
|
-
listener as state changes arrive)
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
``control_device_smart``.
|
|
494
|
+
listener as state changes arrive) while the operation is pending, at
|
|
495
|
+
0.2s intervals except for a final shorter poll that lands exactly on
|
|
496
|
+
the deadline. Returns the final structured status —
|
|
497
|
+
completed/failed/timeout/pending — produced by ``control_device_smart``.
|
|
481
498
|
"""
|
|
482
499
|
operation = get_operation_from_memory(operation_id)
|
|
483
500
|
|
|
@@ -607,31 +624,58 @@ class DeviceControlTools:
|
|
|
607
624
|
@staticmethod
|
|
608
625
|
def _normalize_bulk_parameters(
|
|
609
626
|
value: Any,
|
|
610
|
-
) -> tuple[dict[str, Any] | None, ErrorCode | None]:
|
|
611
|
-
"""Normalize
|
|
627
|
+
) -> tuple[dict[str, Any] | None, ErrorCode | None, str]:
|
|
628
|
+
"""Normalize a present ``parameters`` value and identify invalid ones.
|
|
629
|
+
|
|
630
|
+
Returns ``(parameters, error_code, detail)``. ``detail`` carries the
|
|
631
|
+
decoder's own reason and offset for a JSON failure — without it the
|
|
632
|
+
caller can only say "invalid JSON", which does not tell the sender
|
|
633
|
+
where its string went wrong. It is empty for every other outcome.
|
|
634
|
+
"""
|
|
612
635
|
if isinstance(value, str):
|
|
613
636
|
try:
|
|
614
637
|
value = json.loads(value)
|
|
615
|
-
except json.JSONDecodeError:
|
|
616
|
-
return
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
638
|
+
except json.JSONDecodeError as exc:
|
|
639
|
+
return (
|
|
640
|
+
None,
|
|
641
|
+
ErrorCode.VALIDATION_INVALID_JSON,
|
|
642
|
+
f"{exc.msg} at position {exc.pos}",
|
|
643
|
+
)
|
|
644
|
+
except RecursionError:
|
|
645
|
+
# json.loads recurses per nesting level; a pathologically
|
|
646
|
+
# nested string must skip the row, not abort the batch.
|
|
647
|
+
return (
|
|
648
|
+
None,
|
|
649
|
+
ErrorCode.VALIDATION_INVALID_JSON,
|
|
650
|
+
"JSON nested too deeply",
|
|
651
|
+
)
|
|
652
|
+
if not isinstance(value, dict):
|
|
653
|
+
# A present-but-null value is malformed, same as timeout_seconds
|
|
654
|
+
# and validate_first; an absent key never reaches this helper.
|
|
655
|
+
return None, ErrorCode.VALIDATION_INVALID_PARAMETER, ""
|
|
656
|
+
return value, None, ""
|
|
620
657
|
|
|
621
658
|
@staticmethod
|
|
622
|
-
def _normalize_bulk_timeout(value: Any) ->
|
|
623
|
-
"""Normalize
|
|
624
|
-
|
|
625
|
-
|
|
659
|
+
def _normalize_bulk_timeout(value: Any) -> float | None:
|
|
660
|
+
"""Normalize a non-negative finite timeout, or ``None`` when invalid.
|
|
661
|
+
|
|
662
|
+
Bools are rejected before ``float()`` sees them: ``float(True)`` is 1.0,
|
|
663
|
+
which would silently accept ``timeout_seconds: true`` as a one-second
|
|
664
|
+
wait. Strings are rejected for the same reason — the advertised schema
|
|
665
|
+
is ``strict``, so coercing ``"10"`` here would silently accept what the
|
|
666
|
+
schema tells the model is invalid. ``None`` is invalid here too — the
|
|
667
|
+
caller only calls this when the key is present, and a present-but-null
|
|
668
|
+
timeout is a malformed row.
|
|
669
|
+
"""
|
|
670
|
+
if isinstance(value, (bool, str)) or value is None:
|
|
671
|
+
return None
|
|
626
672
|
try:
|
|
627
673
|
timeout = float(value)
|
|
628
|
-
except (TypeError, ValueError):
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
else (None, False)
|
|
634
|
-
)
|
|
674
|
+
except (TypeError, ValueError, OverflowError):
|
|
675
|
+
# OverflowError: an int too large for float must skip the row,
|
|
676
|
+
# not abort the batch through the generic exception handler.
|
|
677
|
+
return None
|
|
678
|
+
return timeout if math.isfinite(timeout) and timeout >= 0 else None
|
|
635
679
|
|
|
636
680
|
@staticmethod
|
|
637
681
|
def _normalized_bulk_operation(
|
|
@@ -647,6 +691,28 @@ class DeviceControlTools:
|
|
|
647
691
|
normalized["timeout_seconds"] = timeout
|
|
648
692
|
return normalized
|
|
649
693
|
|
|
694
|
+
@staticmethod
|
|
695
|
+
def _skip_bulk_operation(
|
|
696
|
+
skipped_operations: list[dict[str, Any]],
|
|
697
|
+
op: Any,
|
|
698
|
+
code: ErrorCode,
|
|
699
|
+
error: str,
|
|
700
|
+
context: dict[str, Any],
|
|
701
|
+
suggestions: list[str] | None = None,
|
|
702
|
+
) -> None:
|
|
703
|
+
"""Log one rejected bulk row and record its structured skip entry.
|
|
704
|
+
|
|
705
|
+
``create_error_response`` lifts every ``context`` key to the top level,
|
|
706
|
+
so ``index`` needs no separate write; ``operation`` is not a context
|
|
707
|
+
key and is attached here so callers can see the row they sent.
|
|
708
|
+
"""
|
|
709
|
+
logger.warning(f"Bulk control: {error}")
|
|
710
|
+
err_response = create_error_response(
|
|
711
|
+
code, error, suggestions=suggestions, context=context
|
|
712
|
+
)
|
|
713
|
+
err_response["operation"] = op
|
|
714
|
+
skipped_operations.append(err_response)
|
|
715
|
+
|
|
650
716
|
@classmethod
|
|
651
717
|
def _validate_bulk_operations(
|
|
652
718
|
cls,
|
|
@@ -656,41 +722,28 @@ class DeviceControlTools:
|
|
|
656
722
|
valid: list[tuple[int, dict[str, Any], str, str]] = []
|
|
657
723
|
for i, op in enumerate(operations):
|
|
658
724
|
if not isinstance(op, dict):
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
ErrorCode.
|
|
725
|
+
cls._skip_bulk_operation(
|
|
726
|
+
skipped_operations,
|
|
727
|
+
op,
|
|
728
|
+
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
729
|
+
f"Operation at index {i} is not a dict: {type(op).__name__}",
|
|
730
|
+
{"index": i},
|
|
663
731
|
)
|
|
664
|
-
err_response["index"] = i
|
|
665
|
-
err_response["operation"] = op
|
|
666
|
-
skipped_operations.append(err_response)
|
|
667
732
|
continue
|
|
668
733
|
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
"parameters",
|
|
673
|
-
"timeout_seconds",
|
|
674
|
-
"validate_first",
|
|
675
|
-
}
|
|
676
|
-
obsolete_keys = sorted(set(op) - allowed_keys)
|
|
734
|
+
# str() before sorting: a direct Python caller can pass non-string
|
|
735
|
+
# keys, and mixed key types make sorted()/join() raise mid-batch.
|
|
736
|
+
obsolete_keys = sorted(str(key) for key in set(op) - _BULK_OPERATION_KEYS)
|
|
677
737
|
if obsolete_keys:
|
|
678
738
|
key_list = ", ".join(obsolete_keys)
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
logger.warning(f"Bulk control: {error}")
|
|
683
|
-
err_response = create_error_response(
|
|
739
|
+
cls._skip_bulk_operation(
|
|
740
|
+
skipped_operations,
|
|
741
|
+
op,
|
|
684
742
|
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
],
|
|
689
|
-
context={"index": i, "unsupported_keys": obsolete_keys},
|
|
743
|
+
f"Operation at index {i} contains unsupported key(s): {key_list}",
|
|
744
|
+
{"index": i, "unsupported_keys": obsolete_keys},
|
|
745
|
+
suggestions=[_BULK_ROW_SHAPE_SUGGESTION],
|
|
690
746
|
)
|
|
691
|
-
err_response["index"] = i
|
|
692
|
-
err_response["operation"] = op
|
|
693
|
-
skipped_operations.append(err_response)
|
|
694
747
|
continue
|
|
695
748
|
|
|
696
749
|
entity_id = op.get("entity_id")
|
|
@@ -698,83 +751,70 @@ class DeviceControlTools:
|
|
|
698
751
|
missing = [f for f in ("entity_id", "action") if not op.get(f)]
|
|
699
752
|
|
|
700
753
|
if missing:
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
ErrorCode.VALIDATION_MISSING_PARAMETER,
|
|
754
|
+
cls._skip_bulk_operation(
|
|
755
|
+
skipped_operations,
|
|
756
|
+
op,
|
|
757
|
+
ErrorCode.VALIDATION_MISSING_PARAMETER,
|
|
758
|
+
f"Operation at index {i} missing required fields: "
|
|
759
|
+
f"{', '.join(missing)}",
|
|
760
|
+
{"index": i},
|
|
705
761
|
)
|
|
706
|
-
err_response["index"] = i
|
|
707
|
-
err_response["operation"] = op
|
|
708
|
-
skipped_operations.append(err_response)
|
|
709
762
|
continue
|
|
710
763
|
|
|
711
764
|
if not isinstance(entity_id, str) or not isinstance(action, str):
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
765
|
+
cls._skip_bulk_operation(
|
|
766
|
+
skipped_operations,
|
|
767
|
+
op,
|
|
715
768
|
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
716
|
-
|
|
717
|
-
|
|
769
|
+
f"Operation at index {i} requires string entity_id and "
|
|
770
|
+
"action fields",
|
|
771
|
+
{"index": i},
|
|
718
772
|
)
|
|
719
|
-
err_response["index"] = i
|
|
720
|
-
err_response["operation"] = op
|
|
721
|
-
skipped_operations.append(err_response)
|
|
722
773
|
continue
|
|
723
774
|
|
|
724
|
-
parameters, parameter_error =
|
|
725
|
-
|
|
775
|
+
parameters, parameter_error, parameter_detail = (
|
|
776
|
+
cls._normalize_bulk_parameters(op["parameters"])
|
|
777
|
+
if "parameters" in op
|
|
778
|
+
else (None, None, "")
|
|
726
779
|
)
|
|
727
780
|
if parameter_error is not None:
|
|
728
|
-
|
|
729
|
-
f"Operation at index {i} has invalid JSON parameters"
|
|
781
|
+
parameter_message = (
|
|
782
|
+
f"Operation at index {i} has invalid JSON parameters: "
|
|
783
|
+
f"{parameter_detail}"
|
|
730
784
|
if parameter_error == ErrorCode.VALIDATION_INVALID_JSON
|
|
731
785
|
else f"Operation at index {i} parameters must be a JSON object"
|
|
732
786
|
)
|
|
733
|
-
|
|
734
|
-
|
|
787
|
+
cls._skip_bulk_operation(
|
|
788
|
+
skipped_operations,
|
|
789
|
+
op,
|
|
735
790
|
parameter_error,
|
|
736
|
-
|
|
737
|
-
|
|
791
|
+
parameter_message,
|
|
792
|
+
{"index": i},
|
|
738
793
|
)
|
|
739
|
-
err_response["index"] = i
|
|
740
|
-
err_response["operation"] = op
|
|
741
|
-
skipped_operations.append(err_response)
|
|
742
794
|
continue
|
|
743
795
|
|
|
744
|
-
timeout = None
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
timeout
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
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
|
|
796
|
+
timeout: float | None = None
|
|
797
|
+
if "timeout_seconds" in op:
|
|
798
|
+
timeout = cls._normalize_bulk_timeout(op["timeout_seconds"])
|
|
799
|
+
if timeout is None:
|
|
800
|
+
cls._skip_bulk_operation(
|
|
801
|
+
skipped_operations,
|
|
802
|
+
op,
|
|
803
|
+
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
804
|
+
f"Operation at index {i} timeout_seconds must be a "
|
|
805
|
+
"non-negative number",
|
|
806
|
+
{"index": i},
|
|
807
|
+
)
|
|
808
|
+
continue
|
|
766
809
|
|
|
767
|
-
if "validate_first" in op and
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
810
|
+
if "validate_first" in op and not isinstance(op["validate_first"], bool):
|
|
811
|
+
cls._skip_bulk_operation(
|
|
812
|
+
skipped_operations,
|
|
813
|
+
op,
|
|
771
814
|
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
772
|
-
|
|
773
|
-
|
|
815
|
+
f"Operation at index {i} validate_first must be a boolean",
|
|
816
|
+
{"index": i},
|
|
774
817
|
)
|
|
775
|
-
err_response["index"] = i
|
|
776
|
-
err_response["operation"] = op
|
|
777
|
-
skipped_operations.append(err_response)
|
|
778
818
|
continue
|
|
779
819
|
|
|
780
820
|
normalized_op = cls._normalized_bulk_operation(op, parameters, timeout)
|
|
@@ -824,8 +864,24 @@ class DeviceControlTools:
|
|
|
824
864
|
f"mode={'parallel' if parallel else 'sequential'}",
|
|
825
865
|
)
|
|
826
866
|
if not valid_operations:
|
|
827
|
-
|
|
828
|
-
|
|
867
|
+
# Nothing dispatched, so nothing to fail soft for: the batch
|
|
868
|
+
# carve-out from "all tool-level failures raise" exists to
|
|
869
|
+
# protect successful siblings, and there are none. Raising
|
|
870
|
+
# before the component probe keeps the no-dispatch guarantee.
|
|
871
|
+
raise_tool_error(
|
|
872
|
+
create_error_response(
|
|
873
|
+
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
874
|
+
f"All {len(operations)} operation(s) failed validation; "
|
|
875
|
+
"nothing was dispatched",
|
|
876
|
+
suggestions=[
|
|
877
|
+
_BULK_ROW_SHAPE_SUGGESTION,
|
|
878
|
+
"Each operation requires 'entity_id' and 'action'",
|
|
879
|
+
"Check skipped_details for the per-operation reason",
|
|
880
|
+
"Example: {'entity_id': 'light.living_room', "
|
|
881
|
+
+ "'action': 'on'}",
|
|
882
|
+
],
|
|
883
|
+
context={"skipped_details": skipped_operations},
|
|
884
|
+
)
|
|
829
885
|
)
|
|
830
886
|
|
|
831
887
|
# Route through the component's bulk_call_service capability when
|
|
@@ -1439,6 +1495,7 @@ class DeviceControlTools:
|
|
|
1439
1495
|
if skipped_operations:
|
|
1440
1496
|
response["skipped_details"] = skipped_operations
|
|
1441
1497
|
response["suggestions"] = [
|
|
1498
|
+
_BULK_ROW_SHAPE_SUGGESTION,
|
|
1442
1499
|
"Some operations were skipped due to validation errors",
|
|
1443
1500
|
"Each operation requires 'entity_id' and 'action' fields",
|
|
1444
1501
|
"Check skipped_details for specific errors",
|
{ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/smart_search/_base.py
RENAMED
|
@@ -40,9 +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
|
+
# No default for a missing key: a legitimate empty registry is
|
|
44
|
+
# "result": [], so an absent result is a malformed envelope and
|
|
45
|
+
# must degrade loudly, not read as available-and-empty.
|
|
46
|
+
payload = result.get("result")
|
|
44
47
|
if isinstance(payload, list):
|
|
45
48
|
registry = payload
|
|
49
|
+
elif payload is None and "result" not in result:
|
|
50
|
+
cause = "malformed result payload: result key missing"
|
|
46
51
|
else:
|
|
47
52
|
cause = (
|
|
48
53
|
"malformed result payload: expected list, "
|
{ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2385}/src/ha_mcp/tools/smart_search/_entities.py
RENAMED
|
@@ -473,13 +473,15 @@ class EntitySearchMixin(_SearchBase):
|
|
|
473
473
|
area_registry = self._parse_area_registry(results[1], registry_warnings)
|
|
474
474
|
entity_reg_map = self._parse_entity_reg_map(results[2], registry_warnings)
|
|
475
475
|
device_area_map = self._parse_device_area_map(results[3], registry_warnings)
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
floor_registry = self._parse_floor_registry(
|
|
476
|
+
# Availability is read back OUT of the parse rather than re-derived
|
|
477
|
+
# from the raw payload: the parser already decides what counts as
|
|
478
|
+
# usable floor data, and a second predicate here drifted from it
|
|
479
|
+
# once. An empty warnings list means the parser accepted the
|
|
480
|
+
# payload; anything else disables the fuzzy floor/area fallback.
|
|
481
|
+
floor_warnings: list[str] = []
|
|
482
|
+
floor_registry = self._parse_floor_registry(results[4], floor_warnings)
|
|
483
|
+
floor_registry_available = not floor_warnings
|
|
484
|
+
registry_warnings.extend(floor_warnings)
|
|
483
485
|
degraded_warnings = registry_warnings + visibility_warnings
|
|
484
486
|
|
|
485
487
|
matched_area_ids, resolution_warnings = self._resolve_area_query(
|
|
@@ -657,8 +659,15 @@ class EntitySearchMixin(_SearchBase):
|
|
|
657
659
|
|
|
658
660
|
if not floor_registry_available:
|
|
659
661
|
# Without the floor registry, fuzzy area matching could silently
|
|
660
|
-
# recreate the original floor->partial-area misresolution.
|
|
661
|
-
|
|
662
|
+
# recreate the original floor->partial-area misresolution. The
|
|
663
|
+
# fetch warning names the outage; this one names what the outage
|
|
664
|
+
# cost the caller, so "no match" is not read as "no such area".
|
|
665
|
+
return set(), [
|
|
666
|
+
f"Floor data is unavailable, so '{area_query}' was matched only "
|
|
667
|
+
"against exact area IDs, names, and aliases; close-spelling "
|
|
668
|
+
"matching was skipped. Retry, or pass an exact area_id from "
|
|
669
|
+
"ha_list_floors_areas."
|
|
670
|
+
]
|
|
662
671
|
|
|
663
672
|
fuzzy_floor_ids, floor_score = cls._match_fuzzy_registry_ids(
|
|
664
673
|
floor_registry, "floor_id", query_lower
|
|
@@ -768,7 +777,10 @@ class EntitySearchMixin(_SearchBase):
|
|
|
768
777
|
"""Resolve the query to area IDs: exact id/name/alias, then fuzzy.
|
|
769
778
|
|
|
770
779
|
Exact matches delegate to the shared registry matcher so area and floor
|
|
771
|
-
precedence use identical case-insensitive semantics.
|
|
780
|
+
precedence use identical case-insensitive semantics. Exact must win
|
|
781
|
+
outright: a query like "bedroom_kids" partial-matches its parent
|
|
782
|
+
"bedroom" at ratio 100, so falling through to the fuzzy pass would
|
|
783
|
+
aggregate every sibling area's entities under an exact hit.
|
|
772
784
|
"""
|
|
773
785
|
exact_area_ids = cls._match_exact_registry_ids(
|
|
774
786
|
area_registry, "area_id", area_query_lower
|
|
@@ -321,12 +321,11 @@ def _finalize_partial_state(
|
|
|
321
321
|
if partial_local:
|
|
322
322
|
response["partial"] = True
|
|
323
323
|
response["errors"].extend(errors_local)
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
f"{existing_reason}; {local_reason}" if existing_reason else local_reason
|
|
324
|
+
_merge_partial_reason(
|
|
325
|
+
response,
|
|
326
|
+
"; ".join(
|
|
327
|
+
f"{error['surface']}: {error['error']}" for error in errors_local
|
|
328
|
+
),
|
|
330
329
|
)
|
|
331
330
|
|
|
332
331
|
|
|
@@ -2217,7 +2216,11 @@ class SearchTools:
|
|
|
2217
2216
|
raise outcome
|
|
2218
2217
|
if isinstance(outcome, Exception):
|
|
2219
2218
|
partial = True
|
|
2220
|
-
|
|
2219
|
+
# ``str(asyncio.TimeoutError())`` is "" — fall back to the type
|
|
2220
|
+
# name so partial_reason never reads "entities: ".
|
|
2221
|
+
errors.append(
|
|
2222
|
+
{"surface": label, "error": str(outcome) or type(outcome).__name__}
|
|
2223
|
+
)
|
|
2221
2224
|
logger.warning("ha_search %s branch failed: %r", label, outcome)
|
|
2222
2225
|
continue
|
|
2223
2226
|
_apply_search_outcome(response, label, outcome)
|