ha-mcp-dev 7.8.0.dev701__tar.gz → 7.8.0.dev703__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.8.0.dev701/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.8.0.dev703}/PKG-INFO +3 -3
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/README.md +2 -2
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/pyproject.toml +3 -1
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/read_only.py +6 -1
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/settings.css +7 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/settings.js +73 -19
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/settings_ui.py +19 -16
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/backup.py +121 -1
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/tools_filesystem.py +36 -15
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703/src/ha_mcp_dev.egg-info}/PKG-INFO +3 -3
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/LICENSE +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/setup.cfg +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/backup_manager.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/policy/__init__.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/policy/approval_queue.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/policy/evaluator.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/policy/handlers.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/policy/middleware.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/policy/model.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/policy/persistence.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/policy/value_sources.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/auto_backup.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/config_entry_flow.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/smart_search/_base.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/smart_search/_config.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/tools_themes.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/tools/validation_middleware.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/utils/skill_loader.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.8.0.dev701 → ha_mcp_dev-7.8.0.dev703}/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.8.0.
|
|
3
|
+
Version: 7.8.0.dev703
|
|
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
|
|
@@ -275,8 +275,8 @@ Some tools require a companion custom component installed in Home Assistant. Sta
|
|
|
275
275
|
| Tool | Description |
|
|
276
276
|
|------|-------------|
|
|
277
277
|
| `ha_config_set_yaml` *(beta)* | Safely add, replace, or remove top-level YAML keys in `configuration.yaml` and package files (automatic backup, validation, and config check) |
|
|
278
|
-
| `ha_list_files` *(beta)* | List files in allowed directories
|
|
279
|
-
| `ha_read_file` *(beta)* | Read files from allowed paths (config YAML, logs,
|
|
278
|
+
| `ha_list_files` *(beta)* | List files in allowed directories |
|
|
279
|
+
| `ha_read_file` *(beta)* | Read files from allowed paths (config YAML, logs, and allowed directories) |
|
|
280
280
|
| `ha_write_file` *(beta)* | Write files to allowed directories |
|
|
281
281
|
| `ha_delete_file` *(beta)* | Delete files from allowed directories |
|
|
282
282
|
|
|
@@ -245,8 +245,8 @@ Some tools require a companion custom component installed in Home Assistant. Sta
|
|
|
245
245
|
| Tool | Description |
|
|
246
246
|
|------|-------------|
|
|
247
247
|
| `ha_config_set_yaml` *(beta)* | Safely add, replace, or remove top-level YAML keys in `configuration.yaml` and package files (automatic backup, validation, and config check) |
|
|
248
|
-
| `ha_list_files` *(beta)* | List files in allowed directories
|
|
249
|
-
| `ha_read_file` *(beta)* | Read files from allowed paths (config YAML, logs,
|
|
248
|
+
| `ha_list_files` *(beta)* | List files in allowed directories |
|
|
249
|
+
| `ha_read_file` *(beta)* | Read files from allowed paths (config YAML, logs, and allowed directories) |
|
|
250
250
|
| `ha_write_file` *(beta)* | Write files to allowed directories |
|
|
251
251
|
| `ha_delete_file` *(beta)* | Delete files from allowed directories |
|
|
252
252
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ha-mcp-dev"
|
|
7
|
-
version = "7.8.0.
|
|
7
|
+
version = "7.8.0.dev703"
|
|
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"
|
|
@@ -85,6 +85,8 @@ ignore_missing_imports = true
|
|
|
85
85
|
[[tool.mypy.overrides]]
|
|
86
86
|
module = [
|
|
87
87
|
"homeassistant.*",
|
|
88
|
+
"aiohasupervisor",
|
|
89
|
+
"aiohasupervisor.*",
|
|
88
90
|
"aiohttp",
|
|
89
91
|
"voluptuous",
|
|
90
92
|
"jsonschema",
|
|
@@ -66,8 +66,12 @@ class ReadOnlyExemption(NamedTuple):
|
|
|
66
66
|
def _backup_write(args: dict[str, Any]) -> str | None:
|
|
67
67
|
scope = args.get("scope")
|
|
68
68
|
action = args.get("action")
|
|
69
|
+
# Read-only-safe: per-edit backup listing/viewing, and snapshot listing
|
|
70
|
+
# (issue #1586 — pure ``backup/info`` read, no tarball mutation).
|
|
69
71
|
if scope == "edits" and action in ("list", "view"):
|
|
70
72
|
return None
|
|
73
|
+
if scope == "snapshot" and action == "list":
|
|
74
|
+
return None
|
|
71
75
|
return f"scope={scope!r}, action={action!r}"
|
|
72
76
|
|
|
73
77
|
|
|
@@ -144,7 +148,8 @@ def _custom_tool_write(args: dict[str, Any]) -> str | None:
|
|
|
144
148
|
READ_ONLY_EXEMPT_TOOLS: dict[str, ReadOnlyExemption] = {
|
|
145
149
|
"ha_manage_backup": ReadOnlyExemption(
|
|
146
150
|
_backup_write,
|
|
147
|
-
"listing and viewing per-edit backups (scope='edits', action='list' or
|
|
151
|
+
"listing and viewing per-edit backups (scope='edits', action='list' or "
|
|
152
|
+
"'view') and listing snapshots (scope='snapshot', action='list')",
|
|
148
153
|
),
|
|
149
154
|
"ha_manage_addon": ReadOnlyExemption(
|
|
150
155
|
_addon_write,
|
|
@@ -415,6 +415,13 @@
|
|
|
415
415
|
fieldset.a11y-options { border: 0; padding: 0; margin: 0; min-inline-size: auto; }
|
|
416
416
|
.visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0;
|
|
417
417
|
margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
|
|
418
|
+
/* Skip-to-content link: visually hidden until focused, then pinned to the
|
|
419
|
+
top-left so keyboard users can bypass the header. */
|
|
420
|
+
.skip-link { position: absolute; left: 8px; top: -56px; z-index: 100;
|
|
421
|
+
padding: 8px 16px; border-radius: 8px; font-size: 0.875rem; font-weight: 600;
|
|
422
|
+
background: var(--surface); color: var(--text); border: 1px solid var(--border);
|
|
423
|
+
transition: top 0.15s ease; }
|
|
424
|
+
.skip-link:focus { top: 8px; outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
418
425
|
.a11y-option { display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px;
|
|
419
426
|
border: 1px solid var(--border); border-radius: 8px; cursor: pointer; font-size: 0.85rem;
|
|
420
427
|
background: var(--bg); user-select: none; }
|
|
@@ -9,11 +9,13 @@ window.addEventListener('error', (e) => {
|
|
|
9
9
|
const el = document.getElementById('status');
|
|
10
10
|
if (!el) return;
|
|
11
11
|
const where = e.filename ? `${e.filename}:${e.lineno}:${e.colno}` : 'inline';
|
|
12
|
+
setStatusAlert(el, true);
|
|
12
13
|
el.textContent = `JS error: ${e.message} @ ${where}`;
|
|
13
14
|
});
|
|
14
15
|
window.addEventListener('unhandledrejection', (e) => {
|
|
15
16
|
const el = document.getElementById('status');
|
|
16
17
|
if (!el) return;
|
|
18
|
+
setStatusAlert(el, true);
|
|
17
19
|
el.textContent = `Async error: ${e.reason && e.reason.message ? e.reason.message : String(e.reason)}`;
|
|
18
20
|
});
|
|
19
21
|
|
|
@@ -135,18 +137,18 @@ async function loadTools() {
|
|
|
135
137
|
try {
|
|
136
138
|
resp = await fetch('./api/settings/tools');
|
|
137
139
|
} catch (e) {
|
|
138
|
-
updateStatus('Network error reaching /api/settings/tools: ' + e.message);
|
|
140
|
+
updateStatus('Network error reaching /api/settings/tools: ' + e.message, false, true);
|
|
139
141
|
return;
|
|
140
142
|
}
|
|
141
143
|
if (!resp.ok) {
|
|
142
|
-
updateStatus(`/api/settings/tools returned HTTP ${resp.status} ${resp.statusText}
|
|
144
|
+
updateStatus(`/api/settings/tools returned HTTP ${resp.status} ${resp.statusText}`, false, true);
|
|
143
145
|
return;
|
|
144
146
|
}
|
|
145
147
|
let data;
|
|
146
148
|
try {
|
|
147
149
|
data = await resp.json();
|
|
148
150
|
} catch (e) {
|
|
149
|
-
updateStatus('Failed to parse /api/settings/tools response as JSON: ' + e.message);
|
|
151
|
+
updateStatus('Failed to parse /api/settings/tools response as JSON: ' + e.message, false, true);
|
|
150
152
|
return;
|
|
151
153
|
}
|
|
152
154
|
toolData = data.tools || [];
|
|
@@ -169,14 +171,15 @@ async function loadTools() {
|
|
|
169
171
|
// the user where to look instead of leaving them on "Loading".
|
|
170
172
|
updateStatus(
|
|
171
173
|
'No tools found. The sidecar reads ~/.ha-mcp/tool_metadata.json — ' +
|
|
172
|
-
'if missing/empty, restart your MCP client. See ~/.ha-mcp/sidecar.log for details.'
|
|
174
|
+
'if missing/empty, restart your MCP client. See ~/.ha-mcp/sidecar.log for details.',
|
|
175
|
+
false, true
|
|
173
176
|
);
|
|
174
177
|
return;
|
|
175
178
|
}
|
|
176
179
|
try {
|
|
177
180
|
render();
|
|
178
181
|
} catch (e) {
|
|
179
|
-
updateStatus('Render failed: ' + e.message + ' (open browser devtools for the stack)');
|
|
182
|
+
updateStatus('Render failed: ' + e.message + ' (open browser devtools for the stack)', false, true);
|
|
180
183
|
throw e;
|
|
181
184
|
}
|
|
182
185
|
updateStatus('Loaded');
|
|
@@ -765,14 +768,24 @@ async function saveConfig() {
|
|
|
765
768
|
// are on.
|
|
766
769
|
if (restartChannel) restartChannel.postMessage({type: 'restart-required'});
|
|
767
770
|
} else {
|
|
768
|
-
updateStatus('Save failed!');
|
|
771
|
+
updateStatus('Save failed!', false, true);
|
|
769
772
|
}
|
|
770
773
|
}
|
|
771
774
|
|
|
772
|
-
|
|
775
|
+
// Reflect success/error semantics on a status span for assistive tech:
|
|
776
|
+
// failures switch to role=alert/assertive so screen readers interrupt; all
|
|
777
|
+
// other updates stay role=status/polite (matching the static markup). (#1596)
|
|
778
|
+
function setStatusAlert(el, isError) {
|
|
779
|
+
if (!el) return;
|
|
780
|
+
el.setAttribute('role', isError ? 'alert' : 'status');
|
|
781
|
+
el.setAttribute('aria-live', isError ? 'assertive' : 'polite');
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function updateStatus(text, saved, isError) {
|
|
773
785
|
const el = document.getElementById('status');
|
|
774
|
-
el
|
|
786
|
+
setStatusAlert(el, isError);
|
|
775
787
|
el.className = saved ? 'status saved' : 'status';
|
|
788
|
+
el.textContent = text;
|
|
776
789
|
}
|
|
777
790
|
|
|
778
791
|
function applyToolSearch() {
|
|
@@ -916,6 +929,7 @@ async function saveBackupConfig() {
|
|
|
916
929
|
return;
|
|
917
930
|
}
|
|
918
931
|
btn.disabled = true;
|
|
932
|
+
setStatusAlert(statusEl, false);
|
|
919
933
|
statusEl.textContent = 'Saving…';
|
|
920
934
|
try {
|
|
921
935
|
const resp = await fetch('./api/settings/backup-config', {
|
|
@@ -931,6 +945,7 @@ async function saveBackupConfig() {
|
|
|
931
945
|
if (typeof data.error === 'string') msg = data.error;
|
|
932
946
|
else if (data.error.message) msg = data.error.message;
|
|
933
947
|
}
|
|
948
|
+
setStatusAlert(statusEl, true);
|
|
934
949
|
statusEl.textContent = msg;
|
|
935
950
|
return;
|
|
936
951
|
}
|
|
@@ -958,6 +973,7 @@ async function saveBackupConfig() {
|
|
|
958
973
|
}
|
|
959
974
|
} catch (err) {
|
|
960
975
|
btn.disabled = false;
|
|
976
|
+
setStatusAlert(statusEl, true);
|
|
961
977
|
statusEl.textContent = 'Network error: ' + String(err);
|
|
962
978
|
}
|
|
963
979
|
}
|
|
@@ -1014,8 +1030,8 @@ function renderFsCustomPathsSubForm(parentEl, masterOn, fsOn) {
|
|
|
1014
1030
|
: '.storage, secrets.yaml';
|
|
1015
1031
|
info.innerHTML =
|
|
1016
1032
|
`<div class="feature-name">Custom filesystem directories (advanced)</div>` +
|
|
1017
|
-
`<div class="feature-help">Extra directories (one per line
|
|
1018
|
-
`<div class="feature-help">Always blocked (cannot be added): <code>${escapeHtml(denyList)}</code>, path traversal (<code>..</code>), and absolute
|
|
1033
|
+
`<div class="feature-help">Extra directories (one per line) that the file tools may READ and WRITE — either relative to your config dir (e.g. <code>pyscript</code>, <code>python_scripts</code>) or an absolute HAOS sibling volume <code>/share</code>, <code>/media</code>, <code>/ssl</code>, <code>/backup</code> (or a subdirectory of one). Each entry grants both read and write. Applies immediately; no restart needed.</div>` +
|
|
1034
|
+
`<div class="feature-help">Always blocked (cannot be added): <code>${escapeHtml(denyList)}</code>, path traversal (<code>..</code>), and any absolute path outside the HAOS sibling volumes.</div>`;
|
|
1019
1035
|
|
|
1020
1036
|
const control = document.createElement('div');
|
|
1021
1037
|
control.className = 'feature-control';
|
|
@@ -1050,6 +1066,8 @@ function renderFsCustomPathsSubForm(parentEl, masterOn, fsOn) {
|
|
|
1050
1066
|
const status = document.createElement('div');
|
|
1051
1067
|
status.id = 'fsCustomPathsStatus';
|
|
1052
1068
|
status.className = 'feature-help';
|
|
1069
|
+
status.setAttribute('role', 'status');
|
|
1070
|
+
status.setAttribute('aria-live', 'polite');
|
|
1053
1071
|
control.appendChild(ta);
|
|
1054
1072
|
control.appendChild(btn);
|
|
1055
1073
|
control.appendChild(status);
|
|
@@ -1070,6 +1088,7 @@ async function saveFsCustomPaths() {
|
|
|
1070
1088
|
.map(s => s.trim())
|
|
1071
1089
|
.filter(s => s.length);
|
|
1072
1090
|
btn.disabled = true;
|
|
1091
|
+
setStatusAlert(statusEl, false);
|
|
1073
1092
|
statusEl.textContent = 'Saving…';
|
|
1074
1093
|
try {
|
|
1075
1094
|
const resp = await fetch('./api/settings/fs-custom-paths', {
|
|
@@ -1085,6 +1104,7 @@ async function saveFsCustomPaths() {
|
|
|
1085
1104
|
if (typeof data.error === 'string') msg = data.error;
|
|
1086
1105
|
else if (data.error.message) msg = data.error.message;
|
|
1087
1106
|
}
|
|
1107
|
+
setStatusAlert(statusEl, true);
|
|
1088
1108
|
statusEl.textContent = msg;
|
|
1089
1109
|
return;
|
|
1090
1110
|
}
|
|
@@ -1101,6 +1121,7 @@ async function saveFsCustomPaths() {
|
|
|
1101
1121
|
: 'Saved.';
|
|
1102
1122
|
} catch (err) {
|
|
1103
1123
|
btn.disabled = false;
|
|
1124
|
+
setStatusAlert(statusEl, true);
|
|
1104
1125
|
statusEl.textContent = 'Network error: ' + String(err);
|
|
1105
1126
|
}
|
|
1106
1127
|
}
|
|
@@ -2453,15 +2474,18 @@ async function removePolicyRule(toolName) {
|
|
|
2453
2474
|
|
|
2454
2475
|
async function saveGlobalSettings() {
|
|
2455
2476
|
const statusEl = document.getElementById('policy-global-save-status');
|
|
2477
|
+
setStatusAlert(statusEl, false);
|
|
2456
2478
|
statusEl.textContent = 'Saving...';
|
|
2457
2479
|
let resp;
|
|
2458
2480
|
try {
|
|
2459
2481
|
resp = await fetch('./api/policy/config');
|
|
2460
2482
|
} catch (e) {
|
|
2483
|
+
setStatusAlert(statusEl, true);
|
|
2461
2484
|
statusEl.textContent = 'Network error: ' + e.message;
|
|
2462
2485
|
return;
|
|
2463
2486
|
}
|
|
2464
2487
|
if (!resp.ok) {
|
|
2488
|
+
setStatusAlert(statusEl, true);
|
|
2465
2489
|
statusEl.textContent = 'Load failed: ' + resp.status;
|
|
2466
2490
|
return;
|
|
2467
2491
|
}
|
|
@@ -2472,6 +2496,7 @@ async function saveGlobalSettings() {
|
|
|
2472
2496
|
await policyPut(policy, 'Save global settings');
|
|
2473
2497
|
statusEl.textContent = 'Saved.';
|
|
2474
2498
|
} catch (e) {
|
|
2499
|
+
setStatusAlert(statusEl, true);
|
|
2475
2500
|
statusEl.textContent = e.message;
|
|
2476
2501
|
}
|
|
2477
2502
|
}
|
|
@@ -2629,10 +2654,18 @@ setInterval(() => {
|
|
|
2629
2654
|
// Generic dispatcher — every .tab button names its target panel via
|
|
2630
2655
|
// data-panel, every .panel has matching id="panel-<name>". Adding a
|
|
2631
2656
|
// new tab is one button + one panel div; no JS change needed.
|
|
2632
|
-
function activateTab(target) {
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2657
|
+
function activateTab(target, opts) {
|
|
2658
|
+
const focusTab = opts && opts.focusTab;
|
|
2659
|
+
document.querySelectorAll('.tab').forEach(t => {
|
|
2660
|
+
const selected = t.dataset.panel === target;
|
|
2661
|
+
t.classList.toggle('active', selected);
|
|
2662
|
+
// Expose tab state + roving tabindex to assistive tech (WAI-ARIA APG
|
|
2663
|
+
// tabs pattern). Only the selected tab stays in the Tab sequence;
|
|
2664
|
+
// arrow keys move between the rest. (#1596)
|
|
2665
|
+
t.setAttribute('aria-selected', selected ? 'true' : 'false');
|
|
2666
|
+
t.tabIndex = selected ? 0 : -1;
|
|
2667
|
+
if (selected && focusTab) t.focus();
|
|
2668
|
+
});
|
|
2636
2669
|
document.querySelectorAll('.panel').forEach(p =>
|
|
2637
2670
|
p.classList.toggle('active', p.id === 'panel-' + target)
|
|
2638
2671
|
);
|
|
@@ -2649,6 +2682,27 @@ document.querySelectorAll('.tab').forEach(tab => {
|
|
|
2649
2682
|
tab.addEventListener('click', () => activateTab(tab.dataset.panel));
|
|
2650
2683
|
});
|
|
2651
2684
|
|
|
2685
|
+
// Keyboard navigation for the tablist (WAI-ARIA APG tabs pattern): Left/Right
|
|
2686
|
+
// move + activate the adjacent tab, Home/End jump to the ends. (#1596)
|
|
2687
|
+
{
|
|
2688
|
+
const tablist = document.querySelector('.tabs[role="tablist"]');
|
|
2689
|
+
if (tablist) {
|
|
2690
|
+
tablist.addEventListener('keydown', (e) => {
|
|
2691
|
+
const tabs = Array.from(tablist.querySelectorAll('.tab'));
|
|
2692
|
+
const currentIndex = tabs.indexOf(document.activeElement);
|
|
2693
|
+
if (currentIndex === -1) return;
|
|
2694
|
+
let nextIndex = null;
|
|
2695
|
+
if (e.key === 'ArrowRight') nextIndex = (currentIndex + 1) % tabs.length;
|
|
2696
|
+
else if (e.key === 'ArrowLeft') nextIndex = (currentIndex - 1 + tabs.length) % tabs.length;
|
|
2697
|
+
else if (e.key === 'Home') nextIndex = 0;
|
|
2698
|
+
else if (e.key === 'End') nextIndex = tabs.length - 1;
|
|
2699
|
+
if (nextIndex === null) return;
|
|
2700
|
+
e.preventDefault();
|
|
2701
|
+
activateTab(tabs[nextIndex].dataset.panel, { focusTab: true });
|
|
2702
|
+
});
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2652
2706
|
// Cross-tab links — any <a data-panel-link="<name>"> switches tabs
|
|
2653
2707
|
// in-page rather than following the href (used by the "no gated
|
|
2654
2708
|
// tools" empty state to point users at the Tools tab).
|
|
@@ -2858,8 +2912,8 @@ function _advSaveStatusEls() {
|
|
|
2858
2912
|
document.getElementById('advSaveStatusTop'),
|
|
2859
2913
|
].filter(Boolean);
|
|
2860
2914
|
}
|
|
2861
|
-
function _setAdvSaveStatus(text) {
|
|
2862
|
-
_advSaveStatusEls().forEach(el => { el.textContent = text; });
|
|
2915
|
+
function _setAdvSaveStatus(text, isError) {
|
|
2916
|
+
_advSaveStatusEls().forEach(el => { setStatusAlert(el, isError); el.textContent = text; });
|
|
2863
2917
|
}
|
|
2864
2918
|
function _setAdvSaveDisabled(disabled) {
|
|
2865
2919
|
_advSaveBtns().forEach(b => { b.disabled = disabled; });
|
|
@@ -2934,7 +2988,7 @@ async function saveAdvancedSettings() {
|
|
|
2934
2988
|
data = {restart_required: true};
|
|
2935
2989
|
} else {
|
|
2936
2990
|
_setAdvSaveDisabled(false);
|
|
2937
|
-
_setAdvSaveStatus(`Save failed (HTTP ${resp.status}, non-JSON body)
|
|
2991
|
+
_setAdvSaveStatus(`Save failed (HTTP ${resp.status}, non-JSON body)`, true);
|
|
2938
2992
|
return;
|
|
2939
2993
|
}
|
|
2940
2994
|
}
|
|
@@ -2945,7 +2999,7 @@ async function saveAdvancedSettings() {
|
|
|
2945
2999
|
if (typeof data.error === 'string') msg = data.error;
|
|
2946
3000
|
else if (data.error.message) msg = data.error.message;
|
|
2947
3001
|
}
|
|
2948
|
-
_setAdvSaveStatus(msg);
|
|
3002
|
+
_setAdvSaveStatus(msg, true);
|
|
2949
3003
|
return;
|
|
2950
3004
|
}
|
|
2951
3005
|
}
|
|
@@ -2973,7 +3027,7 @@ async function saveAdvancedSettings() {
|
|
|
2973
3027
|
}
|
|
2974
3028
|
} catch (err) {
|
|
2975
3029
|
_setAdvSaveDisabled(false);
|
|
2976
|
-
_setAdvSaveStatus('Network error: ' + String(err));
|
|
3030
|
+
_setAdvSaveStatus('Network error: ' + String(err), true);
|
|
2977
3031
|
}
|
|
2978
3032
|
}
|
|
2979
3033
|
|
|
@@ -787,6 +787,7 @@ _SETTINGS_HTML = (
|
|
|
787
787
|
+ """</style>
|
|
788
788
|
</head>
|
|
789
789
|
<body>
|
|
790
|
+
<a href="#main-content" class="skip-link">Skip to content</a>
|
|
790
791
|
<div class="header">
|
|
791
792
|
<h1>HA-MCP Settings</h1>
|
|
792
793
|
<div style="display:flex;align-items:center;gap:8px">
|
|
@@ -798,15 +799,16 @@ _SETTINGS_HTML = (
|
|
|
798
799
|
<option value="dark">Dark</option>
|
|
799
800
|
</select>
|
|
800
801
|
</label>
|
|
801
|
-
<span id="status" class="status">Loading...</span>
|
|
802
|
+
<span id="status" class="status" role="status" aria-live="polite">Loading...</span>
|
|
802
803
|
</div>
|
|
803
804
|
</div>
|
|
804
|
-
<
|
|
805
|
-
|
|
806
|
-
<button class="tab" data-panel="
|
|
807
|
-
<button class="tab" data-panel="
|
|
808
|
-
<button class="tab" data-panel="
|
|
809
|
-
<button class="tab" data-panel="
|
|
805
|
+
<main id="main-content" tabindex="-1" style="outline:none">
|
|
806
|
+
<div class="tabs" role="tablist" aria-label="Settings sections">
|
|
807
|
+
<button class="tab active" data-panel="tools" role="tab" id="tab-tools" aria-controls="panel-tools" aria-selected="true">Tools</button>
|
|
808
|
+
<button class="tab" data-panel="server" role="tab" id="tab-server" aria-controls="panel-server" aria-selected="false" tabindex="-1">Server Settings</button>
|
|
809
|
+
<button class="tab" data-panel="backups" role="tab" id="tab-backups" aria-controls="panel-backups" aria-selected="false" tabindex="-1">Backups</button>
|
|
810
|
+
<button class="tab" data-panel="tool-security-policies" role="tab" id="tab-tool-security-policies" aria-controls="panel-tool-security-policies" aria-selected="false" tabindex="-1">Tool Security Policies</button>
|
|
811
|
+
<button class="tab" data-panel="accessibility" role="tab" id="tab-accessibility" aria-controls="panel-accessibility" aria-selected="false" tabindex="-1">Accessibility</button>
|
|
810
812
|
</div>
|
|
811
813
|
<div class="restart-notice" id="restartNotice">
|
|
812
814
|
<span class="restart-notice-text" id="restartNoticeText">
|
|
@@ -819,7 +821,7 @@ _SETTINGS_HTML = (
|
|
|
819
821
|
</span>
|
|
820
822
|
<button class="restart-btn" id="restartBtn" style="display:none">Restart Add-on</button>
|
|
821
823
|
</div>
|
|
822
|
-
<div class="panel active" id="panel-tools">
|
|
824
|
+
<div class="panel active" id="panel-tools" role="tabpanel" aria-labelledby="tab-tools" tabindex="0">
|
|
823
825
|
<div class="readonly-notice">
|
|
824
826
|
Server-wide features (Tool Search, YAML config editing, filesystem
|
|
825
827
|
tools, etc.) appear in both the <strong>Server Settings</strong>
|
|
@@ -859,7 +861,7 @@ _SETTINGS_HTML = (
|
|
|
859
861
|
<input type="text" class="search" id="search" placeholder="Search tools...">
|
|
860
862
|
<div id="groups"></div>
|
|
861
863
|
</div>
|
|
862
|
-
<div class="panel" id="panel-server">
|
|
864
|
+
<div class="panel" id="panel-server" role="tabpanel" aria-labelledby="tab-server" tabindex="0">
|
|
863
865
|
<div class="features-sub">
|
|
864
866
|
Tool Search, advanced settings. Changes take effect only after you
|
|
865
867
|
restart the add-on (applies the change server-side) AND reconnect or
|
|
@@ -880,7 +882,7 @@ _SETTINGS_HTML = (
|
|
|
880
882
|
</div>
|
|
881
883
|
<div id="advSaveRowTop" class="adv-save-row" style="display:none;">
|
|
882
884
|
<button id="advSaveBtnTop" class="adv-save-btn">💾 Save advanced settings</button>
|
|
883
|
-
<span id="advSaveStatusTop" class="status"></span>
|
|
885
|
+
<span id="advSaveStatusTop" class="status" role="status" aria-live="polite"></span>
|
|
884
886
|
</div>
|
|
885
887
|
<div id="featuresBody"></div>
|
|
886
888
|
|
|
@@ -917,7 +919,7 @@ _SETTINGS_HTML = (
|
|
|
917
919
|
</div>
|
|
918
920
|
<div id="advSaveRow" class="adv-save-row" style="display:none;">
|
|
919
921
|
<button id="advSaveBtn" class="adv-save-btn">💾 Save advanced settings</button>
|
|
920
|
-
<span id="advSaveStatus" class="status"></span>
|
|
922
|
+
<span id="advSaveStatus" class="status" role="status" aria-live="polite"></span>
|
|
921
923
|
</div>
|
|
922
924
|
|
|
923
925
|
<div id="sidecarStopRow" style="display:none; margin: 16px 0; text-align: right;">
|
|
@@ -926,13 +928,13 @@ _SETTINGS_HTML = (
|
|
|
926
928
|
>Permanently disable settings server</button>
|
|
927
929
|
</div>
|
|
928
930
|
</div>
|
|
929
|
-
<div class="panel" id="panel-backups">
|
|
931
|
+
<div class="panel" id="panel-backups" role="tabpanel" aria-labelledby="tab-backups" tabindex="0">
|
|
930
932
|
<div class="backup-state" id="backupState">Loading backup state…</div>
|
|
931
933
|
<div class="backup-config" id="backupConfig">
|
|
932
934
|
<div class="backup-config-form" id="backupConfigForm"></div>
|
|
933
935
|
<div class="backup-config-actions" id="backupConfigActions" style="display:none">
|
|
934
936
|
<button id="backupConfigSave">Save settings</button>
|
|
935
|
-
<span id="backupConfigStatus" class="status"></span>
|
|
937
|
+
<span id="backupConfigStatus" class="status" role="status" aria-live="polite"></span>
|
|
936
938
|
</div>
|
|
937
939
|
</div>
|
|
938
940
|
<div class="backup-filters">
|
|
@@ -943,7 +945,7 @@ _SETTINGS_HTML = (
|
|
|
943
945
|
</div>
|
|
944
946
|
<div id="backupList"></div>
|
|
945
947
|
</div>
|
|
946
|
-
<div class="panel" id="panel-tool-security-policies">
|
|
948
|
+
<div class="panel" id="panel-tool-security-policies" role="tabpanel" aria-labelledby="tab-tool-security-policies" tabindex="0">
|
|
947
949
|
<h2>Tool Security Policies</h2>
|
|
948
950
|
<p class="features-sub">
|
|
949
951
|
Per-tool approval gating for high-stakes calls. Use the
|
|
@@ -991,7 +993,7 @@ _SETTINGS_HTML = (
|
|
|
991
993
|
</div>
|
|
992
994
|
<div style="margin-top:10px; display:flex; align-items:center; gap:12px">
|
|
993
995
|
<button id="policy-save-global-btn" class="restart-btn">Save global settings</button>
|
|
994
|
-
<span id="policy-global-save-status" class="status"></span>
|
|
996
|
+
<span id="policy-global-save-status" class="status" role="status" aria-live="polite"></span>
|
|
995
997
|
</div>
|
|
996
998
|
</section>
|
|
997
999
|
|
|
@@ -1010,7 +1012,7 @@ _SETTINGS_HTML = (
|
|
|
1010
1012
|
<div id="policy-rules-list"></div>
|
|
1011
1013
|
</section>
|
|
1012
1014
|
</div>
|
|
1013
|
-
<div class="panel" id="panel-accessibility">
|
|
1015
|
+
<div class="panel" id="panel-accessibility" role="tabpanel" aria-labelledby="tab-accessibility" tabindex="0">
|
|
1014
1016
|
<p class="tool-desc" style="margin-bottom:16px">
|
|
1015
1017
|
These settings apply immediately and are saved in this browser and on the
|
|
1016
1018
|
server, so they survive restarts in every mode (including stdio, where the
|
|
@@ -1064,6 +1066,7 @@ _SETTINGS_HTML = (
|
|
|
1064
1066
|
<button id="a11y-reset" class="restart-btn" type="button">Reset to defaults</button>
|
|
1065
1067
|
</section>
|
|
1066
1068
|
</div>
|
|
1069
|
+
</main>
|
|
1067
1070
|
<div class="modal-backdrop" id="modalBackdrop">
|
|
1068
1071
|
<div class="modal">
|
|
1069
1072
|
<div class="modal-header">
|
|
@@ -731,10 +731,126 @@ async def restore_backup(
|
|
|
731
731
|
return None # py/mixed-returns: explicit terminal; error handlers above always raise (NoReturn), unreachable
|
|
732
732
|
|
|
733
733
|
|
|
734
|
+
def _summarize_backup(entry: dict[str, Any]) -> dict[str, Any]:
|
|
735
|
+
"""Project one HA ``backup/info`` entry to the fields a caller needs to
|
|
736
|
+
identify and choose a snapshot (issue #1586).
|
|
737
|
+
|
|
738
|
+
Size is reported per backup agent; we surface the largest reported size
|
|
739
|
+
across agents. Every field is ``.get`` so a schema the running HA version
|
|
740
|
+
doesn't populate yields ``None`` rather than raising.
|
|
741
|
+
"""
|
|
742
|
+
agents = entry.get("agents") or {}
|
|
743
|
+
size_bytes: int | None = None
|
|
744
|
+
if isinstance(agents, dict):
|
|
745
|
+
sizes: list[int] = []
|
|
746
|
+
for a in agents.values():
|
|
747
|
+
if isinstance(a, dict):
|
|
748
|
+
size = a.get("size")
|
|
749
|
+
# Accept int or float (some agents report byte counts as float);
|
|
750
|
+
# bool is an int subclass but never a real size, so exclude it.
|
|
751
|
+
if isinstance(size, (int, float)) and not isinstance(size, bool):
|
|
752
|
+
sizes.append(int(size))
|
|
753
|
+
if sizes:
|
|
754
|
+
size_bytes = max(sizes)
|
|
755
|
+
return {
|
|
756
|
+
"backup_id": entry.get("backup_id"),
|
|
757
|
+
"name": entry.get("name"),
|
|
758
|
+
"date": entry.get("date"),
|
|
759
|
+
"size_bytes": size_bytes,
|
|
760
|
+
"protected": entry.get("protected"),
|
|
761
|
+
"database_included": entry.get("database_included"),
|
|
762
|
+
"homeassistant_included": entry.get("homeassistant_included"),
|
|
763
|
+
"homeassistant_version": entry.get("homeassistant_version"),
|
|
764
|
+
"with_automatic_settings": entry.get("with_automatic_settings"),
|
|
765
|
+
"agent_ids": list(agents.keys()) if isinstance(agents, dict) else [],
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
async def list_backups(client: HomeAssistantClient, limit: int = 200) -> dict[str, Any]:
|
|
770
|
+
"""List the full HA snapshot tarballs known to Home Assistant (issue #1586).
|
|
771
|
+
|
|
772
|
+
Surfaces the inventory HA already returns from its WebSocket ``backup/info``
|
|
773
|
+
command — the same data ``restore_backup`` uses internally to verify a
|
|
774
|
+
``backup_id`` exists. Before this, the snapshot scope exposed only
|
|
775
|
+
``create`` and ``restore``, so a caller had no way to discover backup IDs or
|
|
776
|
+
confirm a specific backup landed; they had to already know the ID. Newest
|
|
777
|
+
first. Read-only: no safety backup, no restart.
|
|
778
|
+
"""
|
|
779
|
+
ws_client = None
|
|
780
|
+
try:
|
|
781
|
+
ws_client, error = await get_connected_ws_client(
|
|
782
|
+
client.base_url, client.token, verify_ssl=client.verify_ssl
|
|
783
|
+
)
|
|
784
|
+
if error:
|
|
785
|
+
raise_tool_error(
|
|
786
|
+
error
|
|
787
|
+
or create_error_response(
|
|
788
|
+
ErrorCode.CONNECTION_FAILED,
|
|
789
|
+
"Failed to connect to Home Assistant WebSocket to list backups",
|
|
790
|
+
)
|
|
791
|
+
)
|
|
792
|
+
ws_client = cast(HomeAssistantWebSocketClient, ws_client)
|
|
793
|
+
|
|
794
|
+
info = await ws_client.send_command("backup/info")
|
|
795
|
+
if not info.get("success"):
|
|
796
|
+
raise_tool_error(
|
|
797
|
+
create_error_response(
|
|
798
|
+
ErrorCode.SERVICE_CALL_FAILED,
|
|
799
|
+
info.get("error", "Failed to retrieve backup information"),
|
|
800
|
+
)
|
|
801
|
+
)
|
|
802
|
+
|
|
803
|
+
result_block = info.get("result") or {}
|
|
804
|
+
raw_backups = result_block.get("backups") or []
|
|
805
|
+
summarized = [_summarize_backup(b) for b in raw_backups]
|
|
806
|
+
# Newest first so "did my backup land?" is answered by the top entry.
|
|
807
|
+
# Parse via _parse_backup_date (handles `Z`/naive) rather than sorting
|
|
808
|
+
# the raw strings lexicographically — `'Z'` > `'+'` would misorder a mix
|
|
809
|
+
# of `...Z` and `...+00:00`. Undated entries sink to the bottom.
|
|
810
|
+
_date_floor = datetime.min.replace(tzinfo=UTC)
|
|
811
|
+
summarized.sort(
|
|
812
|
+
key=lambda b: _parse_backup_date(b.get("date")) or _date_floor,
|
|
813
|
+
reverse=True,
|
|
814
|
+
)
|
|
815
|
+
total = len(summarized)
|
|
816
|
+
if limit and total > limit:
|
|
817
|
+
summarized = summarized[:limit]
|
|
818
|
+
return {
|
|
819
|
+
"success": True,
|
|
820
|
+
"count": len(summarized),
|
|
821
|
+
"total": total,
|
|
822
|
+
"backups": summarized,
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
except ToolError:
|
|
826
|
+
raise
|
|
827
|
+
except Exception as e:
|
|
828
|
+
logger.error(f"Error listing backups: {e}")
|
|
829
|
+
exception_to_structured_error(
|
|
830
|
+
e,
|
|
831
|
+
context={"tool": "list_backups"},
|
|
832
|
+
suggestions=["Check Home Assistant connection and the backup integration"],
|
|
833
|
+
)
|
|
834
|
+
return None # unreachable: exception_to_structured_error always raises
|
|
835
|
+
finally:
|
|
836
|
+
# Always disconnect WebSocket — narrow to transport errors; a
|
|
837
|
+
# programming error during cleanup should still surface.
|
|
838
|
+
if ws_client:
|
|
839
|
+
try:
|
|
840
|
+
await ws_client.disconnect()
|
|
841
|
+
except (TimeoutError, OSError, ConnectionError) as err:
|
|
842
|
+
logger.debug(
|
|
843
|
+
"ws disconnect (cleanup) transport error: %s: %s",
|
|
844
|
+
type(err).__name__,
|
|
845
|
+
err,
|
|
846
|
+
)
|
|
847
|
+
|
|
848
|
+
|
|
734
849
|
# Valid (scope, action) combinations. Anything outside this set is
|
|
735
850
|
# rejected with a structured VALIDATION_INVALID_PARAMETER error.
|
|
736
851
|
_VALID_COMBOS: set[tuple[str, str]] = {
|
|
737
852
|
("snapshot", "create"),
|
|
853
|
+
("snapshot", "list"),
|
|
738
854
|
("snapshot", "restore"),
|
|
739
855
|
("edits", "create"),
|
|
740
856
|
("edits", "list"),
|
|
@@ -799,6 +915,7 @@ def register_backup_tools(
|
|
|
799
915
|
| scope | action | What it does |
|
|
800
916
|
|---|---|---|
|
|
801
917
|
| `snapshot` | `create` | Create a full HA tarball (config + addons, no DB by default). Heavy, seconds-long. |
|
|
918
|
+
| `snapshot` | `list` | List full HA tarball snapshots (id, name, date, size). Read-only — use to discover a `backup_id` or confirm a backup landed. |
|
|
802
919
|
| `snapshot` | `restore` | Restore a full HA tarball. **Restarts HA.** Last-resort recovery. |
|
|
803
920
|
| `edits` | `create` | On-demand snapshot of one entity (`domain` + `entity_id` required). Use before the user manually edits in the HA UI. Same handler path the decorator takes on writes; bypasses the `enable_auto_backup` toggle. |
|
|
804
921
|
| `edits` | `list` | List per-entity auto-backups (lightweight). Filter by `domain` and/or `entity_id`. |
|
|
@@ -817,6 +934,7 @@ def register_backup_tools(
|
|
|
817
934
|
|
|
818
935
|
**Examples:**
|
|
819
936
|
- Snapshot before risky op: `ha_manage_backup(scope="snapshot", action="create", name="Before_Big_Change")`
|
|
937
|
+
- List snapshots (to discover a backup_id or confirm one landed): `ha_manage_backup(scope="snapshot", action="list")`
|
|
820
938
|
- Restore full snapshot: `ha_manage_backup(scope="snapshot", action="restore", backup_id="dd7550ed")`
|
|
821
939
|
- On-demand entity snapshot before a manual UI edit: `ha_manage_backup(scope="edits", action="create", domain="helper_input_boolean", entity_id="kitchen_lights_active")`
|
|
822
940
|
- List recent auto-backups for one automation: `ha_manage_backup(scope="edits", action="list", domain="automation", entity_id="kitchen_lights")`
|
|
@@ -906,7 +1024,7 @@ def register_backup_tools(
|
|
|
906
1024
|
default=200,
|
|
907
1025
|
ge=1,
|
|
908
1026
|
le=10_000,
|
|
909
|
-
description="(edits.list) Maximum number of entries to return.",
|
|
1027
|
+
description="(edits.list / snapshot.list) Maximum number of entries to return.",
|
|
910
1028
|
),
|
|
911
1029
|
] = 200,
|
|
912
1030
|
) -> dict[str, Any]:
|
|
@@ -916,6 +1034,8 @@ def register_backup_tools(
|
|
|
916
1034
|
if scope == "snapshot":
|
|
917
1035
|
if action == "create":
|
|
918
1036
|
return await create_backup(client, name)
|
|
1037
|
+
if action == "list":
|
|
1038
|
+
return await list_backups(client, limit)
|
|
919
1039
|
# action == "restore"
|
|
920
1040
|
bid = _require("backup_id", backup_id, scope, action)
|
|
921
1041
|
return await restore_backup(client, bid, restore_database)
|