ha-mcp-dev 7.14.2.dev2093__tar.gz → 7.14.2.dev2098__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-7.14.2.dev2093/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.14.2.dev2098}/PKG-INFO +1 -1
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/pyproject.toml +1 -1
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/backup_manager.py +53 -22
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/dashboard_screenshot/paths.py +66 -40
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/_handlers_fs.py +20 -2
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/backup.py +5 -2
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/helpers.py +35 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_bug_report.py +153 -4
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_config_dashboards.py +165 -36
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_filesystem.py +79 -38
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/LICENSE +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/README.md +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/setup.cfg +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/browser_landing.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/dashboard_screenshot/theme_guard.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/llm_exposure.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/policy/__init__.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/policy/approval_queue.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/policy/evaluator.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/policy/handlers.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/policy/middleware.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/policy/model.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/policy/persistence.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/policy/value_sources.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/read_only.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/__init__.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/_i18n.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/_persistence.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/_theme.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/locales/README.md +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/locales/de.json +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/locales/en.json +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/locales/ru.json +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/locales/zh-Hans.json +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/settings.css +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/settings.html +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/settings.js +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/strict_bps.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/auto_backup.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/component_api.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/component_config_reads.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/component_devices.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/component_registries.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/component_registry_lookup.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/config_entry_flow.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/radio/__init__.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/radio/base.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/radio/matter.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/radio/thread.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/radio/zigbee.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/radio/zwave.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/smart_search/_base.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/smart_search/_config.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_dev.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_radio.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_themes.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/validation_middleware.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/transport_security.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/update_check.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/utils/skill_loader.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/visibility/__init__.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/visibility/model.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/visibility/persistence.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/visibility/resolver.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/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 = "7.14.2.
|
|
7
|
+
version = "7.14.2.dev2098"
|
|
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
|
|
@@ -766,16 +766,18 @@ class BackupManager:
|
|
|
766
766
|
|
|
767
767
|
# ----- legacy store (pre-#1579 .ha_mcp_tools_backups/) ---------------
|
|
768
768
|
|
|
769
|
-
async def list_legacy(self) -> list[dict[str, Any]]:
|
|
769
|
+
async def list_legacy(self) -> tuple[list[dict[str, Any]], str | None]:
|
|
770
770
|
"""List pre-#1579 ``.bak`` backups via the component service.
|
|
771
771
|
|
|
772
772
|
Each entry is normalized to a synthetic ``name`` (``legacy:<file>``)
|
|
773
773
|
plus ``source="legacy"`` and the decode hints (``file_path`` /
|
|
774
774
|
``path_ambiguous``) so the caller can route view/diff/restore and warn
|
|
775
|
-
on un-restorable (ambiguous) names. Returns ``
|
|
776
|
-
|
|
775
|
+
on un-restorable (ambiguous) names. Returns ``(entries,
|
|
776
|
+
unavailable_reason)``: entries is ``[]`` when the component is too
|
|
777
|
+
old, unconfigured, or absent — ``list`` still works — and the reason
|
|
778
|
+
(when set) lets the caller flag the omission.
|
|
777
779
|
"""
|
|
778
|
-
backups = await _list_legacy_backups(self._client)
|
|
780
|
+
backups, unavailable_reason = await _list_legacy_backups(self._client)
|
|
779
781
|
out: list[dict[str, Any]] = []
|
|
780
782
|
for b in backups:
|
|
781
783
|
filename = b.get("filename")
|
|
@@ -792,7 +794,7 @@ class BackupManager:
|
|
|
792
794
|
"path_ambiguous": b.get("path_ambiguous", True),
|
|
793
795
|
}
|
|
794
796
|
)
|
|
795
|
-
return out
|
|
797
|
+
return out, unavailable_reason
|
|
796
798
|
|
|
797
799
|
async def read_legacy(self, filename: str) -> dict[str, Any]:
|
|
798
800
|
"""Read one legacy ``.bak`` (raw content + decode hints).
|
|
@@ -815,7 +817,7 @@ class BackupManager:
|
|
|
815
817
|
domain: str | None = None,
|
|
816
818
|
entity_id: str | None = None,
|
|
817
819
|
limit: int | None = None,
|
|
818
|
-
) -> list[dict[str, Any]]:
|
|
820
|
+
) -> tuple[list[dict[str, Any]], list[str]]:
|
|
819
821
|
"""Edits-store snapshots plus pre-#1579 legacy ``.bak`` entries (#1579).
|
|
820
822
|
|
|
821
823
|
``list_snapshots`` is sync (dir glob, run off-thread); the legacy store
|
|
@@ -828,9 +830,22 @@ class BackupManager:
|
|
|
828
830
|
Legacy entries get reserved room within ``limit`` so a full edits store
|
|
829
831
|
(>= ``limit`` snapshots) can't truncate the few historical legacy
|
|
830
832
|
entries out of the listing — surfacing them is the whole point.
|
|
833
|
+
|
|
834
|
+
Returns ``(entries, warnings)``: when the legacy sub-fetch was
|
|
835
|
+
suppressed (tools entry not set up / component too old), ``warnings``
|
|
836
|
+
says so, so the tool response doesn't read as a clean, complete list
|
|
837
|
+
with the ``.bak`` history silently missing (#1996).
|
|
831
838
|
"""
|
|
832
839
|
want_legacy = domain in (None, "yaml_file") and entity_id is None
|
|
833
|
-
legacy
|
|
840
|
+
legacy: list[dict[str, Any]] = []
|
|
841
|
+
warnings: list[str] = []
|
|
842
|
+
if want_legacy:
|
|
843
|
+
legacy, unavailable_reason = await self.list_legacy()
|
|
844
|
+
if unavailable_reason:
|
|
845
|
+
warnings.append(
|
|
846
|
+
"Legacy .bak backups could not be listed and are omitted: "
|
|
847
|
+
f"{unavailable_reason}"
|
|
848
|
+
)
|
|
834
849
|
edits_limit = max(1, limit - len(legacy)) if limit and legacy else limit
|
|
835
850
|
entries = await asyncio.to_thread(
|
|
836
851
|
self.list_snapshots, domain=domain, entity_id=entity_id, limit=edits_limit
|
|
@@ -838,7 +853,7 @@ class BackupManager:
|
|
|
838
853
|
entries.extend(legacy)
|
|
839
854
|
if limit:
|
|
840
855
|
entries = entries[:limit]
|
|
841
|
-
return entries
|
|
856
|
+
return entries, warnings
|
|
842
857
|
|
|
843
858
|
async def _diff_legacy(self, filename: str) -> DiffResponseText:
|
|
844
859
|
info = await self.read_legacy(filename)
|
|
@@ -2173,30 +2188,37 @@ async def _restore_yaml_file(client: Any, entity_id: str, config: Any) -> Any:
|
|
|
2173
2188
|
return result
|
|
2174
2189
|
|
|
2175
2190
|
|
|
2176
|
-
async def _list_legacy_backups(client: Any) -> list[dict[str, Any]]:
|
|
2191
|
+
async def _list_legacy_backups(client: Any) -> tuple[list[dict[str, Any]], str | None]:
|
|
2177
2192
|
"""Fetch pre-#1579 ``.bak`` backups via the component list_legacy_backups
|
|
2178
2193
|
service.
|
|
2179
2194
|
|
|
2180
|
-
Returns ``
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2195
|
+
Returns ``(backups, unavailable_reason)``. The legacy store is
|
|
2196
|
+
best-effort: when the component predates the service, isn't configured
|
|
2197
|
+
(tools entry not set up), or is missing entirely, the entry list is ``[]``
|
|
2198
|
+
and ``unavailable_reason`` carries the human-readable cause — a
|
|
2199
|
+
``HomeAssistantError`` or any ``ToolError`` out of
|
|
2200
|
+
``call_mcp_tools_service`` (all its caller-token gates included) lands
|
|
2201
|
+
here (also logged at debug), so the edits ``list`` still works and the
|
|
2202
|
+
caller can surface the degradation as a warning. A ``success: False``
|
|
2203
|
+
service response degrades silently as before. Genuine programming errors
|
|
2204
|
+
propagate.
|
|
2184
2205
|
"""
|
|
2206
|
+
from .tools.helpers import extract_structured_error_reason
|
|
2185
2207
|
from .tools.tools_filesystem import call_mcp_tools_service
|
|
2186
2208
|
from .tools.util_helpers import unwrap_service_response
|
|
2187
2209
|
|
|
2188
2210
|
try:
|
|
2189
2211
|
result = await call_mcp_tools_service(client, "list_legacy_backups", {})
|
|
2190
|
-
except HomeAssistantError as err:
|
|
2212
|
+
except (HomeAssistantError, ToolError) as err:
|
|
2191
2213
|
logger.debug("legacy backup list unavailable: %s", err)
|
|
2192
|
-
return []
|
|
2214
|
+
return [], extract_structured_error_reason(err) or str(err)
|
|
2193
2215
|
if not isinstance(result, dict):
|
|
2194
|
-
return []
|
|
2216
|
+
return [], None
|
|
2195
2217
|
result = unwrap_service_response(result)
|
|
2196
2218
|
if not result.get("success", False):
|
|
2197
|
-
return []
|
|
2219
|
+
return [], None
|
|
2198
2220
|
backups = result.get("backups")
|
|
2199
|
-
return backups if isinstance(backups, list) else []
|
|
2221
|
+
return (backups, None) if isinstance(backups, list) else ([], None)
|
|
2200
2222
|
|
|
2201
2223
|
|
|
2202
2224
|
async def _read_legacy_backup(client: Any, filename: str) -> dict[str, Any]:
|
|
@@ -2204,9 +2226,11 @@ async def _read_legacy_backup(client: Any, filename: str) -> dict[str, Any]:
|
|
|
2204
2226
|
|
|
2205
2227
|
Returns the unwrapped service response (carries ``success`` / ``content`` /
|
|
2206
2228
|
``file_path`` / ``path_ambiguous`` / ``timestamp``). A service-unavailable
|
|
2207
|
-
``HomeAssistantError``
|
|
2208
|
-
surfaces a not-found
|
|
2229
|
+
``HomeAssistantError`` — or a ``ToolError`` from the caller-token gates —
|
|
2230
|
+
is mapped to a ``success: False`` dict so the caller surfaces a not-found
|
|
2231
|
+
rather than crashing.
|
|
2209
2232
|
"""
|
|
2233
|
+
from .tools.helpers import extract_structured_error_reason
|
|
2210
2234
|
from .tools.tools_filesystem import call_mcp_tools_service
|
|
2211
2235
|
from .tools.util_helpers import unwrap_service_response
|
|
2212
2236
|
|
|
@@ -2214,8 +2238,15 @@ async def _read_legacy_backup(client: Any, filename: str) -> dict[str, Any]:
|
|
|
2214
2238
|
result = await call_mcp_tools_service(
|
|
2215
2239
|
client, "read_legacy_backup", {"filename": filename}
|
|
2216
2240
|
)
|
|
2217
|
-
except HomeAssistantError as err:
|
|
2218
|
-
|
|
2241
|
+
except (HomeAssistantError, ToolError) as err:
|
|
2242
|
+
# The human reason, not str(err) verbatim: a ToolError's string is
|
|
2243
|
+
# the whole JSON envelope, which would reach the tool layer as an
|
|
2244
|
+
# unreadable JSON-in-JSON message (#1996's original symptom).
|
|
2245
|
+
logger.debug("legacy backup read unavailable: %s", err)
|
|
2246
|
+
return {
|
|
2247
|
+
"success": False,
|
|
2248
|
+
"error": extract_structured_error_reason(err) or str(err),
|
|
2249
|
+
}
|
|
2219
2250
|
if not isinstance(result, dict):
|
|
2220
2251
|
return {"success": False, "error": f"no response for {filename!r}"}
|
|
2221
2252
|
return unwrap_service_response(result)
|
{ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/dashboard_screenshot/paths.py
RENAMED
|
@@ -347,42 +347,28 @@ async def fetch_dashboard_render_config(
|
|
|
347
347
|
return cast(dict[str, Any], config)
|
|
348
348
|
|
|
349
349
|
|
|
350
|
-
def
|
|
350
|
+
def match_dashboard_view(
|
|
351
351
|
dashboard_url_path: str,
|
|
352
352
|
config: dict[str, Any],
|
|
353
|
-
view_path: str
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
"No view_path was supplied and the dashboard has no static views; "
|
|
373
|
-
"only the base route is available."
|
|
374
|
-
)
|
|
375
|
-
return DashboardRenderTarget(
|
|
376
|
-
dashboard_url_path=base_path,
|
|
377
|
-
view_path=None,
|
|
378
|
-
render_path=base_path,
|
|
379
|
-
view_index=None,
|
|
380
|
-
stable=False,
|
|
381
|
-
warnings=(warning,),
|
|
382
|
-
)
|
|
383
|
-
|
|
384
|
-
cleaned_view_path = view_path
|
|
385
|
-
if not cleaned_view_path.strip():
|
|
353
|
+
view_path: str,
|
|
354
|
+
*,
|
|
355
|
+
strategy_suggestions: tuple[str, ...] = (
|
|
356
|
+
"Use dashboard_path with the frontend route instead",
|
|
357
|
+
),
|
|
358
|
+
) -> tuple[int, dict[str, Any]]:
|
|
359
|
+
"""Match one named Lovelace view by its configured ``views[].path``.
|
|
360
|
+
|
|
361
|
+
Shared by screenshot view resolution and get-mode view scoping so both
|
|
362
|
+
honour identical semantics: exact match on the configured path, with
|
|
363
|
+
structured errors for an empty path, a strategy dashboard (no static
|
|
364
|
+
views to match), and a missing/ambiguous path (the not-found error lists
|
|
365
|
+
the available paths). Returns ``(view_index, view)``.
|
|
366
|
+
|
|
367
|
+
``strategy_suggestions`` lets each caller phrase the strategy-dashboard
|
|
368
|
+
remediation in its own vocabulary (the screenshot tools have a
|
|
369
|
+
``dashboard_path`` route parameter; config scoping does not).
|
|
370
|
+
"""
|
|
371
|
+
if not view_path.strip():
|
|
386
372
|
raise_tool_error(
|
|
387
373
|
create_error_response(
|
|
388
374
|
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
@@ -397,9 +383,9 @@ def resolve_dashboard_view(
|
|
|
397
383
|
"Strategy dashboards do not expose static named view paths.",
|
|
398
384
|
context={
|
|
399
385
|
"dashboard_url_path": dashboard_url_path,
|
|
400
|
-
"view_path":
|
|
386
|
+
"view_path": view_path,
|
|
401
387
|
},
|
|
402
|
-
suggestions=
|
|
388
|
+
suggestions=list(strategy_suggestions),
|
|
403
389
|
)
|
|
404
390
|
)
|
|
405
391
|
views = config.get("views")
|
|
@@ -408,7 +394,7 @@ def resolve_dashboard_view(
|
|
|
408
394
|
matches = [
|
|
409
395
|
(index, view)
|
|
410
396
|
for index, view in enumerate(views)
|
|
411
|
-
if _configured_view_path(view) ==
|
|
397
|
+
if _configured_view_path(view) == view_path
|
|
412
398
|
]
|
|
413
399
|
if len(matches) != 1:
|
|
414
400
|
available = [
|
|
@@ -420,17 +406,57 @@ def resolve_dashboard_view(
|
|
|
420
406
|
raise_tool_error(
|
|
421
407
|
create_error_response(
|
|
422
408
|
ErrorCode.RESOURCE_NOT_FOUND,
|
|
423
|
-
f"Dashboard view path '{
|
|
409
|
+
f"Dashboard view path '{view_path}' is {reason}.",
|
|
424
410
|
context={
|
|
425
411
|
"dashboard_url_path": dashboard_url_path,
|
|
426
|
-
"view_path":
|
|
412
|
+
"view_path": view_path,
|
|
427
413
|
"available_view_paths": available,
|
|
428
414
|
},
|
|
429
415
|
suggestions=["Use a view_path returned by ha_config_get_dashboard"],
|
|
430
416
|
)
|
|
431
417
|
)
|
|
418
|
+
return matches[0]
|
|
419
|
+
|
|
432
420
|
|
|
433
|
-
|
|
421
|
+
def resolve_dashboard_view(
|
|
422
|
+
dashboard_url_path: str,
|
|
423
|
+
config: dict[str, Any],
|
|
424
|
+
view_path: str | None,
|
|
425
|
+
) -> DashboardRenderTarget:
|
|
426
|
+
"""Resolve one named Lovelace view to a canonical frontend route."""
|
|
427
|
+
base_path = dashboard_frontend_path(dashboard_url_path)
|
|
428
|
+
if view_path is None:
|
|
429
|
+
views = config.get("views")
|
|
430
|
+
has_static_views = isinstance(views, list) and bool(views)
|
|
431
|
+
if "strategy" in config:
|
|
432
|
+
warning = (
|
|
433
|
+
"No view_path was supplied; this strategy dashboard generates "
|
|
434
|
+
"its views at runtime, so only the base route is available."
|
|
435
|
+
)
|
|
436
|
+
elif has_static_views:
|
|
437
|
+
warning = (
|
|
438
|
+
"No view_path was supplied; the dashboard base route renders "
|
|
439
|
+
"the first view visible to Puppet's Home Assistant user."
|
|
440
|
+
)
|
|
441
|
+
else:
|
|
442
|
+
warning = (
|
|
443
|
+
"No view_path was supplied and the dashboard has no static views; "
|
|
444
|
+
"only the base route is available."
|
|
445
|
+
)
|
|
446
|
+
return DashboardRenderTarget(
|
|
447
|
+
dashboard_url_path=base_path,
|
|
448
|
+
view_path=None,
|
|
449
|
+
render_path=base_path,
|
|
450
|
+
view_index=None,
|
|
451
|
+
stable=False,
|
|
452
|
+
warnings=(warning,),
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
view_index, _ = match_dashboard_view(dashboard_url_path, config, view_path)
|
|
456
|
+
cleaned_view_path = view_path
|
|
457
|
+
views = config.get("views")
|
|
458
|
+
if not isinstance(views, list):
|
|
459
|
+
views = []
|
|
434
460
|
render_path = _safe_named_render_path(base_path, cleaned_view_path)
|
|
435
461
|
if render_path is None:
|
|
436
462
|
return _fallback_view_target(
|
{ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/_handlers_fs.py
RENAMED
|
@@ -25,6 +25,20 @@ if TYPE_CHECKING:
|
|
|
25
25
|
logger = logging.getLogger(__name__)
|
|
26
26
|
|
|
27
27
|
|
|
28
|
+
def _component_error_reason(exc: Exception) -> str | None:
|
|
29
|
+
"""Extract the human message from a structured ``ToolError``, else None.
|
|
30
|
+
|
|
31
|
+
``raise_tool_error`` serializes the whole error envelope into the
|
|
32
|
+
exception string, so rendering ``str(exc)`` verbatim shows the settings
|
|
33
|
+
UI a raw JSON blob behind a misleading "could not reach" prefix — the
|
|
34
|
+
hard-to-read surface from #1996. Thin seam over the shared extractor
|
|
35
|
+
(imported lazily like the other tools imports in this module).
|
|
36
|
+
"""
|
|
37
|
+
from ..tools.helpers import extract_structured_error_reason
|
|
38
|
+
|
|
39
|
+
return extract_structured_error_reason(exc)
|
|
40
|
+
|
|
41
|
+
|
|
28
42
|
async def _fs_custom_paths_call(
|
|
29
43
|
server: HomeAssistantSmartMCPServer | None, service: str, data: dict[str, Any]
|
|
30
44
|
) -> Any:
|
|
@@ -90,7 +104,10 @@ async def _get_fs_custom_paths(
|
|
|
90
104
|
result = await _fs_custom_paths_call(server, "get_allowed_paths", {})
|
|
91
105
|
except Exception as exc:
|
|
92
106
|
logger.warning("fs-custom-paths GET could not reach ha_mcp_tools: %s", exc)
|
|
93
|
-
return _unavailable(
|
|
107
|
+
return _unavailable(
|
|
108
|
+
_component_error_reason(exc)
|
|
109
|
+
or f"Could not reach the ha_mcp_tools component: {exc}"
|
|
110
|
+
)
|
|
94
111
|
|
|
95
112
|
data = unwrap_service_response(result) if isinstance(result, dict) else {}
|
|
96
113
|
if not isinstance(data, dict) or not data.get("success", False):
|
|
@@ -163,7 +180,8 @@ async def _save_fs_custom_paths(
|
|
|
163
180
|
return JSONResponse(
|
|
164
181
|
create_error_response(
|
|
165
182
|
ErrorCode.SERVICE_CALL_FAILED,
|
|
166
|
-
|
|
183
|
+
_component_error_reason(exc)
|
|
184
|
+
or f"Could not reach the ha_mcp_tools component: {exc}",
|
|
167
185
|
),
|
|
168
186
|
status_code=502,
|
|
169
187
|
)
|
|
@@ -1908,12 +1908,12 @@ async def _edits_list(
|
|
|
1908
1908
|
# Edits-store snapshots + pre-#1579 legacy .bak entries (#1579).
|
|
1909
1909
|
# The manager owns the merge (sync dir-glob off-thread + async
|
|
1910
1910
|
# legacy service call) so this layer stays source-agnostic.
|
|
1911
|
-
entries = await mgr.list_edits_and_legacy(
|
|
1911
|
+
entries, warnings = await mgr.list_edits_and_legacy(
|
|
1912
1912
|
domain=domain,
|
|
1913
1913
|
entity_id=entity_id,
|
|
1914
1914
|
limit=limit,
|
|
1915
1915
|
)
|
|
1916
|
-
|
|
1916
|
+
response: dict[str, Any] = {
|
|
1917
1917
|
"success": True,
|
|
1918
1918
|
"data": {
|
|
1919
1919
|
"backups": entries,
|
|
@@ -1924,6 +1924,9 @@ async def _edits_list(
|
|
|
1924
1924
|
"retain_per_entity": settings.auto_backup_retain_per_entity,
|
|
1925
1925
|
},
|
|
1926
1926
|
}
|
|
1927
|
+
if warnings:
|
|
1928
|
+
response["warnings"] = warnings
|
|
1929
|
+
return response
|
|
1927
1930
|
|
|
1928
1931
|
|
|
1929
1932
|
async def _edits_view(
|
|
@@ -78,6 +78,41 @@ def extract_tool_error_message(te: ToolError) -> str:
|
|
|
78
78
|
return str(te)
|
|
79
79
|
|
|
80
80
|
|
|
81
|
+
def extract_structured_error_reason(exc: BaseException) -> str | None:
|
|
82
|
+
"""Extract "message" + first actionable suggestion from a structured error.
|
|
83
|
+
|
|
84
|
+
Like ``extract_tool_error_message`` but for surfaces that show the reason
|
|
85
|
+
to a human (settings UI, degraded-result warnings): it appends the first
|
|
86
|
+
suggestion when one exists, and returns ``None`` — instead of the raw
|
|
87
|
+
``str(exc)`` JSON envelope — when the payload is not a structured error
|
|
88
|
+
or carries no usable message, so callers pick their own fallback.
|
|
89
|
+
``create_error_response`` emits only the singular ``suggestion`` key for
|
|
90
|
+
one-suggestion errors (the plural ``suggestions`` list needs two or
|
|
91
|
+
more), so both keys are honored.
|
|
92
|
+
"""
|
|
93
|
+
try:
|
|
94
|
+
payload = json.loads(str(exc))
|
|
95
|
+
except (TypeError, ValueError):
|
|
96
|
+
return None
|
|
97
|
+
if not isinstance(payload, dict):
|
|
98
|
+
return None
|
|
99
|
+
error = payload.get("error")
|
|
100
|
+
if not isinstance(error, dict):
|
|
101
|
+
return None
|
|
102
|
+
message = error.get("message")
|
|
103
|
+
if not isinstance(message, str) or not message:
|
|
104
|
+
return None
|
|
105
|
+
suggestions = error.get("suggestions")
|
|
106
|
+
first = (
|
|
107
|
+
suggestions[0]
|
|
108
|
+
if isinstance(suggestions, list) and suggestions
|
|
109
|
+
else error.get("suggestion")
|
|
110
|
+
)
|
|
111
|
+
if isinstance(first, str) and first:
|
|
112
|
+
return f"{message} {first}"
|
|
113
|
+
return message
|
|
114
|
+
|
|
115
|
+
|
|
81
116
|
def validate_identifier_not_empty(
|
|
82
117
|
value: str | None,
|
|
83
118
|
param_name: str,
|