ha-mcp-dev 7.13.0.dev2011__tar.gz → 7.13.0.dev2013__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.dev2011/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.13.0.dev2013}/PKG-INFO +1 -1
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/pyproject.toml +1 -4
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/backup_manager.py +200 -116
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/auto_backup.py +105 -51
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/backup.py +887 -625
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/LICENSE +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/README.md +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/setup.cfg +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/browser_landing.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/dashboard_screenshot/theme_guard.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/llm_exposure.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/policy/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/policy/approval_queue.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/policy/evaluator.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/policy/handlers.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/policy/middleware.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/policy/model.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/policy/persistence.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/policy/value_sources.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/read_only.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/settings_ui/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/settings_ui/_i18n.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/settings_ui/_persistence.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/settings_ui/_theme.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/settings_ui/locales/README.md +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/settings_ui/locales/de.json +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/settings_ui/locales/en.json +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/settings_ui/locales/ru.json +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/settings_ui/settings.css +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/settings_ui/settings.html +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/settings_ui/settings.js +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/strict_bps.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/component_api.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/component_config_reads.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/component_devices.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/component_registries.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/component_registry_lookup.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/config_entry_flow.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/radio/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/radio/base.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/radio/matter.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/radio/thread.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/radio/zigbee.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/radio/zwave.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/smart_search/_base.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/smart_search/_config.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_dev.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_radio.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_themes.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/validation_middleware.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/transport_security.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/update_check.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/utils/skill_loader.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/visibility/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/visibility/model.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/visibility/persistence.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/visibility/resolver.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/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.dev2013"
|
|
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
|
|
@@ -175,9 +175,6 @@ ignore = [
|
|
|
175
175
|
"homeassistant-addon-webhook-proxy/mcp_proxy/__init__.py" = ["C901"]
|
|
176
176
|
"homeassistant-addon-webhook-proxy/start.py" = ["C901"]
|
|
177
177
|
"scripts/extract_tools.py" = ["C901"]
|
|
178
|
-
"src/ha_mcp/backup_manager.py" = ["C901"]
|
|
179
|
-
"src/ha_mcp/tools/auto_backup.py" = ["C901"]
|
|
180
|
-
"src/ha_mcp/tools/backup.py" = ["C901"]
|
|
181
178
|
"src/ha_mcp/tools/tools_config_automations.py" = ["C901"]
|
|
182
179
|
"src/ha_mcp/tools/tools_config_scenes.py" = ["C901"]
|
|
183
180
|
"src/ha_mcp/tools/tools_config_scripts.py" = ["C901"]
|
|
@@ -195,6 +195,13 @@ def _resolve_default_dir() -> Path:
|
|
|
195
195
|
return (Path.home() / ".local" / "share" / "ha_mcp" / "backups").resolve()
|
|
196
196
|
|
|
197
197
|
|
|
198
|
+
# Sentinel returned by ``BackupManager._fetch_config_for_snapshot`` when there
|
|
199
|
+
# is nothing to snapshot — a handled transient fetch failure (already logged),
|
|
200
|
+
# or a None config for an entity that did not exist. Distinct from any real
|
|
201
|
+
# config value so the caller can tell "skip" from a genuine payload.
|
|
202
|
+
_SNAPSHOT_SKIP: Any = object()
|
|
203
|
+
|
|
204
|
+
|
|
198
205
|
class BackupManager:
|
|
199
206
|
"""Per-entity snapshot manager. One instance per server, cached on client."""
|
|
200
207
|
|
|
@@ -317,6 +324,48 @@ class BackupManager:
|
|
|
317
324
|
apply (force can't conjure a snapshot for an entity that
|
|
318
325
|
doesn't exist or has no registered handler).
|
|
319
326
|
"""
|
|
327
|
+
handler = self._resolve_snapshot_handler(
|
|
328
|
+
domain, entity_id, force=force, mandatory=mandatory
|
|
329
|
+
)
|
|
330
|
+
if handler is None:
|
|
331
|
+
return None
|
|
332
|
+
|
|
333
|
+
key = f"{domain}:{entity_id}"
|
|
334
|
+
lock = self._locks.setdefault(key, asyncio.Lock())
|
|
335
|
+
async with lock:
|
|
336
|
+
now = time.monotonic()
|
|
337
|
+
throttle = self.throttle_seconds
|
|
338
|
+
# Skip throttle if no prior snapshot exists for this key.
|
|
339
|
+
# Using ``get(key, 0.0)`` would falsely block the first capture
|
|
340
|
+
# whenever ``monotonic()`` < throttle (typical on a fresh process
|
|
341
|
+
# in CI), since 0.0 would be treated as "last snapshot at
|
|
342
|
+
# monotonic time 0".
|
|
343
|
+
if (
|
|
344
|
+
not force
|
|
345
|
+
and throttle
|
|
346
|
+
and key in self._last_snapshot
|
|
347
|
+
and (now - self._last_snapshot[key]) < throttle
|
|
348
|
+
):
|
|
349
|
+
return None
|
|
350
|
+
config = await self._fetch_config_for_snapshot(
|
|
351
|
+
handler, entity_id, key, mandatory=mandatory
|
|
352
|
+
)
|
|
353
|
+
if config is _SNAPSHOT_SKIP:
|
|
354
|
+
return None
|
|
355
|
+
return await self._write_and_rotate(
|
|
356
|
+
domain, entity_id, key, config, tool_name, now, mandatory=mandatory
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
def _resolve_snapshot_handler(
|
|
360
|
+
self, domain: str, entity_id: str, *, force: bool, mandatory: bool
|
|
361
|
+
) -> DomainHandler | None:
|
|
362
|
+
"""Run the pre-capture guards; return the handler or None to skip.
|
|
363
|
+
|
|
364
|
+
Raises ``MandatoryBackupError`` for the fail-closed cases (an unusable
|
|
365
|
+
backup dir, an unregistered domain) under ``mandatory``. A None return
|
|
366
|
+
is a legitimate skip (feature disabled, no entity id, no handler) — the
|
|
367
|
+
caller returns None and lets the wrapped write proceed.
|
|
368
|
+
"""
|
|
320
369
|
if self._init_dir_error is not None:
|
|
321
370
|
if mandatory:
|
|
322
371
|
raise MandatoryBackupError(
|
|
@@ -344,88 +393,101 @@ class BackupManager:
|
|
|
344
393
|
domain,
|
|
345
394
|
)
|
|
346
395
|
return None
|
|
396
|
+
return handler
|
|
347
397
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
return
|
|
398
|
+
async def _fetch_config_for_snapshot(
|
|
399
|
+
self, handler: DomainHandler, entity_id: str, key: str, *, mandatory: bool
|
|
400
|
+
) -> Any:
|
|
401
|
+
"""Fetch the pre-write config; return ``_SNAPSHOT_SKIP`` to skip capture.
|
|
402
|
+
|
|
403
|
+
A handled transient fetch failure logs a WARNING and returns the
|
|
404
|
+
sentinel (or raises ``MandatoryBackupError`` under ``mandatory``); a
|
|
405
|
+
fetch that returns None because the entity did not exist logs a DEBUG
|
|
406
|
+
and returns the sentinel. Any other value is the config to snapshot.
|
|
407
|
+
"""
|
|
408
|
+
try:
|
|
409
|
+
config = await handler.fetch(self._client, entity_id)
|
|
410
|
+
except _CAPTURE_TRANSIENT_ERRORS as err:
|
|
411
|
+
# Degraded fetches (a non-list WS envelope from an
|
|
412
|
+
# auth-scope change or API drift) raise rather than return
|
|
413
|
+
# None — see ``_require_list``. During auto-backup we skip
|
|
414
|
+
# the snapshot with a WARNING (operator-visible) instead of
|
|
415
|
+
# crashing the pipeline; the same error during a diff/
|
|
416
|
+
# restore propagates to the tool layer as a structured
|
|
417
|
+
# error. The warning level (vs the debug log below) is what
|
|
418
|
+
# distinguishes "fetch broke" from "entity didn't exist".
|
|
419
|
+
if mandatory:
|
|
420
|
+
raise MandatoryBackupError(
|
|
421
|
+
f"could not read the current state of {key} to back "
|
|
422
|
+
f"it up: {type(err).__name__}: {err}"
|
|
423
|
+
) from err
|
|
424
|
+
logger.warning(
|
|
425
|
+
"Auto-backup: fetch failed for %s — %s: %s",
|
|
426
|
+
key,
|
|
427
|
+
type(err).__name__,
|
|
428
|
+
err,
|
|
429
|
+
)
|
|
430
|
+
return _SNAPSHOT_SKIP
|
|
431
|
+
if config is None:
|
|
432
|
+
# Entity didn't exist at fetch time (create operation, or
|
|
433
|
+
# already-deleted at remove time before our pre-fetch).
|
|
434
|
+
logger.debug(
|
|
435
|
+
"Auto-backup: fetch returned None for %s — skipping snapshot",
|
|
436
|
+
key,
|
|
437
|
+
)
|
|
438
|
+
return _SNAPSHOT_SKIP
|
|
439
|
+
return config
|
|
440
|
+
|
|
441
|
+
async def _write_and_rotate(
|
|
442
|
+
self,
|
|
443
|
+
domain: str,
|
|
444
|
+
entity_id: str,
|
|
445
|
+
key: str,
|
|
446
|
+
config: Any,
|
|
447
|
+
tool_name: str | None,
|
|
448
|
+
now: float,
|
|
449
|
+
*,
|
|
450
|
+
mandatory: bool,
|
|
451
|
+
) -> Path | None:
|
|
452
|
+
"""Write the snapshot then rotate old files; return the written Path.
|
|
453
|
+
|
|
454
|
+
Returns None on a handled (non-mandatory) write failure. Raises
|
|
455
|
+
``MandatoryBackupError`` under ``mandatory`` when the write genuinely
|
|
456
|
+
fails (e.g. disk-full).
|
|
457
|
+
"""
|
|
458
|
+
try:
|
|
459
|
+
path = await asyncio.to_thread(
|
|
460
|
+
self._write_snapshot, domain, entity_id, config, tool_name
|
|
461
|
+
)
|
|
462
|
+
except (OSError, yaml.YAMLError) as err:
|
|
463
|
+
if mandatory:
|
|
464
|
+
raise MandatoryBackupError(
|
|
465
|
+
f"could not write the pre-write snapshot for {key}: "
|
|
466
|
+
f"{type(err).__name__}: {err}",
|
|
467
|
+
suggestions=[
|
|
468
|
+
"Free up disk space, or delete old snapshots via "
|
|
469
|
+
"ha_manage_backup(scope='edits', action='delete')",
|
|
470
|
+
],
|
|
471
|
+
) from err
|
|
472
|
+
logger.warning(
|
|
473
|
+
"Auto-backup: write failed for %s — %s: %s",
|
|
474
|
+
key,
|
|
475
|
+
type(err).__name__,
|
|
476
|
+
err,
|
|
477
|
+
)
|
|
478
|
+
return None
|
|
479
|
+
self._last_snapshot[key] = now
|
|
480
|
+
self._maybe_prune_trackers()
|
|
481
|
+
try:
|
|
482
|
+
await asyncio.to_thread(self._rotate, domain, entity_id)
|
|
483
|
+
except OSError as err:
|
|
484
|
+
logger.warning(
|
|
485
|
+
"Auto-backup: rotation failed for %s — %s: %s",
|
|
486
|
+
key,
|
|
487
|
+
type(err).__name__,
|
|
488
|
+
err,
|
|
489
|
+
)
|
|
490
|
+
return path
|
|
429
491
|
|
|
430
492
|
def _maybe_prune_trackers(self) -> None:
|
|
431
493
|
"""Cap per-entity tracker growth.
|
|
@@ -1024,44 +1086,11 @@ def _diff_node(
|
|
|
1024
1086
|
if type(stored) is type(current):
|
|
1025
1087
|
if isinstance(stored, dict):
|
|
1026
1088
|
assert isinstance(current, dict)
|
|
1027
|
-
|
|
1028
|
-
seg = _pointer_segment(str(key))
|
|
1029
|
-
sub_path = f"{path}/{seg}"
|
|
1030
|
-
if key not in current:
|
|
1031
|
-
out.append({"op": "add", "path": sub_path, "value": stored[key]})
|
|
1032
|
-
if len(out) >= max_ops:
|
|
1033
|
-
return
|
|
1034
|
-
else:
|
|
1035
|
-
_diff_node(stored[key], current[key], sub_path, out, max_ops)
|
|
1036
|
-
if len(out) >= max_ops:
|
|
1037
|
-
return
|
|
1038
|
-
for key in current:
|
|
1039
|
-
if key not in stored:
|
|
1040
|
-
seg = _pointer_segment(str(key))
|
|
1041
|
-
out.append({"op": "remove", "path": f"{path}/{seg}"})
|
|
1042
|
-
if len(out) >= max_ops:
|
|
1043
|
-
return
|
|
1089
|
+
_diff_dict_node(stored, current, path, out, max_ops)
|
|
1044
1090
|
return
|
|
1045
1091
|
if isinstance(stored, list):
|
|
1046
1092
|
assert isinstance(current, list)
|
|
1047
|
-
|
|
1048
|
-
for i in range(min_len):
|
|
1049
|
-
_diff_node(stored[i], current[i], f"{path}/{i}", out, max_ops)
|
|
1050
|
-
if len(out) >= max_ops:
|
|
1051
|
-
return
|
|
1052
|
-
if len(stored) > len(current):
|
|
1053
|
-
for value in stored[len(current) :]:
|
|
1054
|
-
out.append({"op": "add", "path": f"{path}/-", "value": value})
|
|
1055
|
-
if len(out) >= max_ops:
|
|
1056
|
-
return
|
|
1057
|
-
elif len(current) > len(stored):
|
|
1058
|
-
# Remove tail entries from highest to lowest index so
|
|
1059
|
-
# successive removes stay valid (RFC 6902 reindexes
|
|
1060
|
-
# after each op).
|
|
1061
|
-
for i in range(len(current) - 1, len(stored) - 1, -1):
|
|
1062
|
-
out.append({"op": "remove", "path": f"{path}/{i}"})
|
|
1063
|
-
if len(out) >= max_ops:
|
|
1064
|
-
return
|
|
1093
|
+
_diff_list_node(stored, current, path, out, max_ops)
|
|
1065
1094
|
return
|
|
1066
1095
|
if stored != current:
|
|
1067
1096
|
out.append({"op": "replace", "path": path or "", "value": stored})
|
|
@@ -1076,6 +1105,61 @@ def _diff_node(
|
|
|
1076
1105
|
out.append({"op": "replace", "path": path or "", "value": stored})
|
|
1077
1106
|
|
|
1078
1107
|
|
|
1108
|
+
def _diff_dict_node(
|
|
1109
|
+
stored: dict[Any, Any],
|
|
1110
|
+
current: dict[Any, Any],
|
|
1111
|
+
path: str,
|
|
1112
|
+
out: list[dict[str, Any]],
|
|
1113
|
+
max_ops: int,
|
|
1114
|
+
) -> None:
|
|
1115
|
+
"""Diff two dicts into JSON-Patch ops (add/remove/recurse per key)."""
|
|
1116
|
+
for key in stored:
|
|
1117
|
+
seg = _pointer_segment(str(key))
|
|
1118
|
+
sub_path = f"{path}/{seg}"
|
|
1119
|
+
if key not in current:
|
|
1120
|
+
out.append({"op": "add", "path": sub_path, "value": stored[key]})
|
|
1121
|
+
if len(out) >= max_ops:
|
|
1122
|
+
return
|
|
1123
|
+
else:
|
|
1124
|
+
_diff_node(stored[key], current[key], sub_path, out, max_ops)
|
|
1125
|
+
if len(out) >= max_ops:
|
|
1126
|
+
return
|
|
1127
|
+
for key in current:
|
|
1128
|
+
if key not in stored:
|
|
1129
|
+
seg = _pointer_segment(str(key))
|
|
1130
|
+
out.append({"op": "remove", "path": f"{path}/{seg}"})
|
|
1131
|
+
if len(out) >= max_ops:
|
|
1132
|
+
return
|
|
1133
|
+
|
|
1134
|
+
|
|
1135
|
+
def _diff_list_node(
|
|
1136
|
+
stored: list[Any],
|
|
1137
|
+
current: list[Any],
|
|
1138
|
+
path: str,
|
|
1139
|
+
out: list[dict[str, Any]],
|
|
1140
|
+
max_ops: int,
|
|
1141
|
+
) -> None:
|
|
1142
|
+
"""Diff two lists into JSON-Patch ops (positional recurse, then tail add/remove)."""
|
|
1143
|
+
min_len = min(len(stored), len(current))
|
|
1144
|
+
for i in range(min_len):
|
|
1145
|
+
_diff_node(stored[i], current[i], f"{path}/{i}", out, max_ops)
|
|
1146
|
+
if len(out) >= max_ops:
|
|
1147
|
+
return
|
|
1148
|
+
if len(stored) > len(current):
|
|
1149
|
+
for value in stored[len(current) :]:
|
|
1150
|
+
out.append({"op": "add", "path": f"{path}/-", "value": value})
|
|
1151
|
+
if len(out) >= max_ops:
|
|
1152
|
+
return
|
|
1153
|
+
elif len(current) > len(stored):
|
|
1154
|
+
# Remove tail entries from highest to lowest index so
|
|
1155
|
+
# successive removes stay valid (RFC 6902 reindexes
|
|
1156
|
+
# after each op).
|
|
1157
|
+
for i in range(len(current) - 1, len(stored) - 1, -1):
|
|
1158
|
+
out.append({"op": "remove", "path": f"{path}/{i}"})
|
|
1159
|
+
if len(out) >= max_ops:
|
|
1160
|
+
return
|
|
1161
|
+
|
|
1162
|
+
|
|
1079
1163
|
def _pointer_segment(key: str) -> str:
|
|
1080
1164
|
"""Escape one JSON-Pointer reference token per RFC 6901 §3.
|
|
1081
1165
|
|
|
@@ -194,60 +194,114 @@ def with_auto_backup(
|
|
|
194
194
|
)
|
|
195
195
|
)
|
|
196
196
|
if enabled:
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
197
|
+
await _capture_pre_write_snapshot(
|
|
198
|
+
func,
|
|
199
|
+
args,
|
|
200
|
+
kwargs,
|
|
201
|
+
settings=settings,
|
|
202
|
+
explicit_client=explicit_client,
|
|
203
|
+
domain=domain,
|
|
204
|
+
domain_fn=domain_fn,
|
|
205
|
+
id_param=id_param,
|
|
206
|
+
id_fn=id_fn,
|
|
207
|
+
mandatory=mandatory,
|
|
204
208
|
)
|
|
205
|
-
if id_fn is not None:
|
|
206
|
-
entity_id = _resolve_str(id_fn(kwargs))
|
|
207
|
-
else:
|
|
208
|
-
entity_id = _resolve_str(kwargs.get(id_param or ""))
|
|
209
|
-
if entity_id:
|
|
210
|
-
try:
|
|
211
|
-
if client_obj is not None:
|
|
212
|
-
mgr = get_backup_manager(client_obj, settings)
|
|
213
|
-
await mgr.maybe_snapshot(
|
|
214
|
-
snap_domain,
|
|
215
|
-
entity_id,
|
|
216
|
-
tool_name=func.__name__,
|
|
217
|
-
mandatory=mandatory,
|
|
218
|
-
)
|
|
219
|
-
elif mandatory:
|
|
220
|
-
# No client to capture with, but this tool requires a
|
|
221
|
-
# backup — fail closed rather than write un-backed-up.
|
|
222
|
-
raise MandatoryBackupError(
|
|
223
|
-
"no Home Assistant client is available to capture "
|
|
224
|
-
"the pre-write backup"
|
|
225
|
-
)
|
|
226
|
-
except MandatoryBackupError as err:
|
|
227
|
-
# A required pre-write snapshot genuinely failed (not a
|
|
228
|
-
# legitimate "nothing to snapshot" skip). Fail closed: the
|
|
229
|
-
# wrapped write never runs, so nothing has been changed.
|
|
230
|
-
# The ToolError this raises propagates rather than being
|
|
231
|
-
# swallowed by the best-effort handler below.
|
|
232
|
-
raise_tool_error(
|
|
233
|
-
create_error_response(
|
|
234
|
-
ErrorCode.BACKUP_CAPTURE_FAILED,
|
|
235
|
-
f"'{func.__name__}' requires a pre-write backup, "
|
|
236
|
-
f"but the snapshot could not be captured: {err}. "
|
|
237
|
-
"The write was blocked and nothing was changed.",
|
|
238
|
-
suggestions=err.suggestions
|
|
239
|
-
or ["Retry once the underlying issue is resolved"],
|
|
240
|
-
context={"tool_name": func.__name__},
|
|
241
|
-
)
|
|
242
|
-
)
|
|
243
|
-
except _DECORATOR_TRANSIENT_ERRORS as err:
|
|
244
|
-
logger.warning(
|
|
245
|
-
"Auto-backup: capture raised %s: %s — write proceeding",
|
|
246
|
-
type(err).__name__,
|
|
247
|
-
err,
|
|
248
|
-
)
|
|
249
209
|
return await func(*args, **kwargs)
|
|
250
210
|
|
|
251
211
|
return wrapper
|
|
252
212
|
|
|
253
213
|
return decorator
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _resolve_backup_client(explicit_client: Any, args: tuple[Any, ...]) -> Any:
|
|
217
|
+
"""Resolve the HA client for capture: the explicit decorator kwarg, else the
|
|
218
|
+
bound ``self._client`` / ``self.client`` of a wrapped class method."""
|
|
219
|
+
client_obj = explicit_client
|
|
220
|
+
if client_obj is None and args:
|
|
221
|
+
client_obj = getattr(args[0], "_client", None) or getattr(
|
|
222
|
+
args[0], "client", None
|
|
223
|
+
)
|
|
224
|
+
return client_obj
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _resolve_snapshot_target(
|
|
228
|
+
kwargs: dict[str, Any],
|
|
229
|
+
*,
|
|
230
|
+
domain: str | None,
|
|
231
|
+
domain_fn: Callable[[dict[str, Any]], str] | None,
|
|
232
|
+
id_param: str | None,
|
|
233
|
+
id_fn: Callable[[dict[str, Any]], str] | None,
|
|
234
|
+
) -> tuple[str, str]:
|
|
235
|
+
"""Compute the ``(domain, entity_id)`` snapshot target from the tool kwargs."""
|
|
236
|
+
snap_domain: str = domain_fn(kwargs) if domain_fn is not None else domain or ""
|
|
237
|
+
if id_fn is not None:
|
|
238
|
+
entity_id = _resolve_str(id_fn(kwargs))
|
|
239
|
+
else:
|
|
240
|
+
entity_id = _resolve_str(kwargs.get(id_param or ""))
|
|
241
|
+
return snap_domain, entity_id
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
async def _capture_pre_write_snapshot(
|
|
245
|
+
func: Callable[..., Awaitable[Any]],
|
|
246
|
+
args: tuple[Any, ...],
|
|
247
|
+
kwargs: dict[str, Any],
|
|
248
|
+
*,
|
|
249
|
+
settings: Any,
|
|
250
|
+
explicit_client: Any,
|
|
251
|
+
domain: str | None,
|
|
252
|
+
domain_fn: Callable[[dict[str, Any]], str] | None,
|
|
253
|
+
id_param: str | None,
|
|
254
|
+
id_fn: Callable[[dict[str, Any]], str] | None,
|
|
255
|
+
mandatory: bool,
|
|
256
|
+
) -> None:
|
|
257
|
+
"""Resolve the snapshot target and capture a pre-write backup.
|
|
258
|
+
|
|
259
|
+
Extracted from ``with_auto_backup``'s wrapper. Best-effort: a transient
|
|
260
|
+
capture failure logs a WARNING and lets the write proceed; a ``mandatory``
|
|
261
|
+
failure maps to a structured ``BACKUP_CAPTURE_FAILED`` error that fails the
|
|
262
|
+
write closed (nothing is changed).
|
|
263
|
+
"""
|
|
264
|
+
client_obj = _resolve_backup_client(explicit_client, args)
|
|
265
|
+
snap_domain, entity_id = _resolve_snapshot_target(
|
|
266
|
+
kwargs, domain=domain, domain_fn=domain_fn, id_param=id_param, id_fn=id_fn
|
|
267
|
+
)
|
|
268
|
+
if entity_id:
|
|
269
|
+
try:
|
|
270
|
+
if client_obj is not None:
|
|
271
|
+
mgr = get_backup_manager(client_obj, settings)
|
|
272
|
+
await mgr.maybe_snapshot(
|
|
273
|
+
snap_domain,
|
|
274
|
+
entity_id,
|
|
275
|
+
tool_name=func.__name__,
|
|
276
|
+
mandatory=mandatory,
|
|
277
|
+
)
|
|
278
|
+
elif mandatory:
|
|
279
|
+
# No client to capture with, but this tool requires a
|
|
280
|
+
# backup — fail closed rather than write un-backed-up.
|
|
281
|
+
raise MandatoryBackupError(
|
|
282
|
+
"no Home Assistant client is available to capture "
|
|
283
|
+
"the pre-write backup"
|
|
284
|
+
)
|
|
285
|
+
except MandatoryBackupError as err:
|
|
286
|
+
# A required pre-write snapshot genuinely failed (not a
|
|
287
|
+
# legitimate "nothing to snapshot" skip). Fail closed: the
|
|
288
|
+
# wrapped write never runs, so nothing has been changed.
|
|
289
|
+
# The ToolError this raises propagates rather than being
|
|
290
|
+
# swallowed by the best-effort handler below.
|
|
291
|
+
raise_tool_error(
|
|
292
|
+
create_error_response(
|
|
293
|
+
ErrorCode.BACKUP_CAPTURE_FAILED,
|
|
294
|
+
f"'{func.__name__}' requires a pre-write backup, "
|
|
295
|
+
f"but the snapshot could not be captured: {err}. "
|
|
296
|
+
"The write was blocked and nothing was changed.",
|
|
297
|
+
suggestions=err.suggestions
|
|
298
|
+
or ["Retry once the underlying issue is resolved"],
|
|
299
|
+
context={"tool_name": func.__name__},
|
|
300
|
+
)
|
|
301
|
+
)
|
|
302
|
+
except _DECORATOR_TRANSIENT_ERRORS as err:
|
|
303
|
+
logger.warning(
|
|
304
|
+
"Auto-backup: capture raised %s: %s — write proceeding",
|
|
305
|
+
type(err).__name__,
|
|
306
|
+
err,
|
|
307
|
+
)
|