ha-mcp-dev 7.13.0.dev1973__tar.gz → 7.13.0.dev1975__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ha_mcp_dev-7.13.0.dev1973/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.13.0.dev1975}/PKG-INFO +1 -1
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/pyproject.toml +1 -1
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/backup_manager.py +63 -21
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/backup.py +149 -20
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/component_api.py +85 -16
- ha_mcp_dev-7.13.0.dev1975/src/ha_mcp/tools/component_config_reads.py +212 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/component_devices.py +41 -6
- ha_mcp_dev-7.13.0.dev1975/src/ha_mcp/tools/component_registries.py +144 -0
- ha_mcp_dev-7.13.0.dev1975/src/ha_mcp/tools/component_registry_lookup.py +161 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/radio/base.py +12 -3
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/reference_validator.py +24 -4
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_areas.py +80 -55
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_blueprints.py +12 -1
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_config_automations.py +31 -2
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_config_dashboards.py +768 -16
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_config_helpers.py +58 -8
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_config_scenes.py +71 -4
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_dev.py +133 -16
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_entities.py +13 -3
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_integrations.py +413 -58
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_search.py +158 -40
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_services.py +114 -5
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_system.py +293 -28
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_voice_assistant.py +16 -4
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_zones.py +19 -3
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/util_helpers.py +31 -8
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/visibility/model.py +27 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/visibility/resolver.py +39 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp_dev.egg-info/SOURCES.txt +3 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/LICENSE +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/README.md +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/setup.cfg +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/browser_landing.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/llm_exposure.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/policy/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/policy/approval_queue.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/policy/evaluator.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/policy/handlers.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/policy/middleware.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/policy/model.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/policy/persistence.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/policy/value_sources.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/read_only.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/settings_ui/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/settings_ui/_i18n.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/settings_ui/_persistence.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/settings_ui/_theme.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/settings_ui/locales/README.md +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/settings_ui/locales/en.json +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/settings_ui/locales/ru.json +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/settings_ui/settings.css +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/settings_ui/settings.html +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/settings_ui/settings.js +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/strict_bps.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/auto_backup.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/config_entry_flow.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/radio/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/radio/matter.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/radio/thread.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/radio/zigbee.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/radio/zwave.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/smart_search/_base.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/smart_search/_config.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_radio.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_themes.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/validation_middleware.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/transport_security.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/update_check.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/utils/skill_loader.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/visibility/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/visibility/persistence.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/tests/test_env_manager.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ha-mcp-dev"
|
|
7
|
-
version = "7.13.0.
|
|
7
|
+
version = "7.13.0.dev1975"
|
|
8
8
|
description = "Home Assistant MCP Server - Complete control of Home Assistant through MCP"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
# Upper bound widened to <3.15 so the package installs on Home Assistant's
|
|
@@ -1296,14 +1296,18 @@ async def _restore_scene(client: Any, entity_id: str, config: Any) -> Any:
|
|
|
1296
1296
|
async def _fetch_dashboard(client: Any, entity_id: str) -> Any:
|
|
1297
1297
|
"""Fetch a dashboard config via the same helper the get tool uses.
|
|
1298
1298
|
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1299
|
+
The identifier is pre-resolved to its canonical url_path via the shared
|
|
1300
|
+
``_resolve_dashboard`` (component ``list`` when available), then the config is
|
|
1301
|
+
read through the component ``get`` (one in-process frame) with a fall back to
|
|
1302
|
+
the legacy ``lovelace/config`` read (``_get_dashboard_config_internal``, which
|
|
1303
|
+
handles the WS envelope, force-cache-bypass, and structured error wrapping).
|
|
1304
|
+
The component refuses YAML bodies, so those capture through legacy unchanged.
|
|
1305
|
+
Imported lazily to avoid an import cycle.
|
|
1303
1306
|
"""
|
|
1304
1307
|
from fastmcp.exceptions import ToolError
|
|
1305
1308
|
|
|
1306
1309
|
from .tools.tools_config_dashboards import (
|
|
1310
|
+
_component_dashboard_config,
|
|
1307
1311
|
_get_dashboard_config_internal,
|
|
1308
1312
|
_resolve_dashboard,
|
|
1309
1313
|
)
|
|
@@ -1329,6 +1333,12 @@ async def _fetch_dashboard(client: Any, entity_id: str) -> Any:
|
|
|
1329
1333
|
err,
|
|
1330
1334
|
)
|
|
1331
1335
|
|
|
1336
|
+
# Component fast path (freshness-safe in-memory read); None ⇒ legacy below,
|
|
1337
|
+
# which also covers YAML dashboards and not-found (nothing to back up).
|
|
1338
|
+
component_config = await _component_dashboard_config(client, fetch_path)
|
|
1339
|
+
if component_config is not None:
|
|
1340
|
+
return component_config
|
|
1341
|
+
|
|
1332
1342
|
try:
|
|
1333
1343
|
config, _config_hash = await _get_dashboard_config_internal(client, fetch_path)
|
|
1334
1344
|
except ToolError as err:
|
|
@@ -1442,10 +1452,21 @@ def _strip_readonly(config: dict[str, Any], *extra: str) -> dict[str, Any]:
|
|
|
1442
1452
|
|
|
1443
1453
|
|
|
1444
1454
|
async def _fetch_label(client: Any, entity_id: str) -> Any:
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
)
|
|
1455
|
+
# Route the capture through the component's ``registries`` capability when
|
|
1456
|
+
# available (one in-process read of the label registry) instead of dumping
|
|
1457
|
+
# the whole registry via WS. Lazy import to avoid the backup_manager →
|
|
1458
|
+
# tools → backup_manager cycle (same pattern as ``_fetch_device``). ``None``
|
|
1459
|
+
# from the helper means "component unavailable"; fall back to the full list.
|
|
1460
|
+
from .tools.component_registries import fetch_registries_via_component
|
|
1461
|
+
|
|
1462
|
+
component_result = await fetch_registries_via_component(client, ["label"])
|
|
1463
|
+
if component_result is not None:
|
|
1464
|
+
items = component_result.get("labels") or []
|
|
1465
|
+
else:
|
|
1466
|
+
items = _require_list(
|
|
1467
|
+
await _ws_send(client, {"type": "config/label_registry/list"}),
|
|
1468
|
+
"config/label_registry/list",
|
|
1469
|
+
)
|
|
1449
1470
|
for item in items:
|
|
1450
1471
|
if item.get("label_id") == entity_id:
|
|
1451
1472
|
return item
|
|
@@ -1466,12 +1487,22 @@ async def _fetch_category(client: Any, entity_id: str) -> Any:
|
|
|
1466
1487
|
scope, _, cat_id = entity_id.partition(":")
|
|
1467
1488
|
if not cat_id:
|
|
1468
1489
|
return None
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1490
|
+
# Same component-first routing as ``_fetch_label``; categories are scoped,
|
|
1491
|
+
# so the requested scope rides ``category_scopes``.
|
|
1492
|
+
from .tools.component_registries import fetch_registries_via_component
|
|
1493
|
+
|
|
1494
|
+
component_result = await fetch_registries_via_component(
|
|
1495
|
+
client, ["category"], category_scopes=[scope]
|
|
1474
1496
|
)
|
|
1497
|
+
if component_result is not None:
|
|
1498
|
+
items = (component_result.get("categories") or {}).get(scope, [])
|
|
1499
|
+
else:
|
|
1500
|
+
items = _require_list(
|
|
1501
|
+
await _ws_send(
|
|
1502
|
+
client, {"type": "config/category_registry/list", "scope": scope}
|
|
1503
|
+
),
|
|
1504
|
+
"config/category_registry/list",
|
|
1505
|
+
)
|
|
1475
1506
|
for item in items:
|
|
1476
1507
|
if item.get("category_id") == cat_id:
|
|
1477
1508
|
return {"scope": scope, **item}
|
|
@@ -1607,19 +1638,30 @@ async def _fetch_area_or_floor(client: Any, entity_id: str) -> Any:
|
|
|
1607
1638
|
kind, _, real_id = entity_id.partition(":")
|
|
1608
1639
|
if not real_id:
|
|
1609
1640
|
return None
|
|
1641
|
+
# Same component-first routing as ``_fetch_label`` / ``_fetch_category``.
|
|
1642
|
+
from .tools.component_registries import fetch_registries_via_component
|
|
1643
|
+
|
|
1610
1644
|
if kind == "area":
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
"
|
|
1614
|
-
|
|
1645
|
+
component_result = await fetch_registries_via_component(client, ["area"])
|
|
1646
|
+
if component_result is not None:
|
|
1647
|
+
items = component_result.get("areas") or []
|
|
1648
|
+
else:
|
|
1649
|
+
items = _require_list(
|
|
1650
|
+
await _ws_send(client, {"type": "config/area_registry/list"}),
|
|
1651
|
+
"config/area_registry/list",
|
|
1652
|
+
)
|
|
1615
1653
|
for item in items:
|
|
1616
1654
|
if item.get("area_id") == real_id:
|
|
1617
1655
|
return {"kind": "area", **item}
|
|
1618
1656
|
elif kind == "floor":
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
"
|
|
1622
|
-
|
|
1657
|
+
component_result = await fetch_registries_via_component(client, ["floor"])
|
|
1658
|
+
if component_result is not None:
|
|
1659
|
+
items = component_result.get("floors") or []
|
|
1660
|
+
else:
|
|
1661
|
+
items = _require_list(
|
|
1662
|
+
await _ws_send(client, {"type": "config/floor_registry/list"}),
|
|
1663
|
+
"config/floor_registry/list",
|
|
1664
|
+
)
|
|
1623
1665
|
for item in items:
|
|
1624
1666
|
if item.get("floor_id") == real_id:
|
|
1625
1667
|
return {"kind": "floor", **item}
|
|
@@ -30,10 +30,21 @@ from ..backup_manager import (
|
|
|
30
30
|
MandatoryBackupError,
|
|
31
31
|
get_backup_manager,
|
|
32
32
|
)
|
|
33
|
-
from ..client.rest_client import
|
|
34
|
-
|
|
33
|
+
from ..client.rest_client import (
|
|
34
|
+
HomeAssistantClient,
|
|
35
|
+
HomeAssistantCommandError,
|
|
36
|
+
HomeAssistantCommandTimeout,
|
|
37
|
+
HomeAssistantError,
|
|
38
|
+
)
|
|
39
|
+
from ..client.websocket_client import HomeAssistantWebSocketClient, get_websocket_client
|
|
35
40
|
from ..config import get_global_settings
|
|
36
41
|
from ..errors import ErrorCode, create_error_response
|
|
42
|
+
from .component_api import (
|
|
43
|
+
component_supports,
|
|
44
|
+
get_component_caps,
|
|
45
|
+
invalidate_caps,
|
|
46
|
+
is_unknown_command,
|
|
47
|
+
)
|
|
37
48
|
from .helpers import (
|
|
38
49
|
exception_to_structured_error,
|
|
39
50
|
get_connected_ws_client,
|
|
@@ -89,6 +100,9 @@ _BACKUP_PROGRESS_INTERVAL_S = 10
|
|
|
89
100
|
# _wait_timeout) can be too tight for a slow or rate-limited remote agent —
|
|
90
101
|
# same class of false-timeout problem this file already fixes for creation.
|
|
91
102
|
_BACKUP_DELETE_WAIT_S = 60.0
|
|
103
|
+
# WS command name is a module-local constant per the component-routing seam
|
|
104
|
+
# pattern (see component_api.py / component_devices.py).
|
|
105
|
+
WS_BACKUP_PREP = "ha_mcp_tools/backup_prep"
|
|
92
106
|
|
|
93
107
|
|
|
94
108
|
def _get_backup_hint_text() -> str:
|
|
@@ -217,6 +231,91 @@ async def _get_backup_password(
|
|
|
217
231
|
return cast(str, default_password)
|
|
218
232
|
|
|
219
233
|
|
|
234
|
+
async def _backup_prep_via_component(
|
|
235
|
+
client: HomeAssistantClient,
|
|
236
|
+
) -> dict[str, Any] | None:
|
|
237
|
+
"""One ``ha_mcp_tools/backup_prep`` read; ``None`` ⇒ run the legacy two-call path.
|
|
238
|
+
|
|
239
|
+
Returns the component's ``{agent_ids, local_agent_id, default_password}``
|
|
240
|
+
payload, replacing the sequential ``backup/agents/info`` (local-agent
|
|
241
|
+
discovery, :func:`_get_local_backup_agent_id`) + ``backup/config/info``
|
|
242
|
+
(default password, :func:`_get_backup_password`) WS round-trips with one
|
|
243
|
+
in-process read. ``None`` on capability miss, downgrade (``unknown_command``
|
|
244
|
+
→ invalidate the cached caps), or command error/timeout (logged) — the
|
|
245
|
+
caller falls back to the legacy sequential calls. A
|
|
246
|
+
``HomeAssistantConnectionError`` (pooled-WS drop) or the plain ``Exception``
|
|
247
|
+
``get_websocket_client()`` raises on a failed (re)connect is caught here and
|
|
248
|
+
mapped to ``None``: the legacy probes (``_get_local_backup_agent_id`` /
|
|
249
|
+
``_get_backup_password``) run on a DEDICATED ``get_connected_ws_client`` socket
|
|
250
|
+
already connected before this read, NOT this pooled one — so a wedged pooled
|
|
251
|
+
socket must fall back to the legacy calls rather than fail create/restore.
|
|
252
|
+
Same caps-gate discipline as ``component_devices.fetch_device_via_component``.
|
|
253
|
+
"""
|
|
254
|
+
caps = await get_component_caps(client)
|
|
255
|
+
if not component_supports(caps, "backup_prep"):
|
|
256
|
+
return None
|
|
257
|
+
try:
|
|
258
|
+
ws = await get_websocket_client(url=client.base_url, token=client.token)
|
|
259
|
+
raw = await ws.send_command(WS_BACKUP_PREP)
|
|
260
|
+
except (HomeAssistantCommandError, HomeAssistantCommandTimeout) as exc:
|
|
261
|
+
if is_unknown_command(exc):
|
|
262
|
+
invalidate_caps(client)
|
|
263
|
+
else:
|
|
264
|
+
logger.warning("%s failed; fell back to legacy: %r", WS_BACKUP_PREP, exc)
|
|
265
|
+
return None
|
|
266
|
+
except Exception as exc:
|
|
267
|
+
# HomeAssistantConnectionError (pooled-WS drop) OR the plain Exception
|
|
268
|
+
# get_websocket_client() raises on a failed (re)connect. The legacy probes
|
|
269
|
+
# ride a dedicated already-connected socket, so fall back rather than fail
|
|
270
|
+
# create/restore on a wedged pooled socket.
|
|
271
|
+
logger.warning(
|
|
272
|
+
"%s connection error; falling back to legacy: %r", WS_BACKUP_PREP, exc
|
|
273
|
+
)
|
|
274
|
+
return None
|
|
275
|
+
result = raw.get("result")
|
|
276
|
+
if not isinstance(result, dict) or "local_agent_id" not in result:
|
|
277
|
+
logger.debug(
|
|
278
|
+
"%s returned a malformed result (missing 'local_agent_id' key); "
|
|
279
|
+
"falling back to legacy",
|
|
280
|
+
WS_BACKUP_PREP,
|
|
281
|
+
)
|
|
282
|
+
return None
|
|
283
|
+
return result
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _raise_no_local_backup_agent_error(agent_ids: list[Any] | None) -> NoReturn:
|
|
287
|
+
"""Same "no local agent" error ``_get_local_backup_agent_id`` raises.
|
|
288
|
+
|
|
289
|
+
Used when the ``backup_prep`` component read comes back with
|
|
290
|
+
``local_agent_id: None`` — the SAME outcome as the legacy probe finding no
|
|
291
|
+
agent named ``"local"``, so both paths fail identically.
|
|
292
|
+
"""
|
|
293
|
+
raise_tool_error(
|
|
294
|
+
create_error_response(
|
|
295
|
+
ErrorCode.SERVICE_CALL_FAILED,
|
|
296
|
+
"No local backup agent found",
|
|
297
|
+
context={"available_agents": [a for a in (agent_ids or []) if a]},
|
|
298
|
+
suggestions=[
|
|
299
|
+
"Backup creation requires a local agent (hassio.local on "
|
|
300
|
+
"Supervised, backup.local on Core); none is registered",
|
|
301
|
+
],
|
|
302
|
+
)
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _raise_no_default_password_error() -> NoReturn:
|
|
307
|
+
"""Same error ``_get_backup_password`` raises when no default password is configured."""
|
|
308
|
+
raise_tool_error(
|
|
309
|
+
create_error_response(
|
|
310
|
+
ErrorCode.SERVICE_CALL_FAILED,
|
|
311
|
+
"No default backup password configured in Home Assistant",
|
|
312
|
+
suggestions=[
|
|
313
|
+
"Configure automatic backups in Home Assistant settings to set a default password"
|
|
314
|
+
],
|
|
315
|
+
)
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
|
|
220
319
|
def _parse_backup_date(raw: Any) -> datetime | None:
|
|
221
320
|
"""Parse an HA backup `date` (ISO-8601, may use `Z` suffix) to a tz-aware
|
|
222
321
|
datetime, returning None on missing/malformed input. Naive timestamps are
|
|
@@ -530,13 +629,24 @@ async def create_backup(
|
|
|
530
629
|
)
|
|
531
630
|
ws_client = cast(HomeAssistantWebSocketClient, ws_client)
|
|
532
631
|
|
|
533
|
-
#
|
|
534
|
-
|
|
632
|
+
# One component read replaces the sequential password + local-agent
|
|
633
|
+
# probes when the ha_mcp_tools component supports backup_prep.
|
|
634
|
+
prep = await _backup_prep_via_component(client)
|
|
635
|
+
if prep is not None:
|
|
636
|
+
password = prep.get("default_password")
|
|
637
|
+
if not password:
|
|
638
|
+
_raise_no_default_password_error()
|
|
639
|
+
local_agent = prep.get("local_agent_id")
|
|
640
|
+
if not local_agent:
|
|
641
|
+
_raise_no_local_backup_agent_error(prep.get("agent_ids"))
|
|
642
|
+
else:
|
|
643
|
+
# Get backup password (raises ToolError on failure)
|
|
644
|
+
password = await _get_backup_password(ws_client)
|
|
535
645
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
646
|
+
# Discover the local backup agent at call time. HA Core registers
|
|
647
|
+
# `backup.local`; HA Supervised registers `hassio.local`. Hardcoding
|
|
648
|
+
# either breaks the other deployment.
|
|
649
|
+
local_agent = await _get_local_backup_agent_id(ws_client)
|
|
540
650
|
|
|
541
651
|
# Generate backup name if not provided
|
|
542
652
|
if not name:
|
|
@@ -768,19 +878,38 @@ async def restore_backup(
|
|
|
768
878
|
)
|
|
769
879
|
)
|
|
770
880
|
|
|
771
|
-
#
|
|
772
|
-
#
|
|
773
|
-
|
|
774
|
-
|
|
881
|
+
# One component read replaces the sequential local-agent + password
|
|
882
|
+
# probes when the ha_mcp_tools component supports backup_prep.
|
|
883
|
+
prep = await _backup_prep_via_component(client)
|
|
884
|
+
if prep is not None:
|
|
885
|
+
# Local backup agent (Supervisor's hassio.local on Supervised,
|
|
886
|
+
# backup.local on Core). Used for both the safety backup and the
|
|
887
|
+
# restore call below.
|
|
888
|
+
local_agent = prep.get("local_agent_id")
|
|
889
|
+
if not local_agent:
|
|
890
|
+
_raise_no_local_backup_agent_error(prep.get("agent_ids"))
|
|
891
|
+
|
|
892
|
+
# Create safety backup BEFORE restoring
|
|
893
|
+
logger.info("Creating safety backup before restore...")
|
|
894
|
+
password = prep.get("default_password")
|
|
895
|
+
if not password:
|
|
896
|
+
# No default password - log warning but continue (restore might still work)
|
|
897
|
+
logger.warning("No default password - proceeding without safety backup")
|
|
898
|
+
password = None
|
|
899
|
+
else:
|
|
900
|
+
# Discover the local backup agent (Supervisor's hassio.local on
|
|
901
|
+
# Supervised, backup.local on Core). Used for both the safety backup
|
|
902
|
+
# and the restore call below.
|
|
903
|
+
local_agent = await _get_local_backup_agent_id(ws_client)
|
|
775
904
|
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
905
|
+
# Create safety backup BEFORE restoring
|
|
906
|
+
logger.info("Creating safety backup before restore...")
|
|
907
|
+
try:
|
|
908
|
+
password = await _get_backup_password(ws_client)
|
|
909
|
+
except ToolError:
|
|
910
|
+
# Password error - log warning but continue (restore might still work)
|
|
911
|
+
logger.warning("No default password - proceeding without safety backup")
|
|
912
|
+
password = None
|
|
784
913
|
|
|
785
914
|
safety_backup_id, safety_warnings = await _create_safety_backup(
|
|
786
915
|
ws_client, password, local_agent, ctx=ctx
|
|
@@ -62,9 +62,20 @@ SUPPORTED_SCHEMA_VERSION = 1
|
|
|
62
62
|
# (the cache key is the process-lifetime REST client — see ``_CAPS_CACHE``).
|
|
63
63
|
# Positive entries never expire on this timer; they are dropped only by
|
|
64
64
|
# ``invalidate_caps`` (a supposedly-supported command coming back
|
|
65
|
-
# ``unknown_command``).
|
|
65
|
+
# ``unknown_command``). This ABSENT-negative window covers a definitive
|
|
66
|
+
# "component responded, no usable surface" verdict (``unknown_command`` /
|
|
67
|
+
# malformed / unsupported schema_version).
|
|
66
68
|
_NEGATIVE_CACHE_TTL_S = 300.0
|
|
67
69
|
|
|
70
|
+
# A separate, much SHORTER window for a TRANSIENT negative: the probe couldn't
|
|
71
|
+
# reach the component at all (WS down / connect failure), which is not a verdict
|
|
72
|
+
# about the component, only about the transport. Caching it briefly stops a
|
|
73
|
+
# WS-broken install from re-paying the slow connect attempt on every tool call
|
|
74
|
+
# (issue #1813 Phase 2, review-5 M8) while still self-healing quickly once the
|
|
75
|
+
# socket recovers. Kept short precisely because the transport may come back at
|
|
76
|
+
# any moment — unlike the absent-negative, which reflects a stable answer.
|
|
77
|
+
_TRANSIENT_NEGATIVE_CACHE_TTL_S = 30.0
|
|
78
|
+
|
|
68
79
|
|
|
69
80
|
def _monotonic() -> float:
|
|
70
81
|
"""Monotonic clock read, isolated so tests can advance it deterministically."""
|
|
@@ -79,13 +90,18 @@ class ComponentCaps:
|
|
|
79
90
|
commands the component actually registered. ``schema_version`` guards the
|
|
80
91
|
wire-format generation of those commands (a consumer needing a reshaped
|
|
81
92
|
payload checks ``schema_version >= N``). ``component_version`` and
|
|
82
|
-
``limits`` are advisory (display / body-size caps).
|
|
93
|
+
``limits`` are advisory (display / body-size caps). ``timezone`` is the
|
|
94
|
+
instance's ``hass.config.time_zone`` (an additive ``info`` field — ``None``
|
|
95
|
+
on a component too old to report it, or when unset); it is cached here for
|
|
96
|
+
the consumers that localize timestamps (issue #1813 Phase 2), but nothing
|
|
97
|
+
routes on it — capability negotiation, not a timezone floor.
|
|
83
98
|
"""
|
|
84
99
|
|
|
85
100
|
schema_version: int
|
|
86
101
|
component_version: str
|
|
87
102
|
capabilities: frozenset[str]
|
|
88
103
|
limits: dict[str, Any]
|
|
104
|
+
timezone: str | None = None
|
|
89
105
|
|
|
90
106
|
|
|
91
107
|
# Weak-keyed by client so the negotiated caps self-evict when the client is
|
|
@@ -94,14 +110,23 @@ class ComponentCaps:
|
|
|
94
110
|
# so a positive entry persists for the whole process (dropped only by
|
|
95
111
|
# ``invalidate_caps`` on an ``unknown_command``). A ``None`` value is a cached
|
|
96
112
|
# *negative* ("probed, no usable WS surface"); absence means "not yet probed".
|
|
97
|
-
#
|
|
98
|
-
#
|
|
113
|
+
# A negative carries an expiry in EXACTLY ONE of the two timestamp maps below —
|
|
114
|
+
# ``_NEGATIVE_CACHE_TS`` (definitive absent, long) or ``_TRANSIENT_NEGATIVE_TS``
|
|
115
|
+
# (transport-only, short) — so a component installed/upgraded mid-session, or one
|
|
116
|
+
# that just became reachable again, is eventually re-probed instead of pinned.
|
|
99
117
|
_CAPS_CACHE: weakref.WeakKeyDictionary[Any, ComponentCaps | None] = (
|
|
100
118
|
weakref.WeakKeyDictionary()
|
|
101
119
|
)
|
|
102
|
-
# Monotonic timestamp a negative
|
|
103
|
-
# Only populated for ``None`` values; positive entries never appear here.
|
|
120
|
+
# Monotonic timestamp a definitive ABSENT-negative was stored, keyed by the same
|
|
121
|
+
# client. Only populated for ``None`` values; positive entries never appear here.
|
|
104
122
|
_NEGATIVE_CACHE_TS: weakref.WeakKeyDictionary[Any, float] = weakref.WeakKeyDictionary()
|
|
123
|
+
# Monotonic timestamp a TRANSIENT (transport-failure) negative was stored. Kept
|
|
124
|
+
# separate from ``_NEGATIVE_CACHE_TS`` so the two negative kinds carry different
|
|
125
|
+
# TTLs; a client is stamped in at most one of the two at a time (the store helpers
|
|
126
|
+
# clear the other).
|
|
127
|
+
_TRANSIENT_NEGATIVE_TS: weakref.WeakKeyDictionary[Any, float] = (
|
|
128
|
+
weakref.WeakKeyDictionary()
|
|
129
|
+
)
|
|
105
130
|
_CAPS_LOCKS: weakref.WeakKeyDictionary[Any, asyncio.Lock] = weakref.WeakKeyDictionary()
|
|
106
131
|
|
|
107
132
|
|
|
@@ -118,30 +143,60 @@ def _live_cache_entry(client: Any) -> tuple[bool, ComponentCaps | None]:
|
|
|
118
143
|
"""Return ``(hit, caps)`` for a still-valid cache entry, else ``(False, None)``.
|
|
119
144
|
|
|
120
145
|
A positive entry is a hit for the process lifetime. A negative (``None``)
|
|
121
|
-
entry is a hit only within
|
|
122
|
-
|
|
123
|
-
|
|
146
|
+
entry is a hit only within its window of when it was stored (monotonic clock):
|
|
147
|
+
``_NEGATIVE_CACHE_TTL_S`` for a definitive absent-negative, the shorter
|
|
148
|
+
``_TRANSIENT_NEGATIVE_CACHE_TTL_S`` for a transport-failure negative. Once the
|
|
149
|
+
relevant window lapses it reports a miss so the caller re-probes and can adopt a
|
|
150
|
+
component that appeared — or became reachable — mid-session.
|
|
124
151
|
"""
|
|
125
152
|
if client not in _CAPS_CACHE:
|
|
126
153
|
return False, None
|
|
127
154
|
cached = _CAPS_CACHE[client]
|
|
128
155
|
if cached is not None:
|
|
129
156
|
return True, cached
|
|
157
|
+
now = _monotonic()
|
|
130
158
|
stored_at = _NEGATIVE_CACHE_TS.get(client)
|
|
131
|
-
if stored_at is not None and (
|
|
159
|
+
if stored_at is not None and (now - stored_at) < _NEGATIVE_CACHE_TTL_S:
|
|
160
|
+
return True, None
|
|
161
|
+
transient_at = _TRANSIENT_NEGATIVE_TS.get(client)
|
|
162
|
+
if (
|
|
163
|
+
transient_at is not None
|
|
164
|
+
and (now - transient_at) < _TRANSIENT_NEGATIVE_CACHE_TTL_S
|
|
165
|
+
):
|
|
132
166
|
return True, None
|
|
133
167
|
return False, None
|
|
134
168
|
|
|
135
169
|
|
|
136
170
|
def _store_caps(client: Any, caps: ComponentCaps | None) -> None:
|
|
137
|
-
"""Cache
|
|
171
|
+
"""Cache a positive result or a definitive ABSENT-negative for ``client``.
|
|
172
|
+
|
|
173
|
+
A ``None`` here is the long-window absent-negative (``info`` responded
|
|
174
|
+
``unknown_command`` / malformed / unsupported schema). Clears any prior
|
|
175
|
+
transient-negative stamp so a client never carries both negative kinds.
|
|
176
|
+
"""
|
|
138
177
|
_CAPS_CACHE[client] = caps
|
|
178
|
+
_TRANSIENT_NEGATIVE_TS.pop(client, None)
|
|
139
179
|
if caps is None:
|
|
140
180
|
_NEGATIVE_CACHE_TS[client] = _monotonic()
|
|
141
181
|
else:
|
|
142
182
|
_NEGATIVE_CACHE_TS.pop(client, None)
|
|
143
183
|
|
|
144
184
|
|
|
185
|
+
def _store_transient_negative(client: Any) -> None:
|
|
186
|
+
"""Cache a SHORT transient negative after a transport-failure probe.
|
|
187
|
+
|
|
188
|
+
The probe could not reach the component (WS down / connect failure), so cache
|
|
189
|
+
``None`` with the short ``_TRANSIENT_NEGATIVE_CACHE_TTL_S`` window: repeated
|
|
190
|
+
calls inside it skip re-paying the slow connect and go straight to legacy
|
|
191
|
+
(issue #1813 Phase 2, review-5 M8), then the next call past the window re-probes
|
|
192
|
+
and adopts a now-reachable component (self-healing). Clears any prior
|
|
193
|
+
absent-negative stamp so a client never carries both negative kinds.
|
|
194
|
+
"""
|
|
195
|
+
_CAPS_CACHE[client] = None
|
|
196
|
+
_NEGATIVE_CACHE_TS.pop(client, None)
|
|
197
|
+
_TRANSIENT_NEGATIVE_TS[client] = _monotonic()
|
|
198
|
+
|
|
199
|
+
|
|
145
200
|
def _parse_caps(response: Any) -> ComponentCaps | None:
|
|
146
201
|
"""Map an ``ha_mcp_tools/info`` response into ``ComponentCaps``.
|
|
147
202
|
|
|
@@ -162,11 +217,15 @@ def _parse_caps(response: Any) -> ComponentCaps | None:
|
|
|
162
217
|
schema_version = int(result.get("schema_version", 0) or 0)
|
|
163
218
|
except (TypeError, ValueError):
|
|
164
219
|
schema_version = 0
|
|
220
|
+
raw_timezone = result.get("timezone")
|
|
165
221
|
return ComponentCaps(
|
|
166
222
|
schema_version=schema_version,
|
|
167
223
|
component_version=str(result.get("component_version", "")),
|
|
168
224
|
capabilities=capabilities,
|
|
169
225
|
limits=raw_limits if isinstance(raw_limits, dict) else {},
|
|
226
|
+
# Additive info field: a component too old to report it (or an unset
|
|
227
|
+
# time_zone) leaves this None; a non-string value is ignored likewise.
|
|
228
|
+
timezone=raw_timezone if isinstance(raw_timezone, str) else None,
|
|
170
229
|
)
|
|
171
230
|
|
|
172
231
|
|
|
@@ -184,13 +243,17 @@ async def get_component_caps(client: Any) -> ComponentCaps | None:
|
|
|
184
243
|
installed / upgraded mid-session (the REST client — the cache key — is not
|
|
185
244
|
recreated on an HA restart) is eventually adopted instead of pinned absent.
|
|
186
245
|
- ``HomeAssistantConnectionError`` / ``HomeAssistantCommandTimeout`` (WS
|
|
187
|
-
down or slow)
|
|
188
|
-
|
|
189
|
-
|
|
246
|
+
down or slow) — and the plain ``Exception`` ``get_websocket_client()`` raises
|
|
247
|
+
when ``WebSocketManager`` can't build the socket: cache a SHORT transient
|
|
248
|
+
negative (``_TRANSIENT_NEGATIVE_CACHE_TTL_S``) so repeated calls on a
|
|
249
|
+
WS-broken install skip the slow connect and go straight to legacy, then
|
|
250
|
+
re-probe once the window lapses (self-healing). The consuming tool's legacy
|
|
251
|
+
path serves the request meanwhile.
|
|
190
252
|
- No credentials on the client (a bare test double with no ``base_url`` /
|
|
191
253
|
``token``): nothing to probe; return ``None`` without caching.
|
|
192
|
-
- Any other unexpected exception: logged at debug,
|
|
193
|
-
|
|
254
|
+
- Any other unexpected exception: logged at debug, cached as the same short
|
|
255
|
+
transient negative (dominated by the connect-establishment failure above; a
|
|
256
|
+
genuine runtime fault re-probes after the window).
|
|
194
257
|
|
|
195
258
|
A probe whose ``schema_version`` is not ``SUPPORTED_SCHEMA_VERSION`` is
|
|
196
259
|
treated as no-caps (cached negative, logged once): the server can't trust
|
|
@@ -223,9 +286,14 @@ async def get_component_caps(client: Any) -> ComponentCaps | None:
|
|
|
223
286
|
logger.debug(
|
|
224
287
|
"%s probe skipped: WS unavailable", INFO_COMMAND, exc_info=True
|
|
225
288
|
)
|
|
289
|
+
_store_transient_negative(client)
|
|
226
290
|
return None
|
|
227
291
|
except Exception:
|
|
292
|
+
# Includes the plain Exception get_websocket_client() raises when
|
|
293
|
+
# WebSocketManager can't build the socket — the connect-establishment
|
|
294
|
+
# failure review-5 M8 targets. Short transient cache, self-healing.
|
|
228
295
|
logger.debug("%s probe failed unexpectedly", INFO_COMMAND, exc_info=True)
|
|
296
|
+
_store_transient_negative(client)
|
|
229
297
|
return None
|
|
230
298
|
|
|
231
299
|
caps = _parse_caps(response)
|
|
@@ -265,3 +333,4 @@ def invalidate_caps(client: Any) -> None:
|
|
|
265
333
|
"""
|
|
266
334
|
_CAPS_CACHE.pop(client, None)
|
|
267
335
|
_NEGATIVE_CACHE_TS.pop(client, None)
|
|
336
|
+
_TRANSIENT_NEGATIVE_TS.pop(client, None)
|