ha-mcp-dev 8.1.1.dev2289__tar.gz → 8.1.1.dev2292__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.1.1.dev2289/src/ha_mcp_dev.egg-info → ha_mcp_dev-8.1.1.dev2292}/PKG-INFO +1 -1
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/pyproject.toml +1 -1
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/backup_manager.py +5 -4
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_config_dashboards.py +237 -84
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/LICENSE +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/MANIFEST.in +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/README.md +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/setup.cfg +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/__init__.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/requirements.txt +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/LICENSE +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/MANIFEST.sha256 +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/VENDORED +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/__init__.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/__main__.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/asyncio/__init__.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/asyncio/client.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/asyncio/connection.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/asyncio/messages.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/asyncio/router.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/asyncio/server.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/auth.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/cli.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/client.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/connection.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/datastructures.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/exceptions.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/extensions/__init__.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/extensions/base.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/extensions/permessage_deflate.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/frames.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/headers.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/http11.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/imports.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/legacy/__init__.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/legacy/auth.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/legacy/client.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/legacy/exceptions.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/legacy/framing.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/legacy/handshake.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/legacy/http.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/legacy/protocol.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/legacy/server.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/protocol.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/proxy.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/py.typed +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/server.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/streams.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/sync/__init__.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/sync/client.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/sync/connection.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/sync/messages.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/sync/router.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/sync/server.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/sync/utils.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/trio/__init__.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/trio/client.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/trio/connection.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/trio/messages.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/trio/router.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/trio/server.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/trio/utils.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/typing.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/uri.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/utils.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/version.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/browser_landing.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/dashboard_screenshot/theme_guard.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/hacs_auto_refresh.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/llm_exposure.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/policy/__init__.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/policy/approval_queue.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/policy/editing.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/policy/evaluator.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/policy/handlers.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/policy/middleware.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/policy/model.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/policy/persistence.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/policy/value_sources.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/read_only.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/redaction.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/__init__.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/_i18n.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/_persistence.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/_theme.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/locales/README.md +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/locales/de.json +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/locales/en.json +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/locales/es.json +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/locales/fr.json +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/locales/it.json +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/locales/nl.json +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/locales/pl.json +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/locales/ru.json +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/locales/sv.json +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/locales/zh-Hans.json +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/settings.css +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/settings.html +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/settings.js +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/strict_bps.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/auto_backup.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/best_practice_result.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/best_practice_variables.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/component_api.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/component_config_reads.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/component_devices.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/component_registries.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/component_registry_lookup.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/config_entry_flow.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/config_entry_flow_form.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/config_entry_flow_menu.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/config_entry_flow_walker.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/hacs_registration.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/radio/__init__.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/radio/base.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/radio/matter.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/radio/thread.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/radio/zigbee.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/radio/zwave.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/smart_search/_base.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/smart_search/_config.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_dev.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_radio.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_security_policy.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_themes.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/validation_middleware.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/transport_security.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/update_check.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/utils/config_write_lock.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/utils/skill_loader.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/visibility/__init__.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/visibility/enforcement.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/visibility/model.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/visibility/persistence.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/visibility/resolver.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/tests/__init__.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/tests/test_constants.py +0 -0
- {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/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.1.1.
|
|
7
|
+
version = "8.1.1.dev2292"
|
|
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
|
|
@@ -1441,10 +1441,11 @@ async def _fetch_dashboard(client: Any, entity_id: str) -> Any:
|
|
|
1441
1441
|
try:
|
|
1442
1442
|
config, _config_hash = await _get_dashboard_config_internal(client, fetch_path)
|
|
1443
1443
|
except ToolError as err:
|
|
1444
|
-
# ToolError carries the structured failure payload
|
|
1445
|
-
#
|
|
1446
|
-
#
|
|
1447
|
-
# specified" is HA's message for
|
|
1444
|
+
# ToolError carries the structured failure payload, including HA's
|
|
1445
|
+
# preserved ``config_not_found`` code; treat a missing/unknown
|
|
1446
|
+
# dashboard as "nothing to back up" (also covers a brand-new dashboard
|
|
1447
|
+
# on the create path). "Unknown config specified" is HA's message for
|
|
1448
|
+
# an unresolved url_path.
|
|
1448
1449
|
msg = str(err).lower()
|
|
1449
1450
|
if "not_found" in msg or "config_not_found" in msg or "unknown config" in msg:
|
|
1450
1451
|
return None
|
{ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_config_dashboards.py
RENAMED
|
@@ -8,7 +8,7 @@ import json
|
|
|
8
8
|
import logging
|
|
9
9
|
import re
|
|
10
10
|
from dataclasses import dataclass, replace
|
|
11
|
-
from typing import Annotated, Any, Literal, cast, overload
|
|
11
|
+
from typing import Annotated, Any, Literal, NoReturn, cast, overload
|
|
12
12
|
|
|
13
13
|
from fastmcp.exceptions import ToolError
|
|
14
14
|
from fastmcp.tools import tool
|
|
@@ -40,7 +40,7 @@ from ..dashboard_screenshot.paths import (
|
|
|
40
40
|
match_dashboard_view,
|
|
41
41
|
resolve_dashboard_view,
|
|
42
42
|
)
|
|
43
|
-
from ..errors import ErrorCode, create_error_response
|
|
43
|
+
from ..errors import ErrorCode, create_error_response, get_error_code, get_error_message
|
|
44
44
|
from ..strict_bps import BestPracticeKeyParam
|
|
45
45
|
from ..utils.config_hash import compute_config_hash
|
|
46
46
|
from ..utils.python_sandbox import (
|
|
@@ -134,8 +134,9 @@ async def _get_dashboard_config_internal(
|
|
|
134
134
|
``tools_config_scenes.py``).
|
|
135
135
|
|
|
136
136
|
Raises ``ToolError`` with ``ErrorCode.SERVICE_CALL_FAILED`` if the
|
|
137
|
-
WebSocket call reports failure or the response is not a dict
|
|
138
|
-
|
|
137
|
+
WebSocket call reports failure or the response is not a dict. HA's
|
|
138
|
+
upstream error code is preserved as ``ha_error_code`` when present, so
|
|
139
|
+
callers can distinguish confirmed absence from an unverifiable read.
|
|
139
140
|
"""
|
|
140
141
|
get_data: dict[str, Any] = {"type": "lovelace/config", "force": True}
|
|
141
142
|
if url_path:
|
|
@@ -144,14 +145,20 @@ async def _get_dashboard_config_internal(
|
|
|
144
145
|
response = await client.send_websocket_message(get_data)
|
|
145
146
|
|
|
146
147
|
if isinstance(response, dict) and not response.get("success", True):
|
|
147
|
-
error_msg = response
|
|
148
|
-
if
|
|
149
|
-
error_msg =
|
|
148
|
+
error_msg = get_error_message(response)
|
|
149
|
+
if error_msg is None:
|
|
150
|
+
error_msg = str(response.get("error", {}))
|
|
151
|
+
ha_error_code = response.get("error_code") or get_error_code(response)
|
|
152
|
+
error_context: dict[str, Any] = {"url_path": url_path}
|
|
153
|
+
if ha_error_code is not None:
|
|
154
|
+
# Preserve HA's code so downstream safety decisions do not have to
|
|
155
|
+
# infer an absent config from a multiply-prefixed message alone.
|
|
156
|
+
error_context["ha_error_code"] = str(ha_error_code)
|
|
150
157
|
raise_tool_error(
|
|
151
158
|
create_error_response(
|
|
152
159
|
ErrorCode.SERVICE_CALL_FAILED,
|
|
153
160
|
f"Dashboard fetch failed: {error_msg}",
|
|
154
|
-
context=
|
|
161
|
+
context=error_context,
|
|
155
162
|
)
|
|
156
163
|
)
|
|
157
164
|
|
|
@@ -772,12 +779,39 @@ def _card_matches(
|
|
|
772
779
|
# regresses silently to never firing — re-verify with major HA upgrades.
|
|
773
780
|
_LAZY_RESOLVE_TRIGGER = "Unknown config specified"
|
|
774
781
|
|
|
782
|
+
# Exact ``lovelace/config`` messages meaning that a storage dashboard exists
|
|
783
|
+
# but has not been given a config yet. Mirrors smart_search/_deep.py and stays
|
|
784
|
+
# deliberately narrower than ``_LAZY_RESOLVE_TRIGGER``: an unknown path or any
|
|
785
|
+
# other pre-read failure must remain fail-closed before a full replacement.
|
|
786
|
+
_NO_STORED_CONFIG_MESSAGES = frozenset(
|
|
787
|
+
{"No config found.", "Command failed: No config found."}
|
|
788
|
+
)
|
|
789
|
+
_DASHBOARD_FETCH_FAILED_PREFIX = "Dashboard fetch failed: "
|
|
790
|
+
_HA_CONFIG_NOT_FOUND_CODE = "config_not_found"
|
|
791
|
+
|
|
775
792
|
|
|
776
793
|
def _should_lazy_resolve(error_msg: str) -> bool:
|
|
777
794
|
"""Return True if a WS error message indicates the identifier needs resolving."""
|
|
778
795
|
return _LAZY_RESOLVE_TRIGGER in error_msg
|
|
779
796
|
|
|
780
797
|
|
|
798
|
+
def _is_no_stored_dashboard_config_error(exc: ToolError) -> bool:
|
|
799
|
+
"""Return whether HA confirms that an existing dashboard has no config."""
|
|
800
|
+
try:
|
|
801
|
+
error_data = json.loads(str(exc))
|
|
802
|
+
except (json.JSONDecodeError, TypeError):
|
|
803
|
+
return False
|
|
804
|
+
if (
|
|
805
|
+
not isinstance(error_data, dict)
|
|
806
|
+
or error_data.get("ha_error_code") != _HA_CONFIG_NOT_FOUND_CODE
|
|
807
|
+
):
|
|
808
|
+
return False
|
|
809
|
+
message = extract_tool_error_message(exc).removeprefix(
|
|
810
|
+
_DASHBOARD_FETCH_FAILED_PREFIX
|
|
811
|
+
)
|
|
812
|
+
return message in _NO_STORED_CONFIG_MESSAGES
|
|
813
|
+
|
|
814
|
+
|
|
781
815
|
# The ha_mcp_tools/dashboards WS command: list / get / search over the live
|
|
782
816
|
# lovelace collection in one in-process frame. Named once so the routing helpers
|
|
783
817
|
# and their tests agree on the wire string (Global-Constraint-2 idiom, mirroring
|
|
@@ -1262,6 +1296,22 @@ async def fetch_dashboards_list(
|
|
|
1262
1296
|
return None
|
|
1263
1297
|
|
|
1264
1298
|
|
|
1299
|
+
def _raise_dashboard_registry_read_error(*, action: str, url_path: str) -> NoReturn:
|
|
1300
|
+
"""Raise the shared user-facing error for an unreadable dashboard registry."""
|
|
1301
|
+
raise_tool_error(
|
|
1302
|
+
create_error_response(
|
|
1303
|
+
ErrorCode.SERVICE_CALL_FAILED,
|
|
1304
|
+
"Failed to read the Home Assistant dashboard registry",
|
|
1305
|
+
suggestions=[
|
|
1306
|
+
"Retry the operation",
|
|
1307
|
+
"Check the Home Assistant connection and logs",
|
|
1308
|
+
"Use ha_config_get_dashboard(list_only=True) to verify registry access",
|
|
1309
|
+
],
|
|
1310
|
+
context={"action": action, "url_path": url_path},
|
|
1311
|
+
)
|
|
1312
|
+
)
|
|
1313
|
+
|
|
1314
|
+
|
|
1265
1315
|
async def _resolve_dashboard(
|
|
1266
1316
|
client: Any, identifier: str
|
|
1267
1317
|
) -> tuple[dict[str, str] | None, list[dict[str, Any]] | None]:
|
|
@@ -2972,7 +3022,12 @@ class DashboardConfigTools:
|
|
|
2972
3022
|
async def _resolve_set_dashboard_url_path(
|
|
2973
3023
|
self, url_path: str
|
|
2974
3024
|
) -> tuple[str, str | None, list[dict[str, Any]] | None]:
|
|
2975
|
-
"""Validate
|
|
3025
|
+
"""Validate the set target and canonicalize supported dashboard identifiers.
|
|
3026
|
+
|
|
3027
|
+
Hyphenless input is allowed only when it exactly matches an existing
|
|
3028
|
+
dashboard's ``url_path`` (plus the built-in ``lovelace`` alias case).
|
|
3029
|
+
New dashboard paths must contain a hyphen. An internal-ID-only match may
|
|
3030
|
+
still canonicalize to a different, hyphenated existing ``url_path``.
|
|
2976
3031
|
|
|
2977
3032
|
Returns ``(url_path, pre_resolved_from, pre_fetched_dashboards)``:
|
|
2978
3033
|
``url_path`` is canonicalized (``"default"`` -> ``"lovelace"``, and an
|
|
@@ -3004,22 +3059,17 @@ class DashboardConfigTools:
|
|
|
3004
3059
|
if url_path == "default":
|
|
3005
3060
|
url_path = "lovelace"
|
|
3006
3061
|
|
|
3007
|
-
# Pre-resolve
|
|
3008
|
-
#
|
|
3009
|
-
#
|
|
3010
|
-
#
|
|
3062
|
+
# Pre-resolve a hyphenless dashboard identifier before enforcing the
|
|
3063
|
+
# exact-url-path exception below. Internal-ID support is preserved when
|
|
3064
|
+
# it canonicalizes to a hyphenated existing path, but an ID-only match
|
|
3065
|
+
# cannot exempt a different hyphenless path from validation.
|
|
3011
3066
|
#
|
|
3012
|
-
#
|
|
3013
|
-
#
|
|
3014
|
-
#
|
|
3015
|
-
#
|
|
3016
|
-
# hit the hyphen-validation error and known their input was
|
|
3017
|
-
# invalid; now the create-vs-update distinction depends on
|
|
3018
|
-
# whether the registry happens to contain a matching id.
|
|
3019
|
-
# We log the rewrite and surface the original identifier as
|
|
3020
|
-
# ``resolved_from`` on the success response so callers can
|
|
3021
|
-
# detect this redirect.
|
|
3067
|
+
# This is still a create-or-update API: a caller intending to create a
|
|
3068
|
+
# dashboard can target an existing one, either by exact url_path or by a
|
|
3069
|
+
# supported internal-ID rewrite. ``action`` reports create vs update;
|
|
3070
|
+
# ``resolved_from`` additionally reports only the latter rewrite.
|
|
3022
3071
|
pre_resolved_from: str | None = None
|
|
3072
|
+
exact_url_path_exists = False
|
|
3023
3073
|
# When the pre-resolver fires and finds a match, ``_resolve_dashboard``
|
|
3024
3074
|
# has already fetched ``lovelace/dashboards/list``. Capture that list
|
|
3025
3075
|
# so the existence-check site below can reuse it instead of paying
|
|
@@ -3027,29 +3077,37 @@ class DashboardConfigTools:
|
|
|
3027
3077
|
pre_fetched_dashboards: list[dict[str, Any]] | None = None
|
|
3028
3078
|
if "-" not in url_path and url_path != "lovelace":
|
|
3029
3079
|
resolved, dashboards = await _resolve_dashboard(self._client, url_path)
|
|
3080
|
+
if dashboards is None:
|
|
3081
|
+
_raise_dashboard_registry_read_error(action="set", url_path=url_path)
|
|
3030
3082
|
if resolved is not None and resolved["url_path"]:
|
|
3031
|
-
|
|
3032
|
-
url_path = resolved["url_path"]
|
|
3033
|
-
pre_resolved_from = original_url_path
|
|
3083
|
+
exact_url_path_exists = resolved["url_path"] == url_path
|
|
3034
3084
|
pre_fetched_dashboards = dashboards
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3085
|
+
if not exact_url_path_exists and "-" in resolved["url_path"]:
|
|
3086
|
+
original_url_path = url_path
|
|
3087
|
+
url_path = resolved["url_path"]
|
|
3088
|
+
pre_resolved_from = original_url_path
|
|
3089
|
+
logger.info(
|
|
3090
|
+
"ha_config_set_dashboard pre-resolver mapped %r -> %r",
|
|
3091
|
+
original_url_path,
|
|
3092
|
+
url_path,
|
|
3093
|
+
)
|
|
3040
3094
|
|
|
3041
|
-
#
|
|
3095
|
+
# Existing exact url_path values may predate HA's creation rule and must
|
|
3096
|
+
# remain valid update targets unchanged.
|
|
3042
3097
|
# The built-in "lovelace" dashboard is exempt since it already exists
|
|
3043
|
-
if "-" not in url_path and url_path != "lovelace":
|
|
3098
|
+
if not exact_url_path_exists and "-" not in url_path and url_path != "lovelace":
|
|
3099
|
+
suggestions = [
|
|
3100
|
+
"Use format like 'my-dashboard' or 'mobile-view'",
|
|
3101
|
+
"Use 'lovelace' or 'default' to edit the default dashboard",
|
|
3102
|
+
]
|
|
3103
|
+
hyphenated_url_path = url_path.replace("_", "-")
|
|
3104
|
+
if hyphenated_url_path != url_path:
|
|
3105
|
+
suggestions.insert(0, f"Try '{hyphenated_url_path}' instead")
|
|
3044
3106
|
raise_tool_error(
|
|
3045
3107
|
create_error_response(
|
|
3046
3108
|
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
3047
3109
|
"url_path must contain a hyphen (-)",
|
|
3048
|
-
suggestions=
|
|
3049
|
-
f"Try '{url_path.replace('_', '-')}' instead",
|
|
3050
|
-
"Use format like 'my-dashboard' or 'mobile-view'",
|
|
3051
|
-
"Use 'lovelace' or 'default' to edit the default dashboard",
|
|
3052
|
-
],
|
|
3110
|
+
suggestions=suggestions,
|
|
3053
3111
|
context={"action": "set", "url_path": url_path},
|
|
3054
3112
|
)
|
|
3055
3113
|
)
|
|
@@ -3102,6 +3160,7 @@ class DashboardConfigTools:
|
|
|
3102
3160
|
url_path: str, python_transform: str, current_config: dict[str, Any]
|
|
3103
3161
|
) -> dict[str, Any]:
|
|
3104
3162
|
"""Run ``python_transform`` against ``current_config`` in the sandbox."""
|
|
3163
|
+
was_strategy_dashboard = "strategy" in current_config
|
|
3105
3164
|
try:
|
|
3106
3165
|
transformed_config = safe_execute(python_transform, current_config)
|
|
3107
3166
|
except PythonSandboxError as e:
|
|
@@ -3127,6 +3186,12 @@ class DashboardConfigTools:
|
|
|
3127
3186
|
context={"action": "python_transform", "url_path": url_path},
|
|
3128
3187
|
)
|
|
3129
3188
|
)
|
|
3189
|
+
DashboardConfigTools._validate_strategy_dashboard_replacement(
|
|
3190
|
+
url_path,
|
|
3191
|
+
was_strategy_dashboard=was_strategy_dashboard,
|
|
3192
|
+
replacement_config=transformed_config,
|
|
3193
|
+
action="python_transform",
|
|
3194
|
+
)
|
|
3130
3195
|
return transformed_config
|
|
3131
3196
|
|
|
3132
3197
|
async def _save_dashboard_python_transform(
|
|
@@ -3255,7 +3320,10 @@ class DashboardConfigTools:
|
|
|
3255
3320
|
if pre_fetched_dashboards is not None:
|
|
3256
3321
|
existing_dashboards = pre_fetched_dashboards
|
|
3257
3322
|
else:
|
|
3258
|
-
|
|
3323
|
+
fetched_dashboards = await fetch_dashboards_list(self._client)
|
|
3324
|
+
if fetched_dashboards is None:
|
|
3325
|
+
_raise_dashboard_registry_read_error(action="set", url_path=url_path)
|
|
3326
|
+
existing_dashboards = fetched_dashboards
|
|
3259
3327
|
dashboard_exists = any(
|
|
3260
3328
|
d.get("url_path") == url_path for d in existing_dashboards
|
|
3261
3329
|
)
|
|
@@ -3344,7 +3412,7 @@ class DashboardConfigTools:
|
|
|
3344
3412
|
) -> tuple[str | None, bool, str | None]:
|
|
3345
3413
|
"""Update metadata for an existing dashboard if any metadata params were provided.
|
|
3346
3414
|
|
|
3347
|
-
Returns ``(dashboard_id, metadata_updated,
|
|
3415
|
+
Returns ``(dashboard_id, metadata_updated, warning)``.
|
|
3348
3416
|
"""
|
|
3349
3417
|
dashboard_id = None
|
|
3350
3418
|
for dashboard in existing_dashboards:
|
|
@@ -3370,13 +3438,13 @@ class DashboardConfigTools:
|
|
|
3370
3438
|
if metadata_update_fields and dashboard_id is None:
|
|
3371
3439
|
# Dashboard ID not found in storage list (e.g. default lovelace on
|
|
3372
3440
|
# fresh installs). Metadata update via lovelace/dashboards/update
|
|
3373
|
-
# is not possible without a storage ID
|
|
3374
|
-
|
|
3441
|
+
# is not possible without a storage ID. The caller either proceeds
|
|
3442
|
+
# with a requested config write or rejects the resulting no-op.
|
|
3443
|
+
warning = (
|
|
3375
3444
|
"Metadata fields were provided but could not be applied: "
|
|
3376
|
-
"dashboard has no storage ID (likely the built-in default dashboard).
|
|
3377
|
-
"Config changes were still saved."
|
|
3445
|
+
"dashboard has no storage ID (likely the built-in default dashboard)."
|
|
3378
3446
|
)
|
|
3379
|
-
return dashboard_id, False,
|
|
3447
|
+
return dashboard_id, False, warning
|
|
3380
3448
|
return dashboard_id, False, None
|
|
3381
3449
|
|
|
3382
3450
|
async def _ensure_dashboard_exists(
|
|
@@ -3391,7 +3459,7 @@ class DashboardConfigTools:
|
|
|
3391
3459
|
) -> tuple[bool, str | None, bool, str | None]:
|
|
3392
3460
|
"""Create the dashboard if missing, else update its metadata if requested.
|
|
3393
3461
|
|
|
3394
|
-
Returns ``(dashboard_exists, dashboard_id, metadata_updated,
|
|
3462
|
+
Returns ``(dashboard_exists, dashboard_id, metadata_updated, warning)`` —
|
|
3395
3463
|
``dashboard_exists`` reflects state *before* this call, so the caller
|
|
3396
3464
|
can distinguish create vs update for the response's
|
|
3397
3465
|
``action``/``dashboard_created`` fields.
|
|
@@ -3409,7 +3477,7 @@ class DashboardConfigTools:
|
|
|
3409
3477
|
)
|
|
3410
3478
|
return dashboard_exists, dashboard_id, False, None
|
|
3411
3479
|
|
|
3412
|
-
dashboard_id, metadata_updated,
|
|
3480
|
+
dashboard_id, metadata_updated, warning = await self._update_dashboard_metadata(
|
|
3413
3481
|
url_path,
|
|
3414
3482
|
existing_dashboards,
|
|
3415
3483
|
title=title,
|
|
@@ -3417,43 +3485,56 @@ class DashboardConfigTools:
|
|
|
3417
3485
|
require_admin=require_admin,
|
|
3418
3486
|
show_in_sidebar=show_in_sidebar,
|
|
3419
3487
|
)
|
|
3420
|
-
return dashboard_exists, dashboard_id, metadata_updated,
|
|
3488
|
+
return dashboard_exists, dashboard_id, metadata_updated, warning
|
|
3421
3489
|
|
|
3422
3490
|
async def _check_dashboard_replace_hash(
|
|
3423
|
-
self,
|
|
3491
|
+
self,
|
|
3492
|
+
url_path: str,
|
|
3493
|
+
config_hash: str | None,
|
|
3494
|
+
replacement_config: dict[str, Any],
|
|
3424
3495
|
) -> str | None:
|
|
3425
|
-
"""
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3496
|
+
"""Validate replacement safety/hash and warn on large full-config replacement.
|
|
3497
|
+
|
|
3498
|
+
Omitting ``config_hash`` remains the force-replace path, but the current
|
|
3499
|
+
config must still be readable so strategy-dashboard protection can be
|
|
3500
|
+
evaluated before any replacement write. The one exception is an existing
|
|
3501
|
+
storage dashboard with no config yet and no supplied ``config_hash``:
|
|
3502
|
+
there is no strategy state to protect, so its first full config may be
|
|
3503
|
+
written. A supplied hash still conflicts with the now-absent config.
|
|
3429
3504
|
"""
|
|
3430
3505
|
try:
|
|
3431
3506
|
existing_config, existing_hash = await _get_dashboard_config_internal(
|
|
3432
3507
|
self._client, url_path
|
|
3433
3508
|
)
|
|
3434
|
-
except ToolError:
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
if not isinstance(existing_config, dict):
|
|
3441
|
-
return None
|
|
3442
|
-
|
|
3443
|
-
existing_config_size = len(json.dumps(existing_config))
|
|
3444
|
-
if config_hash is not None and existing_hash != config_hash:
|
|
3509
|
+
except ToolError as exc:
|
|
3510
|
+
if _is_no_stored_dashboard_config_error(exc):
|
|
3511
|
+
if config_hash is None:
|
|
3512
|
+
return None
|
|
3513
|
+
self._raise_dashboard_hash_conflict(url_path)
|
|
3445
3514
|
raise_tool_error(
|
|
3446
3515
|
create_error_response(
|
|
3447
3516
|
ErrorCode.SERVICE_CALL_FAILED,
|
|
3448
|
-
"
|
|
3517
|
+
"Cannot verify the current dashboard config before full replacement",
|
|
3518
|
+
details=extract_tool_error_message(exc),
|
|
3449
3519
|
suggestions=[
|
|
3450
|
-
"
|
|
3451
|
-
"
|
|
3520
|
+
"Retry the operation",
|
|
3521
|
+
"Read the dashboard with ha_config_get_dashboard first",
|
|
3452
3522
|
],
|
|
3453
3523
|
context={"action": "set", "url_path": url_path},
|
|
3454
3524
|
)
|
|
3455
3525
|
)
|
|
3456
3526
|
|
|
3527
|
+
existing_config_size = len(json.dumps(existing_config))
|
|
3528
|
+
if config_hash is not None and existing_hash != config_hash:
|
|
3529
|
+
self._raise_dashboard_hash_conflict(url_path)
|
|
3530
|
+
|
|
3531
|
+
self._validate_strategy_dashboard_replacement(
|
|
3532
|
+
url_path,
|
|
3533
|
+
was_strategy_dashboard="strategy" in existing_config,
|
|
3534
|
+
replacement_config=replacement_config,
|
|
3535
|
+
action="config",
|
|
3536
|
+
)
|
|
3537
|
+
|
|
3457
3538
|
if existing_config_size >= 10000:
|
|
3458
3539
|
return (
|
|
3459
3540
|
f"Replaced large config ({existing_config_size:,} bytes). "
|
|
@@ -3461,6 +3542,44 @@ class DashboardConfigTools:
|
|
|
3461
3542
|
)
|
|
3462
3543
|
return None
|
|
3463
3544
|
|
|
3545
|
+
@staticmethod
|
|
3546
|
+
def _raise_dashboard_hash_conflict(url_path: str) -> NoReturn:
|
|
3547
|
+
"""Raise the shared optimistic-lock conflict for a full replacement."""
|
|
3548
|
+
raise_tool_error(
|
|
3549
|
+
create_error_response(
|
|
3550
|
+
ErrorCode.SERVICE_CALL_FAILED,
|
|
3551
|
+
"Dashboard modified since last read (conflict)",
|
|
3552
|
+
suggestions=[
|
|
3553
|
+
"Call ha_config_get_dashboard() again",
|
|
3554
|
+
"Use the fresh config_hash, or omit config_hash to force replace",
|
|
3555
|
+
],
|
|
3556
|
+
context={"action": "set", "url_path": url_path},
|
|
3557
|
+
)
|
|
3558
|
+
)
|
|
3559
|
+
|
|
3560
|
+
@staticmethod
|
|
3561
|
+
def _validate_strategy_dashboard_replacement(
|
|
3562
|
+
url_path: str,
|
|
3563
|
+
*,
|
|
3564
|
+
was_strategy_dashboard: bool,
|
|
3565
|
+
replacement_config: dict[str, Any],
|
|
3566
|
+
action: str,
|
|
3567
|
+
) -> None:
|
|
3568
|
+
"""Prevent this tool from taking control of a strategy dashboard."""
|
|
3569
|
+
if not was_strategy_dashboard or "strategy" in replacement_config:
|
|
3570
|
+
return
|
|
3571
|
+
raise_tool_error(
|
|
3572
|
+
create_error_response(
|
|
3573
|
+
ErrorCode.VALIDATION_FAILED,
|
|
3574
|
+
"Strategy dashboards cannot be converted to custom dashboards via this tool",
|
|
3575
|
+
suggestions=[
|
|
3576
|
+
"Use 'Take Control' in the Home Assistant interface to convert it",
|
|
3577
|
+
"Keep a strategy configuration when updating this dashboard",
|
|
3578
|
+
],
|
|
3579
|
+
context={"action": action, "url_path": url_path},
|
|
3580
|
+
)
|
|
3581
|
+
)
|
|
3582
|
+
|
|
3464
3583
|
async def _save_dashboard_config(
|
|
3465
3584
|
self, url_path: str, config_dict: dict[str, Any]
|
|
3466
3585
|
) -> None:
|
|
@@ -3499,7 +3618,7 @@ class DashboardConfigTools:
|
|
|
3499
3618
|
) -> tuple[bool, str | None, dict[str, Any]]:
|
|
3500
3619
|
"""Parse + validate ``config`` and save it as a full replacement.
|
|
3501
3620
|
|
|
3502
|
-
Returns ``(config_updated,
|
|
3621
|
+
Returns ``(config_updated, warning, saved_config)``.
|
|
3503
3622
|
"""
|
|
3504
3623
|
parsed_config = parse_json_param(config, "config")
|
|
3505
3624
|
if parsed_config is None or not isinstance(parsed_config, dict):
|
|
@@ -3515,12 +3634,14 @@ class DashboardConfigTools:
|
|
|
3515
3634
|
)
|
|
3516
3635
|
config_dict = cast(dict[str, Any], parsed_config)
|
|
3517
3636
|
|
|
3518
|
-
|
|
3637
|
+
warning: str | None = None
|
|
3519
3638
|
if dashboard_exists:
|
|
3520
|
-
|
|
3639
|
+
warning = await self._check_dashboard_replace_hash(
|
|
3640
|
+
url_path, config_hash, config_dict
|
|
3641
|
+
)
|
|
3521
3642
|
|
|
3522
3643
|
await self._save_dashboard_config(url_path, config_dict)
|
|
3523
|
-
return True,
|
|
3644
|
+
return True, warning, config_dict
|
|
3524
3645
|
|
|
3525
3646
|
async def _run_dashboard_config_update(
|
|
3526
3647
|
self,
|
|
@@ -3543,7 +3664,7 @@ class DashboardConfigTools:
|
|
|
3543
3664
|
dashboard_exists,
|
|
3544
3665
|
dashboard_id,
|
|
3545
3666
|
metadata_updated,
|
|
3546
|
-
|
|
3667
|
+
metadata_warning,
|
|
3547
3668
|
) = await self._ensure_dashboard_exists(
|
|
3548
3669
|
url_path,
|
|
3549
3670
|
title=title,
|
|
@@ -3555,16 +3676,49 @@ class DashboardConfigTools:
|
|
|
3555
3676
|
|
|
3556
3677
|
config_updated = False
|
|
3557
3678
|
render_config: dict[str, Any] | None = None
|
|
3679
|
+
warnings: list[str] = []
|
|
3558
3680
|
if config is not None:
|
|
3559
3681
|
(
|
|
3560
3682
|
config_updated,
|
|
3561
|
-
|
|
3683
|
+
config_warning,
|
|
3562
3684
|
render_config,
|
|
3563
3685
|
) = await self._apply_dashboard_config(
|
|
3564
3686
|
url_path, config, config_hash, dashboard_exists
|
|
3565
3687
|
)
|
|
3566
|
-
if
|
|
3567
|
-
|
|
3688
|
+
if config_warning:
|
|
3689
|
+
warnings.append(config_warning)
|
|
3690
|
+
|
|
3691
|
+
if dashboard_exists and not config_updated and not metadata_updated:
|
|
3692
|
+
if metadata_warning is not None:
|
|
3693
|
+
raise_tool_error(
|
|
3694
|
+
create_error_response(
|
|
3695
|
+
ErrorCode.SERVICE_CALL_FAILED,
|
|
3696
|
+
"No dashboard changes were applied",
|
|
3697
|
+
details=metadata_warning,
|
|
3698
|
+
suggestions=[
|
|
3699
|
+
"Use a storage-mode dashboard when changing metadata",
|
|
3700
|
+
"Provide config to replace the dashboard configuration",
|
|
3701
|
+
],
|
|
3702
|
+
context={"action": "set", "url_path": url_path},
|
|
3703
|
+
)
|
|
3704
|
+
)
|
|
3705
|
+
raise_tool_error(
|
|
3706
|
+
create_error_response(
|
|
3707
|
+
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
3708
|
+
"No dashboard changes were requested",
|
|
3709
|
+
suggestions=[
|
|
3710
|
+
"Provide config or python_transform to change dashboard content",
|
|
3711
|
+
"Provide a metadata field such as title or icon",
|
|
3712
|
+
"Use ha_config_get_dashboard to read a dashboard without changing it",
|
|
3713
|
+
],
|
|
3714
|
+
context={"action": "set", "url_path": url_path},
|
|
3715
|
+
)
|
|
3716
|
+
)
|
|
3717
|
+
|
|
3718
|
+
if metadata_warning:
|
|
3719
|
+
if config_updated:
|
|
3720
|
+
metadata_warning = f"{metadata_warning} Dashboard config was saved."
|
|
3721
|
+
warnings.insert(0, metadata_warning)
|
|
3568
3722
|
|
|
3569
3723
|
result_dict: dict[str, Any] = {
|
|
3570
3724
|
"success": True,
|
|
@@ -3577,13 +3731,12 @@ class DashboardConfigTools:
|
|
|
3577
3731
|
"message": f"Dashboard {url_path} {'created' if not dashboard_exists else 'updated'} successfully",
|
|
3578
3732
|
}
|
|
3579
3733
|
|
|
3580
|
-
if
|
|
3581
|
-
result_dict["
|
|
3734
|
+
if warnings:
|
|
3735
|
+
result_dict["warnings"] = warnings
|
|
3582
3736
|
if pre_resolved_from is not None:
|
|
3583
|
-
#
|
|
3584
|
-
#
|
|
3585
|
-
#
|
|
3586
|
-
# an existing dashboard was updated instead.
|
|
3737
|
+
# This marks identifier canonicalization only. Exact url_path
|
|
3738
|
+
# matches deliberately omit it; callers use ``action`` to tell
|
|
3739
|
+
# whether the create-or-update operation updated an existing target.
|
|
3587
3740
|
result_dict["resolved_from"] = pre_resolved_from
|
|
3588
3741
|
|
|
3589
3742
|
render_config = await _attach_dashboard_render_paths_after_write(
|
|
@@ -3651,11 +3804,11 @@ class DashboardConfigTools:
|
|
|
3651
3804
|
context={"action": "delete"},
|
|
3652
3805
|
)
|
|
3653
3806
|
resolved, dashboards = await _resolve_dashboard(self._client, url_path)
|
|
3807
|
+
if dashboards is None:
|
|
3808
|
+
_raise_dashboard_registry_read_error(action="delete", url_path=url_path)
|
|
3654
3809
|
if resolved is None:
|
|
3655
3810
|
available_ids = [
|
|
3656
|
-
d.get("url_path")
|
|
3657
|
-
for d in (dashboards or [])[:10]
|
|
3658
|
-
if d.get("url_path")
|
|
3811
|
+
d.get("url_path") for d in dashboards[:10] if d.get("url_path")
|
|
3659
3812
|
]
|
|
3660
3813
|
raise_tool_error(
|
|
3661
3814
|
create_error_response(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/MANIFEST.sha256
RENAMED
|
File without changes
|
{ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/VENDORED
RENAMED
|
File without changes
|
{ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/__init__.py
RENAMED
|
File without changes
|
{ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/__main__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/client.py
RENAMED
|
File without changes
|
{ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/connection.py
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/exceptions.py
RENAMED
|
File without changes
|
|
File without changes
|