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.
Files changed (185) hide show
  1. {ha_mcp_dev-7.13.0.dev1973/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.13.0.dev1975}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/backup_manager.py +63 -21
  4. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/backup.py +149 -20
  5. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/component_api.py +85 -16
  6. ha_mcp_dev-7.13.0.dev1975/src/ha_mcp/tools/component_config_reads.py +212 -0
  7. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/component_devices.py +41 -6
  8. ha_mcp_dev-7.13.0.dev1975/src/ha_mcp/tools/component_registries.py +144 -0
  9. ha_mcp_dev-7.13.0.dev1975/src/ha_mcp/tools/component_registry_lookup.py +161 -0
  10. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/radio/base.py +12 -3
  11. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/reference_validator.py +24 -4
  12. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_areas.py +80 -55
  13. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_blueprints.py +12 -1
  14. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_config_automations.py +31 -2
  15. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_config_dashboards.py +768 -16
  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
  17. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_config_scenes.py +71 -4
  18. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_dev.py +133 -16
  19. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_entities.py +13 -3
  20. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_integrations.py +413 -58
  21. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_search.py +158 -40
  22. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_services.py +114 -5
  23. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_system.py +293 -28
  24. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_voice_assistant.py +16 -4
  25. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_zones.py +19 -3
  26. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/util_helpers.py +31 -8
  27. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/visibility/model.py +27 -0
  28. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/visibility/resolver.py +39 -0
  29. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  30. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp_dev.egg-info/SOURCES.txt +3 -0
  31. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/LICENSE +0 -0
  32. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/MANIFEST.in +0 -0
  33. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/README.md +0 -0
  34. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/setup.cfg +0 -0
  35. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/__init__.py +0 -0
  36. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/__main__.py +0 -0
  37. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/_pypi_marker +0 -0
  38. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/_version.py +0 -0
  39. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/auth/__init__.py +0 -0
  40. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/auth/consent_form.py +0 -0
  41. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/auth/provider.py +0 -0
  42. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/browser_landing.py +0 -0
  43. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/client/__init__.py +0 -0
  44. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/client/rest_client.py +0 -0
  45. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/client/supervisor_client.py +0 -0
  46. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/client/websocket_client.py +0 -0
  47. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/client/websocket_listener.py +0 -0
  48. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/config.py +0 -0
  49. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  50. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  51. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
  52. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
  53. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  54. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/errors.py +0 -0
  55. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/llm_exposure.py +0 -0
  56. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/policy/__init__.py +0 -0
  57. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/policy/approval_queue.py +0 -0
  58. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/policy/evaluator.py +0 -0
  59. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/policy/handlers.py +0 -0
  60. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/policy/middleware.py +0 -0
  61. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/policy/model.py +0 -0
  62. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/policy/persistence.py +0 -0
  63. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/policy/value_sources.py +0 -0
  64. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/py.typed +0 -0
  65. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/read_only.py +0 -0
  66. {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
  67. {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
  68. {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
  69. {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
  70. {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
  71. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  72. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  73. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  74. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  75. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  76. {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
  77. {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
  78. {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
  79. {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
  80. {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
  81. {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
  82. {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
  83. {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
  84. {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
  85. {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
  86. {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
  87. {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
  88. {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
  89. {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
  90. {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
  91. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/server.py +0 -0
  92. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/settings_ui/__init__.py +0 -0
  93. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
  94. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
  95. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
  96. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
  97. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
  98. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
  99. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/settings_ui/_i18n.py +0 -0
  100. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/settings_ui/_persistence.py +0 -0
  101. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
  102. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/settings_ui/_theme.py +0 -0
  103. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
  104. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/settings_ui/locales/README.md +0 -0
  105. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/settings_ui/locales/en.json +0 -0
  106. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/settings_ui/locales/ru.json +0 -0
  107. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/settings_ui/settings.css +0 -0
  108. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/settings_ui/settings.html +0 -0
  109. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/settings_ui/settings.js +0 -0
  110. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/smoke_test.py +0 -0
  111. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  112. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/strict_bps.py +0 -0
  113. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/__init__.py +0 -0
  114. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/auto_backup.py +0 -0
  115. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  116. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/config_entry_flow.py +0 -0
  117. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/device_control.py +0 -0
  118. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/enhanced.py +0 -0
  119. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/helpers.py +0 -0
  120. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/radio/__init__.py +0 -0
  121. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/radio/matter.py +0 -0
  122. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/radio/thread.py +0 -0
  123. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/radio/zigbee.py +0 -0
  124. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/radio/zwave.py +0 -0
  125. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/registry.py +0 -0
  126. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  127. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  128. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  129. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
  130. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
  131. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  132. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  133. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  134. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  135. {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
  136. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_addons.py +0 -0
  137. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  138. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_calendar.py +0 -0
  139. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_camera.py +0 -0
  140. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_categories.py +0 -0
  141. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_code.py +0 -0
  142. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  143. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  144. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_energy.py +0 -0
  145. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  146. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_groups.py +0 -0
  147. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_hacs.py +0 -0
  148. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_history.py +0 -0
  149. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_labels.py +0 -0
  150. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_radio.py +0 -0
  151. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_registry.py +0 -0
  152. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_resources.py +0 -0
  153. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_service.py +0 -0
  154. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_themes.py +0 -0
  155. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_todo.py +0 -0
  156. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_traces.py +0 -0
  157. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_updates.py +0 -0
  158. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_utility.py +0 -0
  159. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  160. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
  161. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/tools/validation_middleware.py +0 -0
  162. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/transforms/__init__.py +0 -0
  163. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/transforms/categorized_search.py +0 -0
  164. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  165. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/transport_security.py +0 -0
  166. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/update_check.py +0 -0
  167. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/utils/__init__.py +0 -0
  168. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/utils/config_hash.py +0 -0
  169. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/utils/data_paths.py +0 -0
  170. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/utils/domain_handlers.py +0 -0
  171. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  172. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  173. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/utils/operation_manager.py +0 -0
  174. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/utils/python_sandbox.py +0 -0
  175. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/utils/skill_loader.py +0 -0
  176. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/utils/usage_logger.py +0 -0
  177. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/visibility/__init__.py +0 -0
  178. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp/visibility/persistence.py +0 -0
  179. {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
  180. {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
  181. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  182. {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
  183. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/tests/__init__.py +0 -0
  184. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/tests/test_constants.py +0 -0
  185. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1975}/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.dev1973
3
+ Version: 7.13.0.dev1975
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.dev1973"
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
- Delegates to ``tools_config_dashboards._get_dashboard_config_internal``
1300
- which handles the WS envelope, force-cache-bypass, and structured
1301
- error wrapping consistently with how the rest of the dashboard surface
1302
- fetches state. Imported lazily to avoid an import cycle.
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
- items = _require_list(
1446
- await _ws_send(client, {"type": "config/label_registry/list"}),
1447
- "config/label_registry/list",
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
- items = _require_list(
1470
- await _ws_send(
1471
- client, {"type": "config/category_registry/list", "scope": scope}
1472
- ),
1473
- "config/category_registry/list",
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
- items = _require_list(
1612
- await _ws_send(client, {"type": "config/area_registry/list"}),
1613
- "config/area_registry/list",
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
- items = _require_list(
1620
- await _ws_send(client, {"type": "config/floor_registry/list"}),
1621
- "config/floor_registry/list",
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 HomeAssistantClient, HomeAssistantError
34
- from ..client.websocket_client import HomeAssistantWebSocketClient
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
- # Get backup password (raises ToolError on failure)
534
- password = await _get_backup_password(ws_client)
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
- # Discover the local backup agent at call time. HA Core registers
537
- # `backup.local`; HA Supervised registers `hassio.local`. Hardcoding
538
- # either breaks the other deployment.
539
- local_agent = await _get_local_backup_agent_id(ws_client)
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
- # Discover the local backup agent (Supervisor's hassio.local on
772
- # Supervised, backup.local on Core). Used for both the safety backup
773
- # and the restore call below.
774
- local_agent = await _get_local_backup_agent_id(ws_client)
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
- # Create safety backup BEFORE restoring
777
- logger.info("Creating safety backup before restore...")
778
- try:
779
- password = await _get_backup_password(ws_client)
780
- except ToolError:
781
- # Password error - log warning but continue (restore might still work)
782
- logger.warning("No default password - proceeding without safety backup")
783
- password = None
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
- # Negatives carry an expiry in ``_NEGATIVE_CACHE_TS`` so a component installed /
98
- # upgraded mid-session is eventually re-probed instead of pinned absent forever.
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 entry was stored, keyed by the same client.
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 ``_NEGATIVE_CACHE_TTL_S`` of when it was stored
122
- (monotonic clock); once that window lapses it reports a miss so the caller
123
- re-probes and can adopt a component that appeared mid-session.
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 (_monotonic() - stored_at) < _NEGATIVE_CACHE_TTL_S:
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 ``caps`` for ``client``, stamping a negative with the current clock."""
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): do **not** cache — re-probe on the next call once the
188
- connection recovers. The consuming tool's legacy path will surface the
189
- transport failure on its own.
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, returns ``None`` without
193
- caching (a transient runtime fault re-probes on the next call).
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)