ha-mcp-dev 8.2.0.dev2352__tar.gz → 8.2.0.dev2354__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.2.0.dev2352/src/ha_mcp_dev.egg-info → ha_mcp_dev-8.2.0.dev2354}/PKG-INFO +1 -1
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/pyproject.toml +1 -1
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/client/rest_client.py +90 -21
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/backup.py +5 -1
- ha_mcp_dev-8.2.0.dev2354/src/ha_mcp/tools/component_config_entries.py +224 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/component_config_reads.py +10 -2
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/component_devices.py +16 -2
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/component_registries.py +8 -1
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/component_registry_lookup.py +16 -2
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/config_entry_flow.py +50 -14
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/config_entry_flow_walker.py +512 -58
- ha_mcp_dev-8.2.0.dev2354/src/ha_mcp/tools/config_entry_identity.py +510 -0
- ha_mcp_dev-8.2.0.dev2354/src/ha_mcp/tools/config_entry_reconfigure.py +1012 -0
- ha_mcp_dev-8.2.0.dev2354/src/ha_mcp/tools/config_entry_reload_watch.py +210 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/device_control.py +3 -1
- ha_mcp_dev-8.2.0.dev2354/src/ha_mcp/tools/integration_reconfigure.py +352 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_blueprints.py +3 -1
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_config_dashboards.py +5 -1
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_config_helpers.py +3 -1
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_dev.py +5 -1
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_entities.py +5 -1
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_integrations.py +124 -4
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_search.py +9 -3
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_service.py +3 -1
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_services.py +5 -1
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_system.py +3 -1
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_voice_assistant.py +3 -1
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/util_helpers.py +5 -1
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp_dev.egg-info/SOURCES.txt +5 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/LICENSE +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/MANIFEST.in +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/README.md +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/setup.cfg +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_rich_compat.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/__init__.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/requirements.txt +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/LICENSE +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/MANIFEST.sha256 +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/VENDORED +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/__init__.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/__main__.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/asyncio/__init__.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/asyncio/client.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/asyncio/connection.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/asyncio/messages.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/asyncio/router.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/asyncio/server.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/auth.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/cli.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/client.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/connection.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/datastructures.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/exceptions.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/extensions/__init__.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/extensions/base.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/extensions/permessage_deflate.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/frames.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/headers.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/http11.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/imports.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/legacy/__init__.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/legacy/auth.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/legacy/client.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/legacy/exceptions.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/legacy/framing.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/legacy/handshake.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/legacy/http.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/legacy/protocol.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/legacy/server.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/protocol.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/proxy.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/py.typed +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/server.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/streams.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/sync/__init__.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/sync/client.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/sync/connection.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/sync/messages.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/sync/router.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/sync/server.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/sync/utils.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/trio/__init__.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/trio/client.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/trio/connection.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/trio/messages.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/trio/router.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/trio/server.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/trio/utils.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/typing.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/uri.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/utils.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/version.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/auth/oidc_compat.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/backup_manager.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/browser_landing.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/dashboard_screenshot/theme_guard.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/hacs_auto_refresh.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/llm_exposure.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/policy/__init__.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/policy/approval_queue.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/policy/editing.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/policy/evaluator.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/policy/handlers.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/policy/middleware.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/policy/model.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/policy/persistence.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/policy/value_sources.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/read_only.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/redaction.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/__init__.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/_i18n.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/_persistence.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/_theme.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/locales/README.md +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/locales/cs.json +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/locales/de.json +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/locales/en.json +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/locales/eo.json +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/locales/es.json +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/locales/fr.json +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/locales/it.json +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/locales/nl.json +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/locales/pl.json +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/locales/ru.json +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/locales/sv.json +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/locales/zh-Hans.json +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/settings.css +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/settings.html +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/settings.js +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/strict_bps.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/auto_backup.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/best_practice_result.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/best_practice_variables.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/component_api.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/config_entry_flow_form.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/config_entry_flow_menu.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/error_log_parsing.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/hacs_registration.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/radio/__init__.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/radio/base.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/radio/matter.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/radio/thread.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/radio/zigbee.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/radio/zwave.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/smart_search/_base.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/smart_search/_config.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_assist_pipeline.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_radio.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_security_policy.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_themes.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/validation_middleware.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/transport_security.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/update_check.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/utils/config_write_lock.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/utils/skill_loader.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/visibility/__init__.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/visibility/enforcement.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/visibility/model.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/visibility/persistence.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/visibility/resolver.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/tests/__init__.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/tests/test_constants.py +0 -0
- {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/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.2.0.
|
|
7
|
+
version = "8.2.0.dev2354"
|
|
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
|
|
@@ -36,9 +36,21 @@ def _is_ssl_error(exc: BaseException) -> bool:
|
|
|
36
36
|
logger = logging.getLogger(__name__)
|
|
37
37
|
|
|
38
38
|
# Transient gateway statuses from a reverse proxy / Supervisor ingress — HA Core
|
|
39
|
-
# restarting or briefly overloaded behind it.
|
|
40
|
-
#
|
|
41
|
-
|
|
39
|
+
# restarting or briefly overloaded behind it. Retried for SAFE METHODS ONLY.
|
|
40
|
+
#
|
|
41
|
+
# None of them proves Home Assistant did not execute the request: RFC 9110 has
|
|
42
|
+
# 502 as "received an invalid response from an inbound server", which a proxy
|
|
43
|
+
# only sends after reaching it, and 504 as the upstream answering too slowly.
|
|
44
|
+
# #1623 extended the retry to writes on the premise that "a gateway 5xx means
|
|
45
|
+
# the request never reached the backend"; that premise is false, and replaying
|
|
46
|
+
# a write can double-apply it — fire an event twice, run a script twice, or
|
|
47
|
+
# turn a completed DELETE into a misleading 404 on the replay.
|
|
48
|
+
#
|
|
49
|
+
# The flake class #1623 fixed was a 502 storm failing ~190 tests at once, which
|
|
50
|
+
# is overwhelmingly reads; those keep the retry. A write that fails loudly is
|
|
51
|
+
# recoverable by the caller, which a silent double-apply is not.
|
|
52
|
+
_RETRYABLE_GATEWAY_STATUS = frozenset({502, 503, 504})
|
|
53
|
+
_SAFE_METHODS = frozenset({"GET", "HEAD", "OPTIONS"})
|
|
42
54
|
_MAX_REQUEST_ATTEMPTS = 3
|
|
43
55
|
# Journald window requested for the Core error log on Supervisor-backed
|
|
44
56
|
# installs. Both such branches of get_error_log() build their request from this
|
|
@@ -304,7 +316,8 @@ class HomeAssistantClient:
|
|
|
304
316
|
Handles auth, HTTP 4xx/5xx, and transport errors in one place.
|
|
305
317
|
Callers parse the body themselves (JSON via `_request`, text via
|
|
306
318
|
`get_addon_logs`, etc.). Transient gateway errors (502/503/504) are
|
|
307
|
-
retried with bounded exponential backoff
|
|
319
|
+
retried with bounded exponential backoff for safe methods only; a write
|
|
320
|
+
is never replayed.
|
|
308
321
|
|
|
309
322
|
Raises:
|
|
310
323
|
HomeAssistantAuthError: 401 response.
|
|
@@ -323,10 +336,11 @@ class HomeAssistantClient:
|
|
|
323
336
|
if response.status_code >= 400:
|
|
324
337
|
message, error_data = self._error_message_from_response(response)
|
|
325
338
|
|
|
326
|
-
|
|
327
|
-
response.status_code in
|
|
328
|
-
and
|
|
329
|
-
)
|
|
339
|
+
retryable = (
|
|
340
|
+
response.status_code in _RETRYABLE_GATEWAY_STATUS
|
|
341
|
+
and method.upper() in _SAFE_METHODS
|
|
342
|
+
)
|
|
343
|
+
if retryable and attempt < _MAX_REQUEST_ATTEMPTS:
|
|
330
344
|
logger.warning(
|
|
331
345
|
f"Transient {response.status_code} from Home Assistant "
|
|
332
346
|
f"(attempt {attempt}/{_MAX_REQUEST_ATTEMPTS}), retrying "
|
|
@@ -1211,6 +1225,28 @@ class HomeAssistantClient:
|
|
|
1211
1225
|
logger.debug(f"Starting config flow for handler: {handler}")
|
|
1212
1226
|
return await self._request("POST", "/config/config_entries/flow", json=payload)
|
|
1213
1227
|
|
|
1228
|
+
async def start_reconfigure_flow(
|
|
1229
|
+
self, handler: str, entry_id: str
|
|
1230
|
+
) -> dict[str, Any]:
|
|
1231
|
+
"""Start Home Assistant's official reconfigure flow for an entry.
|
|
1232
|
+
|
|
1233
|
+
Home Assistant selects ``SOURCE_RECONFIGURE`` when ``entry_id`` is
|
|
1234
|
+
included in the config-flow start payload. The integration's
|
|
1235
|
+
``async_step_reconfigure`` then owns validation and updates the
|
|
1236
|
+
existing entry in place; this method deliberately does not edit
|
|
1237
|
+
storage or delete/recreate entries.
|
|
1238
|
+
"""
|
|
1239
|
+
logger.debug(
|
|
1240
|
+
"Starting reconfigure flow for handler %s and entry %s",
|
|
1241
|
+
handler,
|
|
1242
|
+
entry_id,
|
|
1243
|
+
)
|
|
1244
|
+
return await self._request(
|
|
1245
|
+
"POST",
|
|
1246
|
+
"/config/config_entries/flow",
|
|
1247
|
+
json={"handler": handler, "entry_id": entry_id},
|
|
1248
|
+
)
|
|
1249
|
+
|
|
1214
1250
|
async def submit_config_flow_step(
|
|
1215
1251
|
self, flow_id: str, user_input: dict[str, Any]
|
|
1216
1252
|
) -> dict[str, Any]:
|
|
@@ -1228,6 +1264,10 @@ class HomeAssistantClient:
|
|
|
1228
1264
|
HomeAssistantAPIError: If flow submission fails
|
|
1229
1265
|
"""
|
|
1230
1266
|
logger.debug(f"Submitting flow step for flow_id: {flow_id}")
|
|
1267
|
+
# POST, so no gateway retry — see _RETRYABLE_GATEWAY_STATUS. This is
|
|
1268
|
+
# the call that motivated the rule: HA consumes the flow_id on success,
|
|
1269
|
+
# so a replay returns 404 "Invalid flow specified", a definitive-looking
|
|
1270
|
+
# 4xx that hides a first attempt which may already have committed.
|
|
1231
1271
|
return await self._request(
|
|
1232
1272
|
"POST", f"/config/config_entries/flow/{flow_id}", json=user_input
|
|
1233
1273
|
)
|
|
@@ -1382,6 +1422,43 @@ class HomeAssistantClient:
|
|
|
1382
1422
|
}
|
|
1383
1423
|
)
|
|
1384
1424
|
|
|
1425
|
+
async def list_config_entries(self) -> list[dict[str, Any]]:
|
|
1426
|
+
"""List all config entries from Home Assistant."""
|
|
1427
|
+
logger.debug("Listing Home Assistant config entries")
|
|
1428
|
+
entries: Any = await self._request("GET", "/config/config_entries/entry")
|
|
1429
|
+
if not isinstance(entries, list):
|
|
1430
|
+
raise HomeAssistantAPIError(
|
|
1431
|
+
"Unexpected response format from config entries API",
|
|
1432
|
+
status_code=500,
|
|
1433
|
+
)
|
|
1434
|
+
return [dict(entry) for entry in entries if isinstance(entry, dict)]
|
|
1435
|
+
|
|
1436
|
+
async def _list_registry(self, ws_type: str, label: str) -> list[dict[str, Any]]:
|
|
1437
|
+
"""Read a registry list, raising rather than returning a partial one.
|
|
1438
|
+
|
|
1439
|
+
Callers use "empty" to mean the entry genuinely has nothing registered,
|
|
1440
|
+
so a malformed response must never degrade to [].
|
|
1441
|
+
"""
|
|
1442
|
+
response = await self.send_websocket_message({"type": ws_type})
|
|
1443
|
+
result: Any = response.get("result") if isinstance(response, dict) else None
|
|
1444
|
+
if not isinstance(result, list) or not all(
|
|
1445
|
+
isinstance(item, dict) for item in result
|
|
1446
|
+
):
|
|
1447
|
+
detail = response.get("error") if isinstance(response, dict) else response
|
|
1448
|
+
raise HomeAssistantAPIError(
|
|
1449
|
+
f"Unexpected response from {label} registry API: {detail!r}",
|
|
1450
|
+
status_code=500,
|
|
1451
|
+
)
|
|
1452
|
+
return [dict(item) for item in result]
|
|
1453
|
+
|
|
1454
|
+
async def list_entity_registry(self) -> list[dict[str, Any]]:
|
|
1455
|
+
"""List Home Assistant's entity registry through the official WebSocket API."""
|
|
1456
|
+
return await self._list_registry("config/entity_registry/list", "entity")
|
|
1457
|
+
|
|
1458
|
+
async def list_device_registry(self) -> list[dict[str, Any]]:
|
|
1459
|
+
"""List Home Assistant's device registry through the official WebSocket API."""
|
|
1460
|
+
return await self._list_registry("config/device_registry/list", "device")
|
|
1461
|
+
|
|
1385
1462
|
async def get_config_entry(self, entry_id: str) -> dict[str, Any]:
|
|
1386
1463
|
"""
|
|
1387
1464
|
Get config entry details.
|
|
@@ -1393,25 +1470,17 @@ class HomeAssistantClient:
|
|
|
1393
1470
|
entry_id: Config entry ID
|
|
1394
1471
|
|
|
1395
1472
|
Returns:
|
|
1396
|
-
|
|
1473
|
+
Home Assistant's config-entry fragment (``ConfigEntry.as_json_fragment``):
|
|
1474
|
+
identity, state and capability flags. It carries NO ``data`` key, so the
|
|
1475
|
+
entry's connection settings and credentials are not in it.
|
|
1397
1476
|
|
|
1398
1477
|
Raises:
|
|
1399
1478
|
HomeAssistantAPIError: If entry not found or API error
|
|
1400
1479
|
"""
|
|
1401
1480
|
logger.debug(f"Getting config entry: {entry_id}")
|
|
1402
|
-
|
|
1403
|
-
# Typed as Any because _request returns dict[str, Any] generically,
|
|
1404
|
-
# but this endpoint actually returns a list.
|
|
1405
|
-
entries: Any = await self._request("GET", "/config/config_entries/entry")
|
|
1406
|
-
|
|
1407
|
-
if not isinstance(entries, list):
|
|
1408
|
-
raise HomeAssistantAPIError(
|
|
1409
|
-
"Unexpected response format from config entries API",
|
|
1410
|
-
status_code=500,
|
|
1411
|
-
)
|
|
1412
|
-
|
|
1481
|
+
entries = await self.list_config_entries()
|
|
1413
1482
|
found: dict[str, Any] | None = next(
|
|
1414
|
-
(
|
|
1483
|
+
(entry for entry in entries if entry.get("entry_id") == entry_id), None
|
|
1415
1484
|
)
|
|
1416
1485
|
if found is None:
|
|
1417
1486
|
raise HomeAssistantAPIError(
|
|
@@ -255,7 +255,11 @@ async def _backup_prep_via_component(
|
|
|
255
255
|
if not component_supports(caps, "backup_prep"):
|
|
256
256
|
return None
|
|
257
257
|
try:
|
|
258
|
-
ws = await get_websocket_client(
|
|
258
|
+
ws = await get_websocket_client(
|
|
259
|
+
url=client.base_url,
|
|
260
|
+
token=client.token,
|
|
261
|
+
verify_ssl=getattr(client, "verify_ssl", None),
|
|
262
|
+
)
|
|
259
263
|
raw = await ws.send_command(WS_BACKUP_PREP)
|
|
260
264
|
except (HomeAssistantCommandError, HomeAssistantCommandTimeout) as exc:
|
|
261
265
|
if is_unknown_command(exc):
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""Config-entry reads over the ``ha_mcp_tools`` component gate.
|
|
2
|
+
|
|
3
|
+
Exists for one field: ``unique_id``. Home Assistant deliberately withholds it
|
|
4
|
+
from every config-entry endpoint — the REST list, ``config_entries/get`` and
|
|
5
|
+
``config_entries/get_single`` all serialize ``ConfigEntry.as_json_fragment``,
|
|
6
|
+
which has no ``unique_id`` key — so a server that wants it as an identity
|
|
7
|
+
anchor has no source but the component, which holds the live ``ConfigEntry``.
|
|
8
|
+
|
|
9
|
+
The component's ``config_entries`` command returns that field as a deliberate
|
|
10
|
+
superset of the fragment. This module owns the caps-gated fetch, following the
|
|
11
|
+
routing discipline the sibling component modules established: probe caps, send
|
|
12
|
+
one frame, invalidate on ``unknown_command``, fall back on any component error.
|
|
13
|
+
|
|
14
|
+
**Three states, and callers must keep them apart.** ``unique_id`` is not a
|
|
15
|
+
two-valued field here:
|
|
16
|
+
|
|
17
|
+
- **known, with a value** — the component answered and the entry has one.
|
|
18
|
+
- **known to be absent** — the component answered and the entry has none
|
|
19
|
+
(MQTT-style entries genuinely have no unique_id). The key is present, the
|
|
20
|
+
value is ``None``.
|
|
21
|
+
- **unknown** — no component, an older component predating the field, or a
|
|
22
|
+
transport failure. Nothing can be concluded.
|
|
23
|
+
|
|
24
|
+
:func:`fetch_config_entry_unique_id` returns exactly that distinction so a
|
|
25
|
+
caller never mistakes "we could not read it" for "the entry has none". That is
|
|
26
|
+
what lets the field be additive within ``schema_version`` 1 with no version
|
|
27
|
+
gate: an older component omits the KEY, which is distinguishable from sending
|
|
28
|
+
it as ``None`` — the same discipline as ``device_get``'s opt-in entities join.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import logging
|
|
34
|
+
from typing import Any, NamedTuple
|
|
35
|
+
|
|
36
|
+
from ..client.rest_client import (
|
|
37
|
+
HomeAssistantCommandError,
|
|
38
|
+
HomeAssistantCommandTimeout,
|
|
39
|
+
)
|
|
40
|
+
from ..client.websocket_client import get_websocket_client
|
|
41
|
+
from .component_api import (
|
|
42
|
+
component_supports,
|
|
43
|
+
get_component_caps,
|
|
44
|
+
invalidate_caps,
|
|
45
|
+
is_unknown_command,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
logger = logging.getLogger(__name__)
|
|
49
|
+
|
|
50
|
+
WS_CONFIG_ENTRIES = "ha_mcp_tools/config_entries"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class EntryUniqueId(NamedTuple):
|
|
54
|
+
"""A config entry's ``unique_id``, and whether it could be read at all."""
|
|
55
|
+
|
|
56
|
+
#: False when no component answered, or the component predates the field.
|
|
57
|
+
known: bool
|
|
58
|
+
#: The value when ``known``; always ``None`` when not.
|
|
59
|
+
value: str | None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
UNKNOWN_UNIQUE_ID = EntryUniqueId(known=False, value=None)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
async def _config_entry_rows(client: Any, **query: Any) -> list[dict[str, Any]] | None:
|
|
66
|
+
"""One ``config_entries`` read; ``None`` when the answer cannot be trusted."""
|
|
67
|
+
caps = await get_component_caps(client)
|
|
68
|
+
if not component_supports(caps, "config_entries"):
|
|
69
|
+
return None
|
|
70
|
+
try:
|
|
71
|
+
ws = await get_websocket_client(
|
|
72
|
+
url=client.base_url,
|
|
73
|
+
token=client.token,
|
|
74
|
+
# verify_ssl keys the client pool, so omitting it hands back a
|
|
75
|
+
# DIFFERENT pooled client than the capability probe used and a
|
|
76
|
+
# self-signed setup fails the read after passing detection.
|
|
77
|
+
verify_ssl=getattr(client, "verify_ssl", None),
|
|
78
|
+
)
|
|
79
|
+
raw = await ws.send_command(WS_CONFIG_ENTRIES, **query)
|
|
80
|
+
except (HomeAssistantCommandError, HomeAssistantCommandTimeout) as exc:
|
|
81
|
+
if is_unknown_command(exc):
|
|
82
|
+
invalidate_caps(client)
|
|
83
|
+
else:
|
|
84
|
+
logger.warning("%s failed: %r", WS_CONFIG_ENTRIES, exc)
|
|
85
|
+
return None
|
|
86
|
+
except Exception as exc:
|
|
87
|
+
logger.warning("%s connection error: %r", WS_CONFIG_ENTRIES, exc)
|
|
88
|
+
return None
|
|
89
|
+
result = raw.get("result") if isinstance(raw, dict) else None
|
|
90
|
+
entries = result.get("entries") if isinstance(result, dict) else None
|
|
91
|
+
if not isinstance(entries, list):
|
|
92
|
+
return None
|
|
93
|
+
if any(not isinstance(row, dict) for row in entries):
|
|
94
|
+
logger.debug("%s returned a malformed row", WS_CONFIG_ENTRIES)
|
|
95
|
+
return None
|
|
96
|
+
return list(entries)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
async def fetch_domain_unique_ids(client: Any, domain: str) -> dict[str, str] | None:
|
|
100
|
+
"""Map ``entry_id -> unique_id`` for one domain, or ``None`` if unreadable.
|
|
101
|
+
|
|
102
|
+
One read, not one per candidate. The duplicate scan needs the unique_id of
|
|
103
|
+
every same-domain entry, and Home Assistant's own config-entry rows carry
|
|
104
|
+
none — so without this the scan can only ever compare entry_ids and must
|
|
105
|
+
not claim it checked unique_ids.
|
|
106
|
+
|
|
107
|
+
A genuinely ``None`` ``unique_id`` is the ONLY value omitted from the map,
|
|
108
|
+
so a present key always means a real value. Anything malformed returns
|
|
109
|
+
``None`` for the whole map instead: the duplicate scan reads a returned map
|
|
110
|
+
as a COMPLETED check, so an incomplete one could drop the very duplicate
|
|
111
|
+
row it exists to catch.
|
|
112
|
+
"""
|
|
113
|
+
rows = await _config_entry_rows(client, domain=domain)
|
|
114
|
+
if rows is None:
|
|
115
|
+
return None
|
|
116
|
+
mapping: dict[str, str] = {}
|
|
117
|
+
for row in rows:
|
|
118
|
+
if "unique_id" not in row:
|
|
119
|
+
# An older component: the field is missing wholesale.
|
|
120
|
+
logger.debug(
|
|
121
|
+
"%s rows for domain %s predate the unique_id field",
|
|
122
|
+
WS_CONFIG_ENTRIES,
|
|
123
|
+
domain,
|
|
124
|
+
)
|
|
125
|
+
return None
|
|
126
|
+
entry_id = row.get("entry_id")
|
|
127
|
+
unique_id = row["unique_id"]
|
|
128
|
+
if not isinstance(entry_id, str) or (
|
|
129
|
+
unique_id is not None and not isinstance(unique_id, str)
|
|
130
|
+
):
|
|
131
|
+
# A partial map would be read as a COMPLETED scan by the duplicate
|
|
132
|
+
# check, so a malformed duplicate row could be dropped and let
|
|
133
|
+
# verification pass. Incomplete must be indistinguishable from
|
|
134
|
+
# unreadable.
|
|
135
|
+
logger.debug(
|
|
136
|
+
"%s returned a malformed row for domain %s; map unusable",
|
|
137
|
+
WS_CONFIG_ENTRIES,
|
|
138
|
+
domain,
|
|
139
|
+
)
|
|
140
|
+
return None
|
|
141
|
+
# unique_id None is a valid answer (the entry has none) and is the one
|
|
142
|
+
# value legitimately omitted from the map.
|
|
143
|
+
if unique_id is not None:
|
|
144
|
+
mapping[entry_id] = unique_id
|
|
145
|
+
return mapping
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
async def fetch_config_entry_unique_id(client: Any, entry_id: str) -> EntryUniqueId:
|
|
149
|
+
"""Read one config entry's ``unique_id`` through the component.
|
|
150
|
+
|
|
151
|
+
Returns :data:`UNKNOWN_UNIQUE_ID` whenever the answer cannot be trusted —
|
|
152
|
+
no ``config_entries`` capability, a downgraded component, a transport
|
|
153
|
+
failure, a malformed payload, or a row that predates the field. Never
|
|
154
|
+
raises: an unreadable ``unique_id`` degrades the anchor, and the caller
|
|
155
|
+
decides what that means, rather than failing the whole reconfigure here.
|
|
156
|
+
"""
|
|
157
|
+
caps = await get_component_caps(client)
|
|
158
|
+
if not component_supports(caps, "config_entries"):
|
|
159
|
+
return UNKNOWN_UNIQUE_ID
|
|
160
|
+
try:
|
|
161
|
+
ws = await get_websocket_client(
|
|
162
|
+
url=client.base_url,
|
|
163
|
+
token=client.token,
|
|
164
|
+
# verify_ssl keys the client pool, so omitting it hands back a
|
|
165
|
+
# DIFFERENT pooled client than the capability probe used and a
|
|
166
|
+
# self-signed setup fails the read after passing detection.
|
|
167
|
+
verify_ssl=getattr(client, "verify_ssl", None),
|
|
168
|
+
)
|
|
169
|
+
raw = await ws.send_command(WS_CONFIG_ENTRIES, entry_id=entry_id)
|
|
170
|
+
except (HomeAssistantCommandError, HomeAssistantCommandTimeout) as exc:
|
|
171
|
+
if is_unknown_command(exc):
|
|
172
|
+
invalidate_caps(client)
|
|
173
|
+
else:
|
|
174
|
+
logger.warning(
|
|
175
|
+
"%s failed; unique_id unavailable: %r", WS_CONFIG_ENTRIES, exc
|
|
176
|
+
)
|
|
177
|
+
return UNKNOWN_UNIQUE_ID
|
|
178
|
+
except Exception as exc:
|
|
179
|
+
logger.warning(
|
|
180
|
+
"%s connection error; unique_id unavailable: %r", WS_CONFIG_ENTRIES, exc
|
|
181
|
+
)
|
|
182
|
+
return UNKNOWN_UNIQUE_ID
|
|
183
|
+
|
|
184
|
+
result = raw.get("result") if isinstance(raw, dict) else None
|
|
185
|
+
entries = result.get("entries") if isinstance(result, dict) else None
|
|
186
|
+
if not isinstance(entries, list) or not entries:
|
|
187
|
+
logger.debug(
|
|
188
|
+
"%s returned no row for %s; unique_id unavailable",
|
|
189
|
+
WS_CONFIG_ENTRIES,
|
|
190
|
+
entry_id,
|
|
191
|
+
)
|
|
192
|
+
return UNKNOWN_UNIQUE_ID
|
|
193
|
+
row = entries[0]
|
|
194
|
+
# Key presence is the capability probe: a component older than the field
|
|
195
|
+
# omits it entirely, which must not read as "this entry has no unique_id".
|
|
196
|
+
if not isinstance(row, dict) or "unique_id" not in row:
|
|
197
|
+
logger.debug(
|
|
198
|
+
"%s row for %s predates the unique_id field; unique_id unavailable",
|
|
199
|
+
WS_CONFIG_ENTRIES,
|
|
200
|
+
entry_id,
|
|
201
|
+
)
|
|
202
|
+
return UNKNOWN_UNIQUE_ID
|
|
203
|
+
if row.get("entry_id") != entry_id:
|
|
204
|
+
# Reading another entry's anchor would be worse than reading none.
|
|
205
|
+
logger.debug(
|
|
206
|
+
"%s answered for %r when %r was asked; unique_id unavailable",
|
|
207
|
+
WS_CONFIG_ENTRIES,
|
|
208
|
+
row.get("entry_id"),
|
|
209
|
+
entry_id,
|
|
210
|
+
)
|
|
211
|
+
return UNKNOWN_UNIQUE_ID
|
|
212
|
+
value = row["unique_id"]
|
|
213
|
+
if value is not None and not isinstance(value, str):
|
|
214
|
+
# Malformed is unknown, NOT known-absent: silently turning junk into
|
|
215
|
+
# "this entry has no unique_id" is the exact conflation this type
|
|
216
|
+
# exists to prevent.
|
|
217
|
+
logger.debug(
|
|
218
|
+
"%s returned a non-string unique_id (%r) for %s; unique_id unavailable",
|
|
219
|
+
WS_CONFIG_ENTRIES,
|
|
220
|
+
value,
|
|
221
|
+
entry_id,
|
|
222
|
+
)
|
|
223
|
+
return UNKNOWN_UNIQUE_ID
|
|
224
|
+
return EntryUniqueId(known=True, value=value)
|
{ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/component_config_reads.py
RENAMED
|
@@ -117,7 +117,11 @@ async def fetch_entity_lookup_via_component(
|
|
|
117
117
|
if domain is not None:
|
|
118
118
|
kwargs["domain"] = domain
|
|
119
119
|
try:
|
|
120
|
-
ws = await get_websocket_client(
|
|
120
|
+
ws = await get_websocket_client(
|
|
121
|
+
url=client.base_url,
|
|
122
|
+
token=client.token,
|
|
123
|
+
verify_ssl=getattr(client, "verify_ssl", None),
|
|
124
|
+
)
|
|
121
125
|
raw = await ws.send_command(WS_ENTITY_LOOKUP, **kwargs)
|
|
122
126
|
except (HomeAssistantCommandError, HomeAssistantCommandTimeout) as exc:
|
|
123
127
|
if is_unknown_command(exc):
|
|
@@ -177,7 +181,11 @@ async def fetch_reference_data_via_component(
|
|
|
177
181
|
if not include_states:
|
|
178
182
|
kwargs["include_states"] = False
|
|
179
183
|
try:
|
|
180
|
-
ws = await get_websocket_client(
|
|
184
|
+
ws = await get_websocket_client(
|
|
185
|
+
url=client.base_url,
|
|
186
|
+
token=client.token,
|
|
187
|
+
verify_ssl=getattr(client, "verify_ssl", None),
|
|
188
|
+
)
|
|
181
189
|
raw = await ws.send_command(WS_REFERENCE_DATA, **kwargs)
|
|
182
190
|
except (HomeAssistantCommandError, HomeAssistantCommandTimeout) as exc:
|
|
183
191
|
if is_unknown_command(exc):
|
|
@@ -91,7 +91,14 @@ async def fetch_device_via_component(
|
|
|
91
91
|
if include_entities:
|
|
92
92
|
kwargs["include_entities"] = True
|
|
93
93
|
try:
|
|
94
|
-
ws = await get_websocket_client(
|
|
94
|
+
ws = await get_websocket_client(
|
|
95
|
+
url=client.base_url,
|
|
96
|
+
token=client.token,
|
|
97
|
+
# verify_ssl keys the client pool, so omitting it hands back a
|
|
98
|
+
# DIFFERENT pooled client than the capability probe used and a
|
|
99
|
+
# self-signed setup fails the read after passing detection.
|
|
100
|
+
verify_ssl=getattr(client, "verify_ssl", None),
|
|
101
|
+
)
|
|
95
102
|
raw = await ws.send_command(WS_DEVICE_GET, **kwargs)
|
|
96
103
|
except (HomeAssistantCommandError, HomeAssistantCommandTimeout) as exc:
|
|
97
104
|
if is_unknown_command(exc):
|
|
@@ -163,7 +170,14 @@ async def fetch_device_list_via_component(client: Any) -> dict[str, Any] | None:
|
|
|
163
170
|
if not component_supports(caps, "device_list"):
|
|
164
171
|
return None
|
|
165
172
|
try:
|
|
166
|
-
ws = await get_websocket_client(
|
|
173
|
+
ws = await get_websocket_client(
|
|
174
|
+
url=client.base_url,
|
|
175
|
+
token=client.token,
|
|
176
|
+
# verify_ssl keys the client pool, so omitting it hands back a
|
|
177
|
+
# DIFFERENT pooled client than the capability probe used and a
|
|
178
|
+
# self-signed setup fails the read after passing detection.
|
|
179
|
+
verify_ssl=getattr(client, "verify_ssl", None),
|
|
180
|
+
)
|
|
167
181
|
raw = await ws.send_command(WS_DEVICE_LIST)
|
|
168
182
|
except (HomeAssistantCommandError, HomeAssistantCommandTimeout) as exc:
|
|
169
183
|
if is_unknown_command(exc):
|
{ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/component_registries.py
RENAMED
|
@@ -117,7 +117,14 @@ async def fetch_registries_via_component(
|
|
|
117
117
|
if category_scopes:
|
|
118
118
|
kwargs["category_scopes"] = list(category_scopes)
|
|
119
119
|
try:
|
|
120
|
-
ws = await get_websocket_client(
|
|
120
|
+
ws = await get_websocket_client(
|
|
121
|
+
url=client.base_url,
|
|
122
|
+
token=client.token,
|
|
123
|
+
# verify_ssl keys the client pool, so omitting it hands back a
|
|
124
|
+
# DIFFERENT pooled client than the capability probe used and a
|
|
125
|
+
# self-signed setup fails the read after passing detection.
|
|
126
|
+
verify_ssl=getattr(client, "verify_ssl", None),
|
|
127
|
+
)
|
|
121
128
|
raw = await ws.send_command(WS_REGISTRIES, **kwargs)
|
|
122
129
|
except (HomeAssistantCommandError, HomeAssistantCommandTimeout) as exc:
|
|
123
130
|
if is_unknown_command(exc):
|
{ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/component_registry_lookup.py
RENAMED
|
@@ -85,7 +85,14 @@ async def fetch_entities_for_config_entry_via_component(
|
|
|
85
85
|
if not component_supports(caps, "registry_lookup"):
|
|
86
86
|
return None
|
|
87
87
|
try:
|
|
88
|
-
ws = await get_websocket_client(
|
|
88
|
+
ws = await get_websocket_client(
|
|
89
|
+
url=client.base_url,
|
|
90
|
+
token=client.token,
|
|
91
|
+
# verify_ssl keys the client pool, so omitting it hands back a
|
|
92
|
+
# DIFFERENT pooled client than the capability probe used and a
|
|
93
|
+
# self-signed setup fails the read after passing detection.
|
|
94
|
+
verify_ssl=getattr(client, "verify_ssl", None),
|
|
95
|
+
)
|
|
89
96
|
raw = await ws.send_command(WS_REGISTRY_LOOKUP, config_entry_id=config_entry_id)
|
|
90
97
|
except (HomeAssistantCommandError, HomeAssistantCommandTimeout) as exc:
|
|
91
98
|
if is_unknown_command(exc):
|
|
@@ -133,7 +140,14 @@ async def resolve_entities_via_component(
|
|
|
133
140
|
if not component_supports(caps, "registry_lookup"):
|
|
134
141
|
return None
|
|
135
142
|
try:
|
|
136
|
-
ws = await get_websocket_client(
|
|
143
|
+
ws = await get_websocket_client(
|
|
144
|
+
url=client.base_url,
|
|
145
|
+
token=client.token,
|
|
146
|
+
# verify_ssl keys the client pool, so omitting it hands back a
|
|
147
|
+
# DIFFERENT pooled client than the capability probe used and a
|
|
148
|
+
# self-signed setup fails the read after passing detection.
|
|
149
|
+
verify_ssl=getattr(client, "verify_ssl", None),
|
|
150
|
+
)
|
|
137
151
|
raw = await ws.send_command(WS_REGISTRY_LOOKUP, entity_ids=entity_ids)
|
|
138
152
|
except (HomeAssistantCommandError, HomeAssistantCommandTimeout) as exc:
|
|
139
153
|
if is_unknown_command(exc):
|
|
@@ -11,9 +11,12 @@ for the 17 helper types listed in FLOW_HELPER_TYPES.
|
|
|
11
11
|
|
|
12
12
|
The same flow walkers drive every other config-entry surface, not just
|
|
13
13
|
helpers: ``ha_set_integration`` creates entries for arbitrary domains through
|
|
14
|
-
``create_config_entry`` and edits them through ``update_config_entry_options
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
``create_config_entry`` and edits them through ``update_config_entry_options``;
|
|
15
|
+
``ha_config_set_helper(helper_type="config_subentry")`` drives subentry flows
|
|
16
|
+
through ``set_config_subentry``. Changing a live entry's connection settings is
|
|
17
|
+
a different problem — it commits in place and Home Assistant reloads afterwards
|
|
18
|
+
— and lives in ``config_entry_reconfigure``, which depends on this module for
|
|
19
|
+
the shared flow-abort and sentinel-rejection helpers.
|
|
17
20
|
|
|
18
21
|
The step machinery those entry points drive lives in three sibling modules,
|
|
19
22
|
imported in one direction only (menu <- form <- walker <- here):
|
|
@@ -25,13 +28,17 @@ imported in one direction only (menu <- form <- walker <- here):
|
|
|
25
28
|
"""
|
|
26
29
|
|
|
27
30
|
import asyncio
|
|
31
|
+
import json
|
|
28
32
|
import logging
|
|
29
33
|
from typing import Any, Literal
|
|
30
34
|
|
|
35
|
+
from fastmcp.exceptions import ToolError
|
|
36
|
+
|
|
31
37
|
from ..errors import ErrorCode, create_error_response
|
|
32
38
|
from ..redaction import sentinel_option_keys
|
|
33
39
|
from .config_entry_flow_form import _extract_schema_field_names
|
|
34
40
|
from .config_entry_flow_walker import (
|
|
41
|
+
POST_COMMIT_STATUSES,
|
|
35
42
|
_FlowType,
|
|
36
43
|
_handle_config_subentry_flow_steps,
|
|
37
44
|
_handle_flow_steps,
|
|
@@ -41,6 +48,26 @@ from .helpers import raise_tool_error
|
|
|
41
48
|
logger = logging.getLogger(__name__)
|
|
42
49
|
|
|
43
50
|
|
|
51
|
+
async def _abort_flow_best_effort(client: Any, flow_id: str) -> None:
|
|
52
|
+
"""Abort a still-pending flow without hiding the original failure."""
|
|
53
|
+
try:
|
|
54
|
+
await asyncio.wait_for(client.abort_config_flow(flow_id), timeout=5.0)
|
|
55
|
+
except Exception as abort_err:
|
|
56
|
+
logger.warning("Failed to abort flow %s after error: %s", flow_id, abort_err)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def _abort_subentry_flow_best_effort(client: Any, flow_id: str) -> None:
|
|
60
|
+
"""Abort a pending subentry flow without hiding the original failure."""
|
|
61
|
+
try:
|
|
62
|
+
await asyncio.wait_for(client.abort_config_subentry_flow(flow_id), timeout=5.0)
|
|
63
|
+
except Exception as abort_err:
|
|
64
|
+
logger.warning(
|
|
65
|
+
"Failed to abort config subentry flow %s after error: %s",
|
|
66
|
+
flow_id,
|
|
67
|
+
abort_err,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
44
71
|
def _reject_redaction_sentinels(config_dict: dict[str, Any]) -> None:
|
|
45
72
|
"""Reject config values that are redaction placeholders (#2157).
|
|
46
73
|
|
|
@@ -138,6 +165,11 @@ async def set_config_subentry(
|
|
|
138
165
|
subentry, provided reconfigures that existing subentry.
|
|
139
166
|
``show_advanced_options`` is a no-op on HA 2026.6+ and kept only for older
|
|
140
167
|
HA versions pending removal before HA 2027.6.
|
|
168
|
+
|
|
169
|
+
The reconfigure branch fails when the flow leaves any supplied config key
|
|
170
|
+
unconsumed, where it previously returned success plus a warning — see
|
|
171
|
+
:func:`_handle_config_subentry_flow_steps` for why. The create branch is
|
|
172
|
+
unchanged.
|
|
141
173
|
"""
|
|
142
174
|
_reject_redaction_sentinels(config_dict)
|
|
143
175
|
flow_result = await client.start_config_subentry_flow(
|
|
@@ -174,17 +206,21 @@ async def set_config_subentry(
|
|
|
174
206
|
config_dict,
|
|
175
207
|
is_reconfigure=subentry_id is not None,
|
|
176
208
|
)
|
|
177
|
-
except
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
)
|
|
209
|
+
except asyncio.CancelledError:
|
|
210
|
+
await _abort_subentry_flow_best_effort(client, flow_id)
|
|
211
|
+
raise
|
|
212
|
+
except Exception as flow_error:
|
|
213
|
+
payload: dict[str, Any] = {}
|
|
214
|
+
if isinstance(flow_error, ToolError):
|
|
215
|
+
try:
|
|
216
|
+
parsed_payload = json.loads(str(flow_error))
|
|
217
|
+
except (TypeError, ValueError):
|
|
218
|
+
parsed_payload = {}
|
|
219
|
+
if isinstance(parsed_payload, dict):
|
|
220
|
+
payload = parsed_payload
|
|
221
|
+
post_commit_status = payload.get("status") in POST_COMMIT_STATUSES
|
|
222
|
+
if payload.get("flow_budget_exhausted") or not post_commit_status:
|
|
223
|
+
await _abort_subentry_flow_best_effort(client, flow_id)
|
|
188
224
|
raise
|
|
189
225
|
|
|
190
226
|
response = {
|