ha-mcp-dev 7.13.0.dev1977__tar.gz → 7.13.0.dev1983__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.13.0.dev1977/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.13.0.dev1983}/PKG-INFO +1 -1
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/pyproject.toml +1 -1
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/config.py +1 -1
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/dashboard_screenshot/__init__.py +6 -2
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/dashboard_screenshot/capture.py +158 -86
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/dashboard_screenshot/provision.py +62 -7
- ha_mcp_dev-7.13.0.dev1983/src/ha_mcp/dashboard_screenshot/theme_guard.py +228 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_config_dashboards.py +25 -9
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_dashboard_screenshot.py +29 -15
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp_dev.egg-info/SOURCES.txt +1 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/LICENSE +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/README.md +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/setup.cfg +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/backup_manager.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/browser_landing.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/llm_exposure.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/policy/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/policy/approval_queue.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/policy/evaluator.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/policy/handlers.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/policy/middleware.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/policy/model.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/policy/persistence.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/policy/value_sources.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/read_only.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/settings_ui/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/settings_ui/_i18n.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/settings_ui/_persistence.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/settings_ui/_theme.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/settings_ui/locales/README.md +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/settings_ui/locales/en.json +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/settings_ui/locales/ru.json +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/settings_ui/settings.css +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/settings_ui/settings.html +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/settings_ui/settings.js +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/strict_bps.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/auto_backup.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/component_api.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/component_config_reads.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/component_devices.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/component_registries.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/component_registry_lookup.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/config_entry_flow.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/radio/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/radio/base.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/radio/matter.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/radio/thread.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/radio/zigbee.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/radio/zwave.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/smart_search/_base.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/smart_search/_config.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_dev.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_radio.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_themes.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/validation_middleware.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/transport_security.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/update_check.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/utils/skill_loader.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/visibility/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/visibility/model.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/visibility/persistence.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/visibility/resolver.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/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.13.0.
|
|
7
|
+
version = "7.13.0.dev1983"
|
|
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
|
|
@@ -836,7 +836,7 @@ ADVANCED_SETTINGS_FIELDS: tuple[AdvancedField, ...] = (
|
|
|
836
836
|
AdvancedField("enable_websocket", "ENABLE_WEBSOCKET", bool, "operations", True),
|
|
837
837
|
# Dashboard-screenshot engine URL (#1538): docker/.env users could set
|
|
838
838
|
# HAMCP_DASHBOARD_SCREENSHOT_ENGINE_URL, but add-on users had no path to
|
|
839
|
-
# it. It is resolved live per capture (
|
|
839
|
+
# it. It is resolved live per capture (resolve_engine), so unlike the
|
|
840
840
|
# time budgets it takes effect without a restart. Blank = auto-discover
|
|
841
841
|
# the Puppet add-on via the Supervisor.
|
|
842
842
|
AdvancedField(
|
{ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/dashboard_screenshot/__init__.py
RENAMED
|
@@ -19,7 +19,8 @@ from .capture import (
|
|
|
19
19
|
DashboardImageCapture,
|
|
20
20
|
capture_dashboard_images,
|
|
21
21
|
)
|
|
22
|
-
from .provision import
|
|
22
|
+
from .provision import EngineTarget, resolve_engine
|
|
23
|
+
from .theme_guard import EngineCredential, ThemeGuard
|
|
23
24
|
|
|
24
25
|
__all__ = [
|
|
25
26
|
"DEFAULT_HEIGHT",
|
|
@@ -27,6 +28,9 @@ __all__ = [
|
|
|
27
28
|
"DEFAULT_WAIT_MS",
|
|
28
29
|
"DEFAULT_WIDTH",
|
|
29
30
|
"DashboardImageCapture",
|
|
31
|
+
"EngineCredential",
|
|
32
|
+
"EngineTarget",
|
|
33
|
+
"ThemeGuard",
|
|
30
34
|
"capture_dashboard_images",
|
|
31
|
-
"
|
|
35
|
+
"resolve_engine",
|
|
32
36
|
]
|
{ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/dashboard_screenshot/capture.py
RENAMED
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
The engine (balloob's Puppet add-on, or a docker-compose sidecar)
|
|
4
4
|
authenticates to Home Assistant with its OWN configured long-lived token, so
|
|
5
|
-
this client passes only the dashboard path + render parameters
|
|
6
|
-
ever
|
|
5
|
+
this client passes only the dashboard path + render parameters to the engine
|
|
6
|
+
— no HA token is ever sent to the engine or exposed to the LLM. The
|
|
7
|
+
:mod:`theme_guard` bracket around each capture batch may hold the engine's
|
|
8
|
+
token in server memory to restore the engine user's saved theme (issue
|
|
9
|
+
#1909); see that module's docstring for the containment rules.
|
|
7
10
|
"""
|
|
8
11
|
|
|
9
12
|
from __future__ import annotations
|
|
@@ -20,7 +23,8 @@ from fastmcp.exceptions import ToolError
|
|
|
20
23
|
|
|
21
24
|
from ..errors import ErrorCode, create_error_response
|
|
22
25
|
from ..tools.helpers import raise_tool_error
|
|
23
|
-
from .provision import TOKEN_HINT,
|
|
26
|
+
from .provision import TOKEN_HINT, resolve_engine
|
|
27
|
+
from .theme_guard import ThemeGuard
|
|
24
28
|
|
|
25
29
|
logger = logging.getLogger(__name__)
|
|
26
30
|
|
|
@@ -757,9 +761,30 @@ async def _request_or_collect_failure(
|
|
|
757
761
|
return None
|
|
758
762
|
|
|
759
763
|
|
|
764
|
+
def _viewport_params(
|
|
765
|
+
options: _CaptureOptions, viewport: _ViewportRequest
|
|
766
|
+
) -> dict[str, str]:
|
|
767
|
+
"""Build one viewport's engine query parameters."""
|
|
768
|
+
params: dict[str, str] = {
|
|
769
|
+
"viewport": f"{viewport.width}x{viewport.height}",
|
|
770
|
+
"zoom": str(options.zoom),
|
|
771
|
+
"wait": str(options.wait_ms),
|
|
772
|
+
"format": options.image_format,
|
|
773
|
+
}
|
|
774
|
+
if options.theme is not None:
|
|
775
|
+
params["theme"] = options.theme
|
|
776
|
+
if options.dark_mode:
|
|
777
|
+
# Puppet checks only for query-key presence.
|
|
778
|
+
params["dark"] = ""
|
|
779
|
+
if options.language is not None:
|
|
780
|
+
params["lang"] = options.language
|
|
781
|
+
return params
|
|
782
|
+
|
|
783
|
+
|
|
760
784
|
def _complete_capture_batch(
|
|
761
785
|
captures: list[DashboardImageCapture],
|
|
762
786
|
partial_failures: list[dict[str, Any]] | None,
|
|
787
|
+
guard_warnings: list[str],
|
|
763
788
|
) -> list[DashboardImageCapture]:
|
|
764
789
|
"""Return partial successes, but raise when every requested capture failed."""
|
|
765
790
|
if not captures and partial_failures:
|
|
@@ -769,10 +794,41 @@ def _complete_capture_batch(
|
|
|
769
794
|
"failure_count": len(partial_failures),
|
|
770
795
|
"screenshot_failures": partial_failures,
|
|
771
796
|
}
|
|
797
|
+
if guard_warnings:
|
|
798
|
+
aggregate_failure["warnings"] = [
|
|
799
|
+
*aggregate_failure.get("warnings", []),
|
|
800
|
+
*guard_warnings,
|
|
801
|
+
]
|
|
772
802
|
raise_tool_error(aggregate_failure)
|
|
773
803
|
return captures
|
|
774
804
|
|
|
775
805
|
|
|
806
|
+
def _reraise_with_guard_warnings(error: ToolError, warnings: list[str]) -> NoReturn:
|
|
807
|
+
"""Re-raise a capture ToolError with the theme guard's warnings attached.
|
|
808
|
+
|
|
809
|
+
A batch that raises may already have rendered (and clobbered the engine
|
|
810
|
+
user's theme) before failing, so a failed restore must stay visible on
|
|
811
|
+
the error path too — otherwise the tool contract ("an attempted but
|
|
812
|
+
failed restore surfaces as a warning") silently breaks exactly when the
|
|
813
|
+
theme is most likely left flipped.
|
|
814
|
+
"""
|
|
815
|
+
if not warnings:
|
|
816
|
+
raise error
|
|
817
|
+
try:
|
|
818
|
+
payload = json.loads(str(error))
|
|
819
|
+
except (json.JSONDecodeError, TypeError):
|
|
820
|
+
payload = None
|
|
821
|
+
if not isinstance(payload, dict):
|
|
822
|
+
raise error
|
|
823
|
+
existing = payload.get("warnings")
|
|
824
|
+
payload["warnings"] = [
|
|
825
|
+
*(existing if isinstance(existing, list) else []),
|
|
826
|
+
*warnings,
|
|
827
|
+
]
|
|
828
|
+
raise_tool_error(payload)
|
|
829
|
+
raise AssertionError("unreachable: raise_tool_error always raises")
|
|
830
|
+
|
|
831
|
+
|
|
776
832
|
async def capture_dashboard_images(
|
|
777
833
|
dashboard_path: str,
|
|
778
834
|
*,
|
|
@@ -789,6 +845,8 @@ async def capture_dashboard_images(
|
|
|
789
845
|
image_format: ScreenshotFormat = "png",
|
|
790
846
|
render_timeout_seconds: float = DEFAULT_RENDER_TIMEOUT_SECONDS,
|
|
791
847
|
partial_failures: list[dict[str, Any]] | None = None,
|
|
848
|
+
client: Any | None = None,
|
|
849
|
+
capture_warnings: list[str] | None = None,
|
|
792
850
|
) -> list[DashboardImageCapture]:
|
|
793
851
|
"""Render one or more ordered dashboard images via the screenshot engine.
|
|
794
852
|
|
|
@@ -796,6 +854,12 @@ async def capture_dashboard_images(
|
|
|
796
854
|
preset or custom dimensions when needed; omitting it preserves each
|
|
797
855
|
preset's native orientation. ``full_page`` is a compatibility alias for
|
|
798
856
|
requesting the engine's native ``WIDTHxauto`` viewport.
|
|
857
|
+
|
|
858
|
+
The batch is bracketed by a :class:`ThemeGuard` that restores the engine
|
|
859
|
+
user's saved frontend theme if rendering changed it (issue #1909).
|
|
860
|
+
``client`` supplies the guard's non-add-on credential fallback and
|
|
861
|
+
``capture_warnings`` collects the guard's non-fatal warnings; both are
|
|
862
|
+
optional and never affect the captures themselves.
|
|
799
863
|
"""
|
|
800
864
|
path = _validate_dashboard_path(dashboard_path)
|
|
801
865
|
options = validate_capture_parameters(
|
|
@@ -814,94 +878,102 @@ async def capture_dashboard_images(
|
|
|
814
878
|
)
|
|
815
879
|
viewports = _capture_viewports(options)
|
|
816
880
|
|
|
817
|
-
|
|
881
|
+
engine_target = await resolve_engine()
|
|
882
|
+
engine = engine_target.url
|
|
818
883
|
url = f"{engine}/{path}"
|
|
819
884
|
mime_type = _MIME_TYPES[options.image_format]
|
|
820
885
|
captures: list[DashboardImageCapture] = []
|
|
821
886
|
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
)
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
"limit_bytes": MAX_BATCH_PAYLOAD_BYTES,
|
|
864
|
-
},
|
|
887
|
+
# The engine dispatches a theme write into the authenticated frontend on
|
|
888
|
+
# cold renders, which Home Assistant persists to the engine user's real
|
|
889
|
+
# profile (issue #1909). Snapshot before, restore after — including when
|
|
890
|
+
# the batch fails, since the engine may already have rendered (and
|
|
891
|
+
# written) before the failure.
|
|
892
|
+
guard = ThemeGuard.for_capture(engine_target.addon_credential, client)
|
|
893
|
+
await guard.take_snapshot()
|
|
894
|
+
batch_error: ToolError | None = None
|
|
895
|
+
try:
|
|
896
|
+
async with httpx.AsyncClient(
|
|
897
|
+
timeout=httpx.Timeout(options.render_timeout_seconds)
|
|
898
|
+
) as http_client:
|
|
899
|
+
for capture_index, viewport in enumerate(viewports):
|
|
900
|
+
params = _viewport_params(options, viewport)
|
|
901
|
+
|
|
902
|
+
request_context = {
|
|
903
|
+
"path": path,
|
|
904
|
+
"preset": viewport.preset,
|
|
905
|
+
"width": viewport.width,
|
|
906
|
+
"height": viewport.height,
|
|
907
|
+
"requested_format": options.image_format,
|
|
908
|
+
"capture_index": capture_index,
|
|
909
|
+
"capture_count": len(viewports),
|
|
910
|
+
"completed_count": len(captures),
|
|
911
|
+
}
|
|
912
|
+
aggregate_bytes = sum(capture.size_bytes for capture in captures)
|
|
913
|
+
remaining_batch_bytes = MAX_BATCH_PAYLOAD_BYTES - aggregate_bytes
|
|
914
|
+
if remaining_batch_bytes <= 0:
|
|
915
|
+
assert partial_failures is not None
|
|
916
|
+
partial_failures.append(
|
|
917
|
+
create_error_response(
|
|
918
|
+
ErrorCode.IMAGE_PAYLOAD_TOO_LARGE,
|
|
919
|
+
"Screenshot image batch reached the server's safe "
|
|
920
|
+
"inline-image limit.",
|
|
921
|
+
context={
|
|
922
|
+
**request_context,
|
|
923
|
+
"aggregate_bytes_before_capture": aggregate_bytes,
|
|
924
|
+
"limit_kind": "batch",
|
|
925
|
+
"limit_bytes": MAX_BATCH_PAYLOAD_BYTES,
|
|
926
|
+
},
|
|
927
|
+
)
|
|
865
928
|
)
|
|
866
|
-
)
|
|
867
|
-
break
|
|
868
|
-
|
|
869
|
-
outcome = await _request_or_collect_failure(
|
|
870
|
-
http_client,
|
|
871
|
-
url=url,
|
|
872
|
-
path=path,
|
|
873
|
-
engine=engine,
|
|
874
|
-
params=params,
|
|
875
|
-
options=options,
|
|
876
|
-
viewport=viewport,
|
|
877
|
-
mime_type=mime_type,
|
|
878
|
-
request_context=request_context,
|
|
879
|
-
aggregate_bytes=aggregate_bytes,
|
|
880
|
-
remaining_batch_bytes=remaining_batch_bytes,
|
|
881
|
-
partial_failures=partial_failures,
|
|
882
|
-
)
|
|
883
|
-
if outcome is None:
|
|
884
|
-
if (
|
|
885
|
-
partial_failures
|
|
886
|
-
and partial_failures[-1].get("limit_kind") == "batch"
|
|
887
|
-
):
|
|
888
929
|
break
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
height=capture_height,
|
|
897
|
-
preset=viewport.preset,
|
|
898
|
-
orientation=viewport.orientation,
|
|
899
|
-
image_format=options.image_format,
|
|
900
|
-
mime_type=mime_type,
|
|
901
|
-
size_bytes=len(image_data),
|
|
930
|
+
|
|
931
|
+
outcome = await _request_or_collect_failure(
|
|
932
|
+
http_client,
|
|
933
|
+
url=url,
|
|
934
|
+
path=path,
|
|
935
|
+
engine=engine,
|
|
936
|
+
params=params,
|
|
902
937
|
options=options,
|
|
903
|
-
|
|
938
|
+
viewport=viewport,
|
|
939
|
+
mime_type=mime_type,
|
|
940
|
+
request_context=request_context,
|
|
941
|
+
aggregate_bytes=aggregate_bytes,
|
|
942
|
+
remaining_batch_bytes=remaining_batch_bytes,
|
|
943
|
+
partial_failures=partial_failures,
|
|
904
944
|
)
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
945
|
+
if outcome is None:
|
|
946
|
+
if (
|
|
947
|
+
partial_failures
|
|
948
|
+
and partial_failures[-1].get("limit_kind") == "batch"
|
|
949
|
+
):
|
|
950
|
+
break
|
|
951
|
+
continue
|
|
952
|
+
image_data, capture_height, fallback_used = outcome
|
|
953
|
+
|
|
954
|
+
captures.append(
|
|
955
|
+
DashboardImageCapture(
|
|
956
|
+
data=image_data,
|
|
957
|
+
width=viewport.width,
|
|
958
|
+
height=capture_height,
|
|
959
|
+
preset=viewport.preset,
|
|
960
|
+
orientation=viewport.orientation,
|
|
961
|
+
image_format=options.image_format,
|
|
962
|
+
mime_type=mime_type,
|
|
963
|
+
size_bytes=len(image_data),
|
|
964
|
+
options=options,
|
|
965
|
+
legacy_full_page_fallback=fallback_used,
|
|
966
|
+
)
|
|
967
|
+
)
|
|
968
|
+
except ToolError as exc:
|
|
969
|
+
# Held (not re-raised here) so the restore in ``finally`` runs first
|
|
970
|
+
# and its outcome can be attached to the error payload below.
|
|
971
|
+
batch_error = exc
|
|
972
|
+
finally:
|
|
973
|
+
await guard.restore()
|
|
974
|
+
if capture_warnings is not None:
|
|
975
|
+
capture_warnings.extend(guard.warnings)
|
|
976
|
+
|
|
977
|
+
if batch_error is not None:
|
|
978
|
+
_reraise_with_guard_warnings(batch_error, guard.warnings)
|
|
979
|
+
return _complete_capture_batch(captures, partial_failures, guard.warnings)
|
{ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/dashboard_screenshot/provision.py
RENAMED
|
@@ -23,6 +23,7 @@ from __future__ import annotations
|
|
|
23
23
|
|
|
24
24
|
import logging
|
|
25
25
|
import os
|
|
26
|
+
from dataclasses import dataclass
|
|
26
27
|
from typing import Any
|
|
27
28
|
|
|
28
29
|
import httpx
|
|
@@ -30,9 +31,27 @@ from fastmcp.exceptions import ToolError
|
|
|
30
31
|
|
|
31
32
|
from ..errors import ErrorCode, create_error_response
|
|
32
33
|
from ..tools.helpers import raise_tool_error
|
|
34
|
+
from .theme_guard import EngineCredential, addon_credential_from_options
|
|
33
35
|
|
|
34
36
|
logger = logging.getLogger(__name__)
|
|
35
37
|
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True, slots=True)
|
|
40
|
+
class EngineTarget:
|
|
41
|
+
"""A resolved screenshot engine endpoint.
|
|
42
|
+
|
|
43
|
+
``addon_credential`` authenticates as the engine's user (extracted from
|
|
44
|
+
the Puppet add-on's Supervisor options during discovery, so the theme
|
|
45
|
+
guard needs no second round-trip and the raw secret-bearing options
|
|
46
|
+
dict never leaves this module). It is ``None`` for an explicitly
|
|
47
|
+
configured engine URL. The token inside never leaves the server process
|
|
48
|
+
— it must not be logged or surfaced in responses.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
url: str
|
|
52
|
+
addon_credential: EngineCredential | None = None
|
|
53
|
+
|
|
54
|
+
|
|
36
55
|
ENGINE_PORT = 10000
|
|
37
56
|
# The Supervisor slug is ``<repo-hash>_puppet`` for balloob's Puppet add-on.
|
|
38
57
|
# ``str.endswith`` accepts a tuple, kept as one for easy future extension.
|
|
@@ -146,8 +165,8 @@ def _supervisor_addon_listing(payload: Any) -> list[dict[str, Any]]:
|
|
|
146
165
|
return addons
|
|
147
166
|
|
|
148
167
|
|
|
149
|
-
async def
|
|
150
|
-
"""
|
|
168
|
+
async def resolve_engine() -> EngineTarget:
|
|
169
|
+
"""Resolve the screenshot engine endpoint, or raise ToolError.
|
|
151
170
|
|
|
152
171
|
See module docstring for the three-mode resolution order.
|
|
153
172
|
"""
|
|
@@ -155,10 +174,13 @@ async def resolve_engine_url() -> str:
|
|
|
155
174
|
|
|
156
175
|
explicit = (get_global_settings().dashboard_screenshot_engine_url or "").strip()
|
|
157
176
|
if explicit:
|
|
158
|
-
return
|
|
177
|
+
return EngineTarget(
|
|
178
|
+
url=explicit.rstrip("/"),
|
|
179
|
+
addon_credential=await _addon_credential_best_effort(),
|
|
180
|
+
)
|
|
159
181
|
|
|
160
182
|
if os.environ.get("SUPERVISOR_TOKEN"):
|
|
161
|
-
return await
|
|
183
|
+
return await _discover_engine_via_supervisor()
|
|
162
184
|
|
|
163
185
|
raise_tool_error(
|
|
164
186
|
create_error_response(
|
|
@@ -178,8 +200,35 @@ async def resolve_engine_url() -> str:
|
|
|
178
200
|
raise AssertionError("unreachable: raise_tool_error always raises")
|
|
179
201
|
|
|
180
202
|
|
|
181
|
-
async def
|
|
182
|
-
"""
|
|
203
|
+
async def _addon_credential_best_effort() -> EngineCredential | None:
|
|
204
|
+
"""Best-effort theme-guard credential for an explicitly configured URL.
|
|
205
|
+
|
|
206
|
+
HA OS / Supervised users may set ``HAMCP_DASHBOARD_SCREENSHOT_ENGINE_URL``
|
|
207
|
+
to override auto-discovery while still running the Puppet add-on; without
|
|
208
|
+
its token the theme guard would go inactive exactly where the add-on
|
|
209
|
+
credential exists (``_client_credential`` refuses Supervisor-proxy auth).
|
|
210
|
+
Any failure — no Supervisor, no verified add-on, not started — simply
|
|
211
|
+
means "no credential": the explicit URL may point at a non-add-on
|
|
212
|
+
engine, so this must never raise. If the explicit URL targets a
|
|
213
|
+
different engine than the discovered add-on, the guard still only ever
|
|
214
|
+
writes back a value that the discovered add-on's own user changed
|
|
215
|
+
mid-capture, which only that add-on's renders would do — a safe no-op.
|
|
216
|
+
"""
|
|
217
|
+
if not os.environ.get("SUPERVISOR_TOKEN"):
|
|
218
|
+
return None
|
|
219
|
+
try:
|
|
220
|
+
return (await _discover_engine_via_supervisor()).addon_credential
|
|
221
|
+
except Exception:
|
|
222
|
+
logger.debug(
|
|
223
|
+
"No Supervisor-discoverable engine credential for the explicit "
|
|
224
|
+
"engine URL; the theme guard stays inactive.",
|
|
225
|
+
exc_info=True,
|
|
226
|
+
)
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
async def _discover_engine_via_supervisor() -> EngineTarget:
|
|
231
|
+
"""Find the Puppet add-on via the Supervisor and return its endpoint.
|
|
183
232
|
|
|
184
233
|
Requires the ha-mcp add-on's ``manager`` role (already declared) for the
|
|
185
234
|
read-only ``/addons`` + ``/addons/<slug>/info`` endpoints. Raises a
|
|
@@ -229,7 +278,13 @@ async def _discover_engine_url_via_supervisor() -> str:
|
|
|
229
278
|
context={"slug": slug},
|
|
230
279
|
)
|
|
231
280
|
)
|
|
232
|
-
|
|
281
|
+
options = data.get("options")
|
|
282
|
+
return EngineTarget(
|
|
283
|
+
url=f"http://{hostname}:{ENGINE_PORT}",
|
|
284
|
+
addon_credential=addon_credential_from_options(
|
|
285
|
+
options if isinstance(options, dict) else None
|
|
286
|
+
),
|
|
287
|
+
)
|
|
233
288
|
except ToolError:
|
|
234
289
|
raise
|
|
235
290
|
except (httpx.HTTPError, KeyError, ValueError) as e:
|