ha-mcp-dev 7.6.0.dev645__tar.gz → 7.6.0.dev647__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.6.0.dev645/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.6.0.dev647}/PKG-INFO +4 -3
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/README.md +2 -1
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/pyproject.toml +2 -2
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/client/websocket_client.py +16 -3
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/config.py +43 -0
- ha_mcp_dev-7.6.0.dev647/src/ha_mcp/dashboard_screenshot/__init__.py +28 -0
- ha_mcp_dev-7.6.0.dev647/src/ha_mcp/dashboard_screenshot/capture.py +183 -0
- ha_mcp_dev-7.6.0.dev647/src/ha_mcp/dashboard_screenshot/provision.py +187 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/settings.js +5 -1
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/tools_addons.py +351 -6
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/tools_config_dashboards.py +164 -5
- ha_mcp_dev-7.6.0.dev647/src/ha_mcp/tools/tools_dashboard_screenshot.py +126 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647/src/ha_mcp_dev.egg-info}/PKG-INFO +4 -3
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp_dev.egg-info/SOURCES.txt +4 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp_dev.egg-info/requires.txt +1 -1
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/LICENSE +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/setup.cfg +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/backup_manager.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/policy/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/policy/approval_queue.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/policy/evaluator.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/policy/handlers.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/policy/middleware.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/policy/model.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/policy/persistence.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/policy/value_sources.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/settings.css +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/settings_ui.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/auto_backup.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/smart_search/_base.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/smart_search/_config.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/tools/validation_middleware.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/utils/skill_loader.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.6.0.dev645 → ha_mcp_dev-7.6.0.dev647}/tests/test_env_manager.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ha-mcp-dev
|
|
3
|
-
Version: 7.6.0.
|
|
3
|
+
Version: 7.6.0.dev647
|
|
4
4
|
Summary: Home Assistant MCP Server - Complete control of Home Assistant through MCP
|
|
5
5
|
Author-email: Julien <github@qc-h.net>
|
|
6
6
|
License: MIT
|
|
@@ -25,7 +25,7 @@ Requires-Dist: python-dotenv==1.2.2
|
|
|
25
25
|
Requires-Dist: truststore==0.10.4
|
|
26
26
|
Requires-Dist: websockets==16.0
|
|
27
27
|
Requires-Dist: cryptography==48.0.0
|
|
28
|
-
Requires-Dist: pydantic-monty==0.0.
|
|
28
|
+
Requires-Dist: pydantic-monty==0.0.18
|
|
29
29
|
Dynamic: license-file
|
|
30
30
|
|
|
31
31
|
> **Breaking change (v7.3.0):** `ha_config_set_yaml` has been moved to [beta](docs/beta.md).
|
|
@@ -219,6 +219,7 @@ Spend less time configuring, more time enjoying your smart home.
|
|
|
219
219
|
| **Blueprints** | `ha_get_blueprint`, `ha_import_blueprint` |
|
|
220
220
|
| **Calendar** | `ha_config_get_calendar_events`, `ha_config_remove_calendar_event`, `ha_config_set_calendar_event` |
|
|
221
221
|
| **Camera** | `ha_get_camera_image` |
|
|
222
|
+
| **Dashboard** | `ha_get_dashboard_screenshot` *(beta)* |
|
|
222
223
|
| **Dashboards** | `ha_config_delete_dashboard_resource`, `ha_config_delete_dashboard`, `ha_config_get_dashboard`, `ha_config_list_dashboard_resources`, `ha_config_set_dashboard_resource`, `ha_config_set_dashboard` |
|
|
223
224
|
| **Device Registry** | `ha_get_device`, `ha_remove_device`, `ha_set_device` |
|
|
224
225
|
| **Energy** | `ha_manage_energy_prefs` |
|
|
@@ -234,7 +235,7 @@ Spend less time configuring, more time enjoying your smart home.
|
|
|
234
235
|
| **Scripts** | `ha_config_get_script`, `ha_config_remove_script`, `ha_config_set_script` |
|
|
235
236
|
| **Search & Discovery** | `ha_deep_search`, `ha_get_overview`, `ha_get_state`, `ha_search_entities` |
|
|
236
237
|
| **Service & Device Control** | `ha_bulk_control`, `ha_call_event`, `ha_call_service`, `ha_get_operation_status`, `ha_list_services` |
|
|
237
|
-
| **System** | `
|
|
238
|
+
| **System** | `ha_config_set_yaml` *(beta)*, `ha_get_updates`, `ha_manage_backup`, `ha_manage_custom_tool` *(beta)*, `ha_reload_core`, `ha_restart` |
|
|
238
239
|
| **Todo Lists** | `ha_get_todo`, `ha_remove_todo_item`, `ha_set_todo_item` |
|
|
239
240
|
| **Utilities** | `ha_eval_template`, `ha_install_mcp_tools` *(beta)*, `ha_report_issue` |
|
|
240
241
|
| **Zones** | `ha_get_zone`, `ha_remove_zone`, `ha_set_zone` |
|
|
@@ -189,6 +189,7 @@ Spend less time configuring, more time enjoying your smart home.
|
|
|
189
189
|
| **Blueprints** | `ha_get_blueprint`, `ha_import_blueprint` |
|
|
190
190
|
| **Calendar** | `ha_config_get_calendar_events`, `ha_config_remove_calendar_event`, `ha_config_set_calendar_event` |
|
|
191
191
|
| **Camera** | `ha_get_camera_image` |
|
|
192
|
+
| **Dashboard** | `ha_get_dashboard_screenshot` *(beta)* |
|
|
192
193
|
| **Dashboards** | `ha_config_delete_dashboard_resource`, `ha_config_delete_dashboard`, `ha_config_get_dashboard`, `ha_config_list_dashboard_resources`, `ha_config_set_dashboard_resource`, `ha_config_set_dashboard` |
|
|
193
194
|
| **Device Registry** | `ha_get_device`, `ha_remove_device`, `ha_set_device` |
|
|
194
195
|
| **Energy** | `ha_manage_energy_prefs` |
|
|
@@ -204,7 +205,7 @@ Spend less time configuring, more time enjoying your smart home.
|
|
|
204
205
|
| **Scripts** | `ha_config_get_script`, `ha_config_remove_script`, `ha_config_set_script` |
|
|
205
206
|
| **Search & Discovery** | `ha_deep_search`, `ha_get_overview`, `ha_get_state`, `ha_search_entities` |
|
|
206
207
|
| **Service & Device Control** | `ha_bulk_control`, `ha_call_event`, `ha_call_service`, `ha_get_operation_status`, `ha_list_services` |
|
|
207
|
-
| **System** | `
|
|
208
|
+
| **System** | `ha_config_set_yaml` *(beta)*, `ha_get_updates`, `ha_manage_backup`, `ha_manage_custom_tool` *(beta)*, `ha_reload_core`, `ha_restart` |
|
|
208
209
|
| **Todo Lists** | `ha_get_todo`, `ha_remove_todo_item`, `ha_set_todo_item` |
|
|
209
210
|
| **Utilities** | `ha_eval_template`, `ha_install_mcp_tools` *(beta)*, `ha_report_issue` |
|
|
210
211
|
| **Zones** | `ha_get_zone`, `ha_remove_zone`, `ha_set_zone` |
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ha-mcp-dev"
|
|
7
|
-
version = "7.6.0.
|
|
7
|
+
version = "7.6.0.dev647"
|
|
8
8
|
description = "Home Assistant MCP Server - Complete control of Home Assistant through MCP"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.13,<3.14"
|
|
@@ -31,7 +31,7 @@ dependencies = [
|
|
|
31
31
|
"truststore==0.10.4",
|
|
32
32
|
"websockets==16.0",
|
|
33
33
|
"cryptography==48.0.0",
|
|
34
|
-
"pydantic-monty==0.0.
|
|
34
|
+
"pydantic-monty==0.0.18",
|
|
35
35
|
]
|
|
36
36
|
|
|
37
37
|
[project.urls]
|
|
@@ -536,7 +536,12 @@ class HomeAssistantWebSocketClient:
|
|
|
536
536
|
|
|
537
537
|
Args:
|
|
538
538
|
command_type: Type of command to send
|
|
539
|
-
|
|
539
|
+
_wait_timeout: Seconds to wait for the response (consumed from
|
|
540
|
+
``kwargs``, not forwarded to Home Assistant). Defaults to 30s,
|
|
541
|
+
which suits fast commands; long-running ones (e.g. a
|
|
542
|
+
``supervisor/api`` add-on install) must raise this so the
|
|
543
|
+
client doesn't give up before Home Assistant replies.
|
|
544
|
+
**kwargs: Command parameters (merged into the outgoing message)
|
|
540
545
|
|
|
541
546
|
Returns:
|
|
542
547
|
Response from Home Assistant
|
|
@@ -544,6 +549,14 @@ class HomeAssistantWebSocketClient:
|
|
|
544
549
|
if not self._state.is_ready:
|
|
545
550
|
raise HomeAssistantConnectionError("WebSocket not authenticated")
|
|
546
551
|
|
|
552
|
+
# Pull the wait timeout out of kwargs rather than making it a positional
|
|
553
|
+
# parameter: callers unpack a ``dict[str, object]`` via
|
|
554
|
+
# ``send_command(cmd, **message)``, and a typed positional param would
|
|
555
|
+
# break that call shape under mypy. The leading underscore keeps it out
|
|
556
|
+
# of the HA message namespace — HA WebSocket fields never start with
|
|
557
|
+
# one — so it can never shadow a real command field when popped.
|
|
558
|
+
wait_timeout: float = kwargs.pop("_wait_timeout", 30.0)
|
|
559
|
+
|
|
547
560
|
message_id = self.get_next_message_id()
|
|
548
561
|
message = {"id": message_id, "type": command_type, **kwargs}
|
|
549
562
|
|
|
@@ -556,9 +569,9 @@ class HomeAssistantWebSocketClient:
|
|
|
556
569
|
self.cancel_pending_response(message_id)
|
|
557
570
|
raise
|
|
558
571
|
|
|
559
|
-
# Wait for response outside the lock
|
|
572
|
+
# Wait for response outside the lock.
|
|
560
573
|
try:
|
|
561
|
-
response = await asyncio.wait_for(future, timeout=
|
|
574
|
+
response = await asyncio.wait_for(future, timeout=wait_timeout)
|
|
562
575
|
logger.debug(f"WebSocket response for id {message_id}: {response}")
|
|
563
576
|
|
|
564
577
|
# Process standard Home Assistant WebSocket response
|
|
@@ -193,6 +193,25 @@ class Settings(BaseSettings):
|
|
|
193
193
|
False, alias="HAMCP_ENABLE_CUSTOM_COMPONENT_INTEGRATION"
|
|
194
194
|
)
|
|
195
195
|
|
|
196
|
+
# Dashboard screenshot mode — the ``ha_get_dashboard_screenshot`` tool
|
|
197
|
+
# plus the ``include_screenshot`` / ``return_screenshot`` params on the
|
|
198
|
+
# dashboard get/set tools. Renders a Lovelace view to a PNG via a
|
|
199
|
+
# separate, opt-in headless-Chromium screenshot add-on (balloob's Puppet
|
|
200
|
+
# add-on, or a docker-compose sidecar). Off by default; nothing heavy is
|
|
201
|
+
# pulled unless the user enables it AND installs the engine.
|
|
202
|
+
enable_dashboard_screenshot: bool = Field(
|
|
203
|
+
False, alias="HAMCP_ENABLE_DASHBOARD_SCREENSHOT"
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# Base URL of the screenshot engine (e.g. ``http://puppet:10000`` or a
|
|
207
|
+
# docker-compose sidecar). A connection string, NOT a beta toggle, so
|
|
208
|
+
# it is intentionally absent from FEATURE_FLAG_FIELDS. Left blank, the
|
|
209
|
+
# provisioner auto-discovers the Puppet add-on via the Supervisor in
|
|
210
|
+
# HA OS / Supervised mode; Container / Core users set it explicitly.
|
|
211
|
+
dashboard_screenshot_engine_url: str = Field(
|
|
212
|
+
"", alias="HAMCP_DASHBOARD_SCREENSHOT_ENGINE_URL"
|
|
213
|
+
)
|
|
214
|
+
|
|
196
215
|
# Code Mode — sandboxed Python execution via pydantic-monty.
|
|
197
216
|
# Provides an "escape hatch" tool (ha_manage_custom_tool) that lets LLMs write
|
|
198
217
|
# custom one-off Python code when no existing tool covers the request.
|
|
@@ -265,6 +284,7 @@ class Settings(BaseSettings):
|
|
|
265
284
|
@field_validator(
|
|
266
285
|
"enable_filesystem_tools",
|
|
267
286
|
"enable_custom_component_integration",
|
|
287
|
+
"enable_dashboard_screenshot",
|
|
268
288
|
mode="before",
|
|
269
289
|
)
|
|
270
290
|
@classmethod
|
|
@@ -289,6 +309,23 @@ class Settings(BaseSettings):
|
|
|
289
309
|
raise ValueError("Home Assistant URL must start with http:// or https://")
|
|
290
310
|
return v.rstrip("/") # Remove trailing slash
|
|
291
311
|
|
|
312
|
+
@field_validator("dashboard_screenshot_engine_url")
|
|
313
|
+
@classmethod
|
|
314
|
+
def validate_dashboard_screenshot_engine_url(cls, v: str) -> str:
|
|
315
|
+
"""Validate the optional screenshot-engine URL (env/.env only).
|
|
316
|
+
|
|
317
|
+
Blank = auto-discover the engine add-on via the Supervisor. When set
|
|
318
|
+
(the Docker/Container sidecar path) it must be an http(s) URL, so a
|
|
319
|
+
typo fails loudly at startup instead of silently 0-byte-failing later.
|
|
320
|
+
"""
|
|
321
|
+
if not v:
|
|
322
|
+
return v
|
|
323
|
+
if not v.startswith(("http://", "https://")):
|
|
324
|
+
raise ValueError(
|
|
325
|
+
"Screenshot engine URL must start with http:// or https://"
|
|
326
|
+
)
|
|
327
|
+
return v.rstrip("/")
|
|
328
|
+
|
|
292
329
|
@field_validator("homeassistant_token")
|
|
293
330
|
@classmethod
|
|
294
331
|
def validate_homeassistant_token(cls, v: str) -> str:
|
|
@@ -478,6 +515,11 @@ FEATURE_FLAG_FIELDS: tuple[FeatureFlagField, ...] = (
|
|
|
478
515
|
# the web UI Server Settings tab) can write the flag. Without this
|
|
479
516
|
# entry, the UI save logic would have nowhere to land the value.
|
|
480
517
|
FeatureFlagField("enable_code_mode", "ENABLE_CODE_MODE", bool),
|
|
518
|
+
FeatureFlagField(
|
|
519
|
+
"enable_dashboard_screenshot",
|
|
520
|
+
"HAMCP_ENABLE_DASHBOARD_SCREENSHOT",
|
|
521
|
+
bool,
|
|
522
|
+
),
|
|
481
523
|
)
|
|
482
524
|
|
|
483
525
|
# Override-file location is the same data dir that holds tool_config.json
|
|
@@ -517,6 +559,7 @@ BETA_FEATURE_FIELDS: tuple[str, ...] = (
|
|
|
517
559
|
"enable_custom_component_integration",
|
|
518
560
|
"enable_code_mode",
|
|
519
561
|
"enable_lite_docstrings",
|
|
562
|
+
"enable_dashboard_screenshot",
|
|
520
563
|
)
|
|
521
564
|
|
|
522
565
|
# ===== Advanced settings panel registry =====
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Dashboard screenshot support (opt-in, beta).
|
|
2
|
+
|
|
3
|
+
Renders a Home Assistant Lovelace dashboard view to a PNG via a separate,
|
|
4
|
+
single-purpose headless-Chromium screenshot engine (balloob's Puppet add-on,
|
|
5
|
+
or a docker-compose sidecar).
|
|
6
|
+
|
|
7
|
+
The heavy browser runtime lives entirely in that separate engine — this
|
|
8
|
+
package is a thin async HTTP client plus deployment-mode discovery, so the
|
|
9
|
+
default ha-mcp install stays lightweight unless the user opts in.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from .capture import (
|
|
15
|
+
DEFAULT_HEIGHT,
|
|
16
|
+
DEFAULT_WAIT_MS,
|
|
17
|
+
DEFAULT_WIDTH,
|
|
18
|
+
capture_dashboard_png,
|
|
19
|
+
)
|
|
20
|
+
from .provision import resolve_engine_url
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"DEFAULT_HEIGHT",
|
|
24
|
+
"DEFAULT_WAIT_MS",
|
|
25
|
+
"DEFAULT_WIDTH",
|
|
26
|
+
"capture_dashboard_png",
|
|
27
|
+
"resolve_engine_url",
|
|
28
|
+
]
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Thin async HTTP client to the dashboard screenshot engine.
|
|
2
|
+
|
|
3
|
+
The engine (balloob's Puppet add-on, or a docker-compose sidecar)
|
|
4
|
+
authenticates to Home Assistant with its OWN configured long-lived token, so
|
|
5
|
+
this client passes only the dashboard path + render parameters — no HA token
|
|
6
|
+
ever flows through ha-mcp or the LLM for screenshots.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from urllib.parse import quote
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
|
|
16
|
+
from ..errors import ErrorCode, create_error_response
|
|
17
|
+
from ..tools.helpers import raise_tool_error
|
|
18
|
+
from .provision import TOKEN_HINT, resolve_engine_url
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
# Shared Field-description clause for the ``full_page`` screenshot param, reused
|
|
23
|
+
# across ha_get_dashboard_screenshot and the get/set screenshot options so the
|
|
24
|
+
# wording stays in one place instead of being copy-pasted per tool.
|
|
25
|
+
FULL_PAGE_PARAM_DESC = (
|
|
26
|
+
"capture the whole scrollable dashboard instead of just the viewport "
|
|
27
|
+
"(use when content runs below the fold)"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
DEFAULT_WIDTH = 1280
|
|
31
|
+
DEFAULT_HEIGHT = 800
|
|
32
|
+
# Cold-start render-settle. The engine waits this long after the dashboard
|
|
33
|
+
# reports loaded so canvas/chart cards (history-graph, ApexCharts,
|
|
34
|
+
# mini-graph) have time to paint. Heavy custom chart cards may need more —
|
|
35
|
+
# exposed as a per-request parameter on the tools so the caller can raise it.
|
|
36
|
+
DEFAULT_WAIT_MS = 2500
|
|
37
|
+
_REQUEST_TIMEOUT_S = 60.0
|
|
38
|
+
|
|
39
|
+
# full_page render height. The Puppet engine clips to the requested viewport
|
|
40
|
+
# height (it has no native full-page mode), so capturing a whole scrollable
|
|
41
|
+
# dashboard means asking for a tall viewport. This is the engine's max useful
|
|
42
|
+
# height; dashboards taller than this still clip, and shorter ones get trailing
|
|
43
|
+
# whitespace. Once the engine gains a native fullPage param (upstream), prefer
|
|
44
|
+
# that instead — it auto-sizes to content with no cap and no whitespace.
|
|
45
|
+
FULL_PAGE_HEIGHT = 4096
|
|
46
|
+
|
|
47
|
+
# Characters/sequences that would let an LLM-supplied path escape the
|
|
48
|
+
# dashboard route and reshape the engine request (scheme, authority,
|
|
49
|
+
# query/fragment, traversal, backslash). The engine renders whatever path it
|
|
50
|
+
# is handed with a full-HA credential, so the path must stay a plain
|
|
51
|
+
# frontend route segment.
|
|
52
|
+
_FORBIDDEN_PATH_BITS = ("://", "//", "..", "@", "\\", "?", "#")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _validate_dashboard_path(dashboard_path: str) -> str:
|
|
56
|
+
"""Return a safe, stripped dashboard path or raise ToolError.
|
|
57
|
+
|
|
58
|
+
Rejects anything that isn't a plain Lovelace frontend route — no scheme,
|
|
59
|
+
authority, query, fragment, traversal, or backslash — so the caller can't
|
|
60
|
+
point the credentialed engine at arbitrary URLs or admin routes.
|
|
61
|
+
"""
|
|
62
|
+
raw = (dashboard_path or "").strip()
|
|
63
|
+
if not raw:
|
|
64
|
+
raise_tool_error(
|
|
65
|
+
create_error_response(
|
|
66
|
+
ErrorCode.VALIDATION_MISSING_PARAMETER,
|
|
67
|
+
"dashboard_path is required (e.g. 'lovelace/0' or 'my-dashboard').",
|
|
68
|
+
suggestions=["Pass a Lovelace path such as 'lovelace/0'"],
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
if any(bit in raw for bit in _FORBIDDEN_PATH_BITS) or any(
|
|
72
|
+
ord(c) < 0x20 for c in raw
|
|
73
|
+
):
|
|
74
|
+
raise_tool_error(
|
|
75
|
+
create_error_response(
|
|
76
|
+
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
77
|
+
f"Invalid dashboard_path {dashboard_path!r}: pass a plain "
|
|
78
|
+
"Lovelace path like 'lovelace/0' or 'my-dashboard/kitchen'.",
|
|
79
|
+
details="Path may not contain URLs, query strings, fragments, "
|
|
80
|
+
"'..' segments, backslashes, or control characters.",
|
|
81
|
+
context={"dashboard_path": dashboard_path},
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
# Percent-encode each surviving segment so only the explicit query params
|
|
85
|
+
# (set in capture_dashboard_png) ever reach the engine — defense in depth
|
|
86
|
+
# on top of the rejection above.
|
|
87
|
+
segments = [seg for seg in raw.strip("/").split("/") if seg not in ("", ".")]
|
|
88
|
+
if not segments:
|
|
89
|
+
# e.g. "/" or "/." — the engine would serve its config/UI HTML for the
|
|
90
|
+
# root path, not a dashboard PNG, which would be a confusing silent
|
|
91
|
+
# failure. Require a concrete view path.
|
|
92
|
+
raise_tool_error(
|
|
93
|
+
create_error_response(
|
|
94
|
+
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
95
|
+
f"Invalid dashboard_path {dashboard_path!r}: cannot be empty or "
|
|
96
|
+
"the root path.",
|
|
97
|
+
details="Pass a specific Lovelace view, e.g. 'lovelace/0'.",
|
|
98
|
+
context={"dashboard_path": dashboard_path},
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
return "/".join(quote(seg, safe="") for seg in segments)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
async def capture_dashboard_png(
|
|
105
|
+
dashboard_path: str,
|
|
106
|
+
*,
|
|
107
|
+
width: int = DEFAULT_WIDTH,
|
|
108
|
+
height: int = DEFAULT_HEIGHT,
|
|
109
|
+
zoom: float = 1.0,
|
|
110
|
+
wait_ms: int = DEFAULT_WAIT_MS,
|
|
111
|
+
full_page: bool = False,
|
|
112
|
+
) -> bytes:
|
|
113
|
+
"""Render ``dashboard_path`` to PNG bytes via the screenshot engine.
|
|
114
|
+
|
|
115
|
+
With ``full_page=True`` the whole scrollable dashboard is captured rather
|
|
116
|
+
than just the viewport: the engine clips to the requested height, so we ask
|
|
117
|
+
for a tall viewport (``FULL_PAGE_HEIGHT``). ``height`` is ignored in that
|
|
118
|
+
case. See ``FULL_PAGE_HEIGHT`` for the interim caveats (cap + whitespace).
|
|
119
|
+
|
|
120
|
+
Raises :class:`ToolError` if the engine is unreachable or returns an error.
|
|
121
|
+
"""
|
|
122
|
+
path = _validate_dashboard_path(dashboard_path)
|
|
123
|
+
engine = await resolve_engine_url()
|
|
124
|
+
effective_height = FULL_PAGE_HEIGHT if full_page else int(height)
|
|
125
|
+
params: dict[str, str] = {
|
|
126
|
+
"viewport": f"{int(width)}x{effective_height}",
|
|
127
|
+
"zoom": str(zoom),
|
|
128
|
+
"wait": str(int(wait_ms)),
|
|
129
|
+
"format": "png",
|
|
130
|
+
}
|
|
131
|
+
url = f"{engine}/{path}"
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
async with httpx.AsyncClient(timeout=httpx.Timeout(_REQUEST_TIMEOUT_S)) as h:
|
|
135
|
+
resp = await h.get(url, params=params)
|
|
136
|
+
except httpx.HTTPError as e:
|
|
137
|
+
raise_tool_error(
|
|
138
|
+
create_error_response(
|
|
139
|
+
ErrorCode.CONNECTION_FAILED,
|
|
140
|
+
f"Could not reach the dashboard screenshot engine at {engine}.",
|
|
141
|
+
details=str(e),
|
|
142
|
+
context={"engine_url": engine},
|
|
143
|
+
suggestions=[
|
|
144
|
+
"Ensure the Puppet screenshot add-on (or sidecar) is "
|
|
145
|
+
"installed and running",
|
|
146
|
+
# Puppet restarts itself when navigation fails, so a
|
|
147
|
+
# missing/invalid token shows up as a dropped connection
|
|
148
|
+
# rather than an HTTP error.
|
|
149
|
+
f"If it is running, its access token is likely missing or "
|
|
150
|
+
f"invalid — {TOKEN_HINT}",
|
|
151
|
+
"Check HAMCP_DASHBOARD_SCREENSHOT_ENGINE_URL on "
|
|
152
|
+
"Docker/Container deployments",
|
|
153
|
+
],
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
if resp.status_code >= 400:
|
|
158
|
+
raise_tool_error(
|
|
159
|
+
create_error_response(
|
|
160
|
+
ErrorCode.SERVICE_CALL_FAILED,
|
|
161
|
+
f"Screenshot engine returned HTTP {resp.status_code} for "
|
|
162
|
+
f"dashboard '{path}'.",
|
|
163
|
+
details=resp.text[:300],
|
|
164
|
+
context={"status_code": resp.status_code, "path": path},
|
|
165
|
+
suggestions=[
|
|
166
|
+
"Verify the dashboard path exists",
|
|
167
|
+
f"If the engine landed on the login page, its access token "
|
|
168
|
+
f"is missing/invalid — {TOKEN_HINT}",
|
|
169
|
+
"Increase wait_ms for heavy chart cards",
|
|
170
|
+
],
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
if not resp.content:
|
|
174
|
+
raise_tool_error(
|
|
175
|
+
create_error_response(
|
|
176
|
+
ErrorCode.SERVICE_CALL_FAILED,
|
|
177
|
+
f"Screenshot engine returned an empty image for '{path}'.",
|
|
178
|
+
details="The dashboard path may be invalid, or the engine's "
|
|
179
|
+
"access token may be missing/expired.",
|
|
180
|
+
context={"path": path},
|
|
181
|
+
)
|
|
182
|
+
)
|
|
183
|
+
return resp.content
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Locate the dashboard screenshot engine for the current deployment.
|
|
2
|
+
|
|
3
|
+
The engine is balloob's **Puppet** add-on (https://github.com/balloob/
|
|
4
|
+
home-assistant-addons), a headless-Chromium renderer. ha-mcp does not vendor
|
|
5
|
+
it — the user installs it themselves.
|
|
6
|
+
|
|
7
|
+
Three deployment modes, resolved lazily on first tool use (never at
|
|
8
|
+
startup, never silently installed):
|
|
9
|
+
|
|
10
|
+
1. **Explicit** — ``HAMCP_DASHBOARD_SCREENSHOT_ENGINE_URL`` is set. Used
|
|
11
|
+
verbatim. This is the Docker / Container path (a docker-compose
|
|
12
|
+
sidecar), and also lets HA OS users override auto-discovery.
|
|
13
|
+
2. **HA OS / Supervised** — ``SUPERVISOR_TOKEN`` is present. The Puppet
|
|
14
|
+
add-on (slug ``*_puppet``) is discovered via the Supervisor REST API and
|
|
15
|
+
reached on the internal network by its hostname. The user must have
|
|
16
|
+
installed + started it themselves (add balloob's add-on repository, then
|
|
17
|
+
install "Puppet") — we never auto-install.
|
|
18
|
+
3. **Neither** (stdio / standalone) — a clear :class:`ToolError` explaining
|
|
19
|
+
how to enable it.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import logging
|
|
25
|
+
import os
|
|
26
|
+
|
|
27
|
+
import httpx
|
|
28
|
+
from fastmcp.exceptions import ToolError
|
|
29
|
+
|
|
30
|
+
from ..errors import ErrorCode, create_error_response
|
|
31
|
+
from ..tools.helpers import raise_tool_error
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
ENGINE_PORT = 10000
|
|
36
|
+
# The Supervisor slug is ``<repo-hash>_puppet`` for balloob's Puppet add-on;
|
|
37
|
+
# ``_ha_mcp_screenshot`` is the legacy vendored engine, still accepted so a
|
|
38
|
+
# mid-migration install keeps working. ``str.endswith`` takes this tuple.
|
|
39
|
+
ENGINE_SLUG_SUFFIXES = ("_puppet", "_ha_mcp_screenshot")
|
|
40
|
+
|
|
41
|
+
_REPO_URL = "https://github.com/balloob/home-assistant-addons"
|
|
42
|
+
|
|
43
|
+
# The single source of truth for the "configure the access token" instruction,
|
|
44
|
+
# reused across every engine-troubleshooting message here and in capture.py so
|
|
45
|
+
# a copy edit only has to happen once.
|
|
46
|
+
TOKEN_HINT = (
|
|
47
|
+
"set the add-on's 'access_token' option to a Home Assistant long-lived "
|
|
48
|
+
"access token (create one in Profile > Security, ideally for a dedicated "
|
|
49
|
+
"low-privilege user) and (re)start it"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
_INSTALL_HELP = (
|
|
53
|
+
"Dashboard screenshot mode is enabled, but the Puppet screenshot engine "
|
|
54
|
+
"add-on is not installed. On HA OS / Supervised: (1) add balloob's add-on "
|
|
55
|
+
f"repository ({_REPO_URL}) under Settings > Add-ons > Add-on Store > "
|
|
56
|
+
"Repositories, then install the 'Puppet' add-on; and (2) it REQUIRES a "
|
|
57
|
+
f"token — {TOKEN_HINT}. Without a token the engine only serves a "
|
|
58
|
+
"configuration-instructions page. On Docker / Container, run the Puppet "
|
|
59
|
+
"image as a sidecar (with its access_token set) and point ha-mcp at it via "
|
|
60
|
+
"HAMCP_DASHBOARD_SCREENSHOT_ENGINE_URL (e.g. http://puppet:10000)."
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
_NOT_STARTED_HELP = (
|
|
64
|
+
"The Puppet screenshot engine add-on is installed but not started. Open "
|
|
65
|
+
f"Settings > Add-ons > Puppet, {TOKEN_HINT} (enable 'Start on boot' to keep "
|
|
66
|
+
"it available)."
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
_STDIO_HELP = (
|
|
70
|
+
"Dashboard screenshot mode needs either HA OS / Supervised (add balloob's "
|
|
71
|
+
"add-on repository and install the 'Puppet' add-on) or a Puppet sidecar "
|
|
72
|
+
"reachable via HAMCP_DASHBOARD_SCREENSHOT_ENGINE_URL. This deployment "
|
|
73
|
+
"(stdio / standalone) can host neither — see docs/beta.md."
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
async def resolve_engine_url() -> str:
|
|
78
|
+
"""Return the base URL of the screenshot engine, or raise ToolError.
|
|
79
|
+
|
|
80
|
+
See module docstring for the three-mode resolution order.
|
|
81
|
+
"""
|
|
82
|
+
from ..config import get_global_settings
|
|
83
|
+
|
|
84
|
+
explicit = (get_global_settings().dashboard_screenshot_engine_url or "").strip()
|
|
85
|
+
if explicit:
|
|
86
|
+
return explicit.rstrip("/")
|
|
87
|
+
|
|
88
|
+
if os.environ.get("SUPERVISOR_TOKEN"):
|
|
89
|
+
return await _discover_engine_url_via_supervisor()
|
|
90
|
+
|
|
91
|
+
raise_tool_error(
|
|
92
|
+
create_error_response(
|
|
93
|
+
ErrorCode.SERVICE_CALL_FAILED,
|
|
94
|
+
"Dashboard screenshot mode is not available in this deployment.",
|
|
95
|
+
details=_STDIO_HELP,
|
|
96
|
+
suggestions=[
|
|
97
|
+
"Use HA OS / Supervised and install the screenshot engine add-on",
|
|
98
|
+
"Or run the engine as a sidecar and set "
|
|
99
|
+
"HAMCP_DASHBOARD_SCREENSHOT_ENGINE_URL",
|
|
100
|
+
],
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
async def _discover_engine_url_via_supervisor() -> str:
|
|
106
|
+
"""Find the Puppet add-on via the Supervisor and return its internal URL.
|
|
107
|
+
|
|
108
|
+
Requires the ha-mcp add-on's ``manager`` role (already declared) for the
|
|
109
|
+
read-only ``/addons`` + ``/addons/<slug>/info`` endpoints. Raises a
|
|
110
|
+
ToolError with actionable guidance when the engine is missing or stopped.
|
|
111
|
+
"""
|
|
112
|
+
from ..client.supervisor_client import make_supervisor_httpx_client
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
async with make_supervisor_httpx_client(timeout=15.0, verify=True) as sup:
|
|
116
|
+
listing = await sup.get("/addons")
|
|
117
|
+
listing.raise_for_status()
|
|
118
|
+
addons = listing.json().get("data", {}).get("addons", [])
|
|
119
|
+
|
|
120
|
+
matches = [
|
|
121
|
+
a
|
|
122
|
+
for a in addons
|
|
123
|
+
if str(a.get("slug", "")).endswith(ENGINE_SLUG_SUFFIXES)
|
|
124
|
+
]
|
|
125
|
+
if not matches:
|
|
126
|
+
raise_tool_error(
|
|
127
|
+
create_error_response(
|
|
128
|
+
ErrorCode.RESOURCE_NOT_FOUND,
|
|
129
|
+
"The Puppet screenshot engine add-on is not installed.",
|
|
130
|
+
details=_INSTALL_HELP,
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# The /addons list is only reliable for slug discovery (the
|
|
135
|
+
# repo-hash prefix is not known ahead of time). Per-addon ``state``
|
|
136
|
+
# and ``hostname`` are authoritative on /addons/<slug>/info — the
|
|
137
|
+
# same source ha_manage_addon trusts. The list can report a
|
|
138
|
+
# stale/absent ``state`` for a freshly-started add-on, so read state
|
|
139
|
+
# from /info. With more than one match (e.g. the legacy vendored
|
|
140
|
+
# engine alongside Puppet), prefer whichever is started.
|
|
141
|
+
last_slug: str | None = None
|
|
142
|
+
last_state: str | None = None
|
|
143
|
+
for match in matches:
|
|
144
|
+
slug = str(match["slug"])
|
|
145
|
+
info = await sup.get(f"/addons/{slug}/info")
|
|
146
|
+
info.raise_for_status()
|
|
147
|
+
data = info.json().get("data", {})
|
|
148
|
+
last_slug, last_state = slug, data.get("state")
|
|
149
|
+
if data.get("state") != "started":
|
|
150
|
+
continue
|
|
151
|
+
hostname = data.get("hostname") or data.get("ip_address")
|
|
152
|
+
if not hostname:
|
|
153
|
+
raise_tool_error(
|
|
154
|
+
create_error_response(
|
|
155
|
+
ErrorCode.SERVICE_CALL_FAILED,
|
|
156
|
+
f"Screenshot engine add-on '{slug}' is started but "
|
|
157
|
+
"the Supervisor returned no hostname/ip_address.",
|
|
158
|
+
context={"slug": slug},
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
return f"http://{hostname}:{ENGINE_PORT}"
|
|
162
|
+
|
|
163
|
+
# Matched an installed engine, but none was started.
|
|
164
|
+
raise_tool_error(
|
|
165
|
+
create_error_response(
|
|
166
|
+
ErrorCode.SERVICE_CALL_FAILED,
|
|
167
|
+
"The Puppet screenshot engine add-on is installed but not started.",
|
|
168
|
+
details=_NOT_STARTED_HELP,
|
|
169
|
+
context={"slug": last_slug, "state": last_state},
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
except ToolError:
|
|
173
|
+
raise
|
|
174
|
+
except (httpx.HTTPError, KeyError, ValueError) as e:
|
|
175
|
+
logger.warning("Screenshot engine discovery via Supervisor failed: %s", e)
|
|
176
|
+
raise_tool_error(
|
|
177
|
+
create_error_response(
|
|
178
|
+
ErrorCode.CONNECTION_FAILED,
|
|
179
|
+
"Could not query the Supervisor to locate the Puppet "
|
|
180
|
+
"screenshot engine add-on.",
|
|
181
|
+
details=str(e),
|
|
182
|
+
suggestions=[
|
|
183
|
+
"Verify the Puppet add-on is installed and started",
|
|
184
|
+
"Or set HAMCP_DASHBOARD_SCREENSHOT_ENGINE_URL explicitly",
|
|
185
|
+
],
|
|
186
|
+
)
|
|
187
|
+
)
|
|
@@ -1088,9 +1088,13 @@ const FEATURE_META = {
|
|
|
1088
1088
|
label: "Enable lite tool docstrings (beta)",
|
|
1089
1089
|
help: "Beta feature — disabled by default. Replaces the docstrings on a handful of heavy ha-mcp tools (automations, scripts, scenes, helpers, dashboards, ha_call_service, ha_config_set_yaml) with shorter variants that defer schema and example detail to the ha_get_skill_guide tool (or its skill:// resource). WARNING: this reduces idle token usage, but may degrade LLM performance — the trimmed descriptions rely on the LLM actually calling the skill tool or reading the skill resource for detail, which is not guaranteed (some models will skip the extra tool call and end up with less guidance than they had before). Best paired with a client that supports MCP resources or with enable_tool_search. Requires restart to take effect.",
|
|
1090
1090
|
},
|
|
1091
|
+
enable_dashboard_screenshot: {
|
|
1092
|
+
label: "Enable dashboard screenshot mode (beta)",
|
|
1093
|
+
help: "Beta feature — disabled by default. Adds the ha_get_dashboard_screenshot tool plus include_screenshot / return_screenshot options on the dashboard get/set tools, so AI assistants can see a rendered PNG of a Lovelace dashboard (e.g. to verify one they just created). Rendering runs in a separate, opt-in engine — balloob's \"Puppet\" add-on (headless Chromium) — which you install once (add balloob's add-on repository, then install \"Puppet\") and give a long-lived access token; on Docker/Container deployments you run that engine as a sidecar and set HAMCP_DASHBOARD_SCREENSHOT_ENGINE_URL. Nothing heavy is installed unless you both enable this and install the engine. Requires restart to take effect. REQUIRES the master \"Enable beta features\" toggle above (and in the web UI) to be on — otherwise this sub-flag is ignored at runtime regardless of its value here.",
|
|
1094
|
+
},
|
|
1091
1095
|
};
|
|
1092
1096
|
|
|
1093
|
-
// The
|
|
1097
|
+
// The beta sub-flag fields gated by the master beta toggle. Populated
|
|
1094
1098
|
// from the ``beta_sub_flags`` array in the /api/settings/features
|
|
1095
1099
|
// response so the JS stays in sync with Python's
|
|
1096
1100
|
// ``config.BETA_FEATURE_FIELDS`` without duplicating the name list here.
|