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.
Files changed (187) hide show
  1. {ha_mcp_dev-7.13.0.dev2011/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.13.0.dev2013}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/pyproject.toml +1 -4
  3. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/backup_manager.py +200 -116
  4. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/auto_backup.py +105 -51
  5. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/backup.py +887 -625
  6. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  7. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/LICENSE +0 -0
  8. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/MANIFEST.in +0 -0
  9. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/README.md +0 -0
  10. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/setup.cfg +0 -0
  11. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/__init__.py +0 -0
  12. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/__main__.py +0 -0
  13. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/_pypi_marker +0 -0
  14. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/_version.py +0 -0
  15. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/auth/__init__.py +0 -0
  16. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/auth/consent_form.py +0 -0
  17. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/auth/provider.py +0 -0
  18. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/browser_landing.py +0 -0
  19. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/client/__init__.py +0 -0
  20. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/client/rest_client.py +0 -0
  21. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/client/supervisor_client.py +0 -0
  22. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/client/websocket_client.py +0 -0
  23. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/client/websocket_listener.py +0 -0
  24. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/config.py +0 -0
  25. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  26. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  27. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
  28. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
  29. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  30. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/dashboard_screenshot/theme_guard.py +0 -0
  31. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/errors.py +0 -0
  32. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/llm_exposure.py +0 -0
  33. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/policy/__init__.py +0 -0
  34. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/policy/approval_queue.py +0 -0
  35. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/policy/evaluator.py +0 -0
  36. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/policy/handlers.py +0 -0
  37. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/policy/middleware.py +0 -0
  38. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/policy/model.py +0 -0
  39. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/policy/persistence.py +0 -0
  40. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/policy/value_sources.py +0 -0
  41. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/py.typed +0 -0
  42. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/read_only.py +0 -0
  43. {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
  44. {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
  45. {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
  46. {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
  47. {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
  48. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  49. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  50. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  51. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  52. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  53. {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
  54. {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
  55. {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
  56. {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
  57. {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
  58. {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
  59. {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
  60. {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
  61. {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
  62. {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
  63. {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
  64. {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
  65. {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
  66. {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
  67. {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
  68. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/server.py +0 -0
  69. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/settings_ui/__init__.py +0 -0
  70. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
  71. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
  72. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
  73. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
  74. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
  75. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
  76. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/settings_ui/_i18n.py +0 -0
  77. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/settings_ui/_persistence.py +0 -0
  78. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
  79. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/settings_ui/_theme.py +0 -0
  80. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
  81. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/settings_ui/locales/README.md +0 -0
  82. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/settings_ui/locales/de.json +0 -0
  83. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/settings_ui/locales/en.json +0 -0
  84. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/settings_ui/locales/ru.json +0 -0
  85. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/settings_ui/settings.css +0 -0
  86. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/settings_ui/settings.html +0 -0
  87. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/settings_ui/settings.js +0 -0
  88. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/smoke_test.py +0 -0
  89. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  90. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/strict_bps.py +0 -0
  91. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/__init__.py +0 -0
  92. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  93. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/component_api.py +0 -0
  94. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/component_config_reads.py +0 -0
  95. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/component_devices.py +0 -0
  96. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/component_registries.py +0 -0
  97. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/component_registry_lookup.py +0 -0
  98. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/config_entry_flow.py +0 -0
  99. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/device_control.py +0 -0
  100. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/enhanced.py +0 -0
  101. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/helpers.py +0 -0
  102. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/radio/__init__.py +0 -0
  103. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/radio/base.py +0 -0
  104. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/radio/matter.py +0 -0
  105. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/radio/thread.py +0 -0
  106. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/radio/zigbee.py +0 -0
  107. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/radio/zwave.py +0 -0
  108. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/reference_validator.py +0 -0
  109. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/registry.py +0 -0
  110. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  111. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  112. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  113. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
  114. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
  115. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  116. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  117. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  118. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  119. {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
  120. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_addons.py +0 -0
  121. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_areas.py +0 -0
  122. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  123. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  124. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_calendar.py +0 -0
  125. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_camera.py +0 -0
  126. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_categories.py +0 -0
  127. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_code.py +0 -0
  128. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  129. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  130. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  131. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  132. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  133. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  134. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_dev.py +0 -0
  135. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_energy.py +0 -0
  136. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_entities.py +0 -0
  137. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  138. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_groups.py +0 -0
  139. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_hacs.py +0 -0
  140. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_history.py +0 -0
  141. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_integrations.py +0 -0
  142. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_labels.py +0 -0
  143. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_radio.py +0 -0
  144. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_registry.py +0 -0
  145. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_resources.py +0 -0
  146. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_search.py +0 -0
  147. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_service.py +0 -0
  148. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_services.py +0 -0
  149. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_system.py +0 -0
  150. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_themes.py +0 -0
  151. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_todo.py +0 -0
  152. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_traces.py +0 -0
  153. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_updates.py +0 -0
  154. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_utility.py +0 -0
  155. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  156. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  157. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
  158. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/tools_zones.py +0 -0
  159. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/util_helpers.py +0 -0
  160. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/tools/validation_middleware.py +0 -0
  161. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/transforms/__init__.py +0 -0
  162. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/transforms/categorized_search.py +0 -0
  163. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  164. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/transport_security.py +0 -0
  165. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/update_check.py +0 -0
  166. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/utils/__init__.py +0 -0
  167. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/utils/config_hash.py +0 -0
  168. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/utils/data_paths.py +0 -0
  169. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/utils/domain_handlers.py +0 -0
  170. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  171. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  172. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/utils/operation_manager.py +0 -0
  173. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/utils/python_sandbox.py +0 -0
  174. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/utils/skill_loader.py +0 -0
  175. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/utils/usage_logger.py +0 -0
  176. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/visibility/__init__.py +0 -0
  177. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/visibility/model.py +0 -0
  178. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/visibility/persistence.py +0 -0
  179. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp/visibility/resolver.py +0 -0
  180. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  181. {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
  182. {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
  183. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  184. {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
  185. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/tests/__init__.py +0 -0
  186. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/tests/test_constants.py +0 -0
  187. {ha_mcp_dev-7.13.0.dev2011 → ha_mcp_dev-7.13.0.dev2013}/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.13.0.dev2011
3
+ Version: 7.13.0.dev2013
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
@@ -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.dev2011"
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
- key = f"{domain}:{entity_id}"
349
- lock = self._locks.setdefault(key, asyncio.Lock())
350
- async with lock:
351
- now = time.monotonic()
352
- throttle = self.throttle_seconds
353
- # Skip throttle if no prior snapshot exists for this key.
354
- # Using ``get(key, 0.0)`` would falsely block the first capture
355
- # whenever ``monotonic()`` < throttle (typical on a fresh process
356
- # in CI), since 0.0 would be treated as "last snapshot at
357
- # monotonic time 0".
358
- if (
359
- not force
360
- and throttle
361
- and key in self._last_snapshot
362
- and (now - self._last_snapshot[key]) < throttle
363
- ):
364
- return None
365
- try:
366
- config = await handler.fetch(self._client, entity_id)
367
- except _CAPTURE_TRANSIENT_ERRORS as err:
368
- # Degraded fetches (a non-list WS envelope from an
369
- # auth-scope change or API drift) raise rather than return
370
- # None — see ``_require_list``. During auto-backup we skip
371
- # the snapshot with a WARNING (operator-visible) instead of
372
- # crashing the pipeline; the same error during a diff/
373
- # restore propagates to the tool layer as a structured
374
- # error. The warning level (vs the debug log below) is what
375
- # distinguishes "fetch broke" from "entity didn't exist".
376
- if mandatory:
377
- raise MandatoryBackupError(
378
- f"could not read the current state of {key} to back "
379
- f"it up: {type(err).__name__}: {err}"
380
- ) from err
381
- logger.warning(
382
- "Auto-backup: fetch failed for %s %s: %s",
383
- key,
384
- type(err).__name__,
385
- err,
386
- )
387
- return None
388
- if config is None:
389
- # Entity didn't exist at fetch time (create operation, or
390
- # already-deleted at remove time before our pre-fetch).
391
- logger.debug(
392
- "Auto-backup: fetch returned None for %s — skipping snapshot",
393
- key,
394
- )
395
- return None
396
- try:
397
- path = await asyncio.to_thread(
398
- self._write_snapshot, domain, entity_id, config, tool_name
399
- )
400
- except (OSError, yaml.YAMLError) as err:
401
- if mandatory:
402
- raise MandatoryBackupError(
403
- f"could not write the pre-write snapshot for {key}: "
404
- f"{type(err).__name__}: {err}",
405
- suggestions=[
406
- "Free up disk space, or delete old snapshots via "
407
- "ha_manage_backup(scope='edits', action='delete')",
408
- ],
409
- ) from err
410
- logger.warning(
411
- "Auto-backup: write failed for %s — %s: %s",
412
- key,
413
- type(err).__name__,
414
- err,
415
- )
416
- return None
417
- self._last_snapshot[key] = now
418
- self._maybe_prune_trackers()
419
- try:
420
- await asyncio.to_thread(self._rotate, domain, entity_id)
421
- except OSError as err:
422
- logger.warning(
423
- "Auto-backup: rotation failed for %s — %s: %s",
424
- key,
425
- type(err).__name__,
426
- err,
427
- )
428
- return path
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
- for key in stored:
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
- min_len = min(len(stored), len(current))
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
- client_obj = explicit_client
198
- if client_obj is None and args:
199
- client_obj = getattr(args[0], "_client", None) or getattr(
200
- args[0], "client", None
201
- )
202
- snap_domain: str = (
203
- domain_fn(kwargs) if domain_fn is not None else domain or ""
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
+ )