ha-mcp-dev 8.3.0.dev2442__tar.gz → 8.3.0.dev2445__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 (288) hide show
  1. {ha_mcp_dev-8.3.0.dev2442/src/ha_mcp_dev.egg-info → ha_mcp_dev-8.3.0.dev2445}/PKG-INFO +1 -1
  2. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/pyproject.toml +1 -1
  3. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/client/rest_client.py +262 -50
  4. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/settings_ui/_handlers_server.py +30 -0
  5. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/settings_ui/locales/en.json +2 -0
  6. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/settings_ui/settings.js +32 -8
  7. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/error_log_parsing.py +208 -4
  8. ha_mcp_dev-8.3.0.dev2445/src/ha_mcp/tools/log_common.py +199 -0
  9. ha_mcp_dev-8.3.0.dev2445/src/ha_mcp/tools/log_sources.py +764 -0
  10. ha_mcp_dev-8.3.0.dev2445/src/ha_mcp/tools/log_sources_supervisor.py +326 -0
  11. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/registry.py +12 -0
  12. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/tools_bug_report.py +15 -6
  13. ha_mcp_dev-8.3.0.dev2445/src/ha_mcp/tools/tools_logs.py +296 -0
  14. ha_mcp_dev-8.3.0.dev2445/src/ha_mcp/tools/tools_utility.py +277 -0
  15. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  16. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp_dev.egg-info/SOURCES.txt +4 -0
  17. ha_mcp_dev-8.3.0.dev2442/src/ha_mcp/tools/tools_utility.py +0 -1600
  18. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/LICENSE +0 -0
  19. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/MANIFEST.in +0 -0
  20. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/README.md +0 -0
  21. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/setup.cfg +0 -0
  22. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/__init__.py +0 -0
  23. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/__main__.py +0 -0
  24. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_pypi_marker +0 -0
  25. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_rich_compat.py +0 -0
  26. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/__init__.py +0 -0
  27. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/requirements.txt +0 -0
  28. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/LICENSE +0 -0
  29. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/MANIFEST.sha256 +0 -0
  30. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/VENDORED +0 -0
  31. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/__init__.py +0 -0
  32. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/__main__.py +0 -0
  33. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/asyncio/__init__.py +0 -0
  34. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/asyncio/client.py +0 -0
  35. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/asyncio/connection.py +0 -0
  36. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/asyncio/messages.py +0 -0
  37. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/asyncio/router.py +0 -0
  38. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/asyncio/server.py +0 -0
  39. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/auth.py +0 -0
  40. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/cli.py +0 -0
  41. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/client.py +0 -0
  42. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/connection.py +0 -0
  43. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/datastructures.py +0 -0
  44. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/exceptions.py +0 -0
  45. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/extensions/__init__.py +0 -0
  46. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/extensions/base.py +0 -0
  47. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/extensions/permessage_deflate.py +0 -0
  48. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/frames.py +0 -0
  49. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/headers.py +0 -0
  50. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/http11.py +0 -0
  51. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/imports.py +0 -0
  52. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/legacy/__init__.py +0 -0
  53. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/legacy/auth.py +0 -0
  54. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/legacy/client.py +0 -0
  55. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/legacy/exceptions.py +0 -0
  56. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/legacy/framing.py +0 -0
  57. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/legacy/handshake.py +0 -0
  58. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/legacy/http.py +0 -0
  59. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/legacy/protocol.py +0 -0
  60. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/legacy/server.py +0 -0
  61. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/protocol.py +0 -0
  62. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/proxy.py +0 -0
  63. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/py.typed +0 -0
  64. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/server.py +0 -0
  65. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/streams.py +0 -0
  66. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/sync/__init__.py +0 -0
  67. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/sync/client.py +0 -0
  68. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/sync/connection.py +0 -0
  69. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/sync/messages.py +0 -0
  70. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/sync/router.py +0 -0
  71. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/sync/server.py +0 -0
  72. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/sync/utils.py +0 -0
  73. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/trio/__init__.py +0 -0
  74. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/trio/client.py +0 -0
  75. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/trio/connection.py +0 -0
  76. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/trio/messages.py +0 -0
  77. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/trio/router.py +0 -0
  78. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/trio/server.py +0 -0
  79. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/trio/utils.py +0 -0
  80. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/typing.py +0 -0
  81. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/uri.py +0 -0
  82. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/utils.py +0 -0
  83. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_vendor/websockets/version.py +0 -0
  84. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/_version.py +0 -0
  85. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/auth/__init__.py +0 -0
  86. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/auth/consent_form.py +0 -0
  87. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/auth/oidc_compat.py +0 -0
  88. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/auth/provider.py +0 -0
  89. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/backup_manager.py +0 -0
  90. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/browser_landing.py +0 -0
  91. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/client/__init__.py +0 -0
  92. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/client/supervisor_client.py +0 -0
  93. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/client/websocket_client.py +0 -0
  94. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/client/websocket_listener.py +0 -0
  95. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/config.py +0 -0
  96. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  97. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  98. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
  99. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
  100. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  101. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/dashboard_screenshot/theme_guard.py +0 -0
  102. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/errors.py +0 -0
  103. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/hacs_auto_refresh.py +0 -0
  104. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/llm_exposure.py +0 -0
  105. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/log_filters.py +0 -0
  106. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/policy/__init__.py +0 -0
  107. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/policy/approval_queue.py +0 -0
  108. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/policy/editing.py +0 -0
  109. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/policy/evaluator.py +0 -0
  110. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/policy/handlers.py +0 -0
  111. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/policy/middleware.py +0 -0
  112. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/policy/model.py +0 -0
  113. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/policy/persistence.py +0 -0
  114. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/policy/value_sources.py +0 -0
  115. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/py.typed +0 -0
  116. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/read_only.py +0 -0
  117. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/redaction.py +0 -0
  118. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/renamed_tools.py +0 -0
  119. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  120. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  121. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  122. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  123. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  124. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  125. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  126. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  127. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  128. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  129. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  130. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  131. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
  132. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  133. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
  134. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  135. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  136. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  137. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  138. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  139. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  140. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  141. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
  142. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  143. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  144. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/server.py +0 -0
  145. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/settings_ui/__init__.py +0 -0
  146. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
  147. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
  148. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
  149. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
  150. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
  151. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/settings_ui/_i18n.py +0 -0
  152. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/settings_ui/_locale_policy.py +0 -0
  153. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/settings_ui/_persistence.py +0 -0
  154. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
  155. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/settings_ui/_theme.py +0 -0
  156. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
  157. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/settings_ui/locales/README.md +0 -0
  158. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/settings_ui/locales/cs.json +0 -0
  159. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/settings_ui/locales/de.json +0 -0
  160. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/settings_ui/locales/eo.json +0 -0
  161. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/settings_ui/locales/es.json +0 -0
  162. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/settings_ui/locales/fr.json +0 -0
  163. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/settings_ui/locales/it.json +0 -0
  164. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/settings_ui/locales/ko.json +0 -0
  165. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/settings_ui/locales/nl.json +0 -0
  166. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/settings_ui/locales/pl.json +0 -0
  167. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/settings_ui/locales/ru.json +0 -0
  168. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/settings_ui/locales/sv.json +0 -0
  169. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/settings_ui/locales/tlh.json +0 -0
  170. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/settings_ui/locales/zh-Hans.json +0 -0
  171. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/settings_ui/settings.css +0 -0
  172. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/settings_ui/settings.html +0 -0
  173. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/smoke_test.py +0 -0
  174. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  175. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/strict_bps.py +0 -0
  176. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/__init__.py +0 -0
  177. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/auto_backup.py +0 -0
  178. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/backup.py +0 -0
  179. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  180. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/best_practice_result.py +0 -0
  181. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/best_practice_variables.py +0 -0
  182. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/bulk_selector.py +0 -0
  183. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/component_api.py +0 -0
  184. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/component_config_entries.py +0 -0
  185. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/component_config_reads.py +0 -0
  186. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/component_devices.py +0 -0
  187. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/component_registries.py +0 -0
  188. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/component_registry_lookup.py +0 -0
  189. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/config_entry_flow.py +0 -0
  190. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/config_entry_flow_form.py +0 -0
  191. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/config_entry_flow_menu.py +0 -0
  192. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/config_entry_flow_walker.py +0 -0
  193. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/config_entry_identity.py +0 -0
  194. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/config_entry_reconfigure.py +0 -0
  195. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/config_entry_reload_watch.py +0 -0
  196. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/device_control.py +0 -0
  197. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/hacs_registration.py +0 -0
  198. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/helpers.py +0 -0
  199. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/integration_reconfigure.py +0 -0
  200. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/radio/__init__.py +0 -0
  201. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/radio/base.py +0 -0
  202. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/radio/matter.py +0 -0
  203. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/radio/thread.py +0 -0
  204. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/radio/zigbee.py +0 -0
  205. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/radio/zwave.py +0 -0
  206. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/reference_validator.py +0 -0
  207. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/renamed_tool_middleware.py +0 -0
  208. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  209. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  210. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  211. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
  212. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
  213. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  214. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/smart_search/_graph.py +0 -0
  215. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  216. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  217. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  218. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
  219. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/tools_addons.py +0 -0
  220. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/tools_areas.py +0 -0
  221. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/tools_assist_pipeline.py +0 -0
  222. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  223. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/tools_calendar.py +0 -0
  224. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/tools_camera.py +0 -0
  225. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/tools_categories.py +0 -0
  226. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/tools_code.py +0 -0
  227. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  228. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  229. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  230. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  231. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  232. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  233. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/tools_dev.py +0 -0
  234. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/tools_energy.py +0 -0
  235. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/tools_entities.py +0 -0
  236. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  237. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/tools_groups.py +0 -0
  238. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/tools_hacs.py +0 -0
  239. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/tools_history.py +0 -0
  240. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/tools_integrations.py +0 -0
  241. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/tools_labels.py +0 -0
  242. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/tools_radio.py +0 -0
  243. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/tools_registry.py +0 -0
  244. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/tools_resources.py +0 -0
  245. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/tools_search.py +0 -0
  246. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/tools_security_policy.py +0 -0
  247. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/tools_service.py +0 -0
  248. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/tools_services.py +0 -0
  249. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/tools_system.py +0 -0
  250. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/tools_themes.py +0 -0
  251. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/tools_todo.py +0 -0
  252. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/tools_traces.py +0 -0
  253. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/tools_updates.py +0 -0
  254. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  255. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  256. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
  257. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/tools_zones.py +0 -0
  258. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/util_helpers.py +0 -0
  259. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/tools/validation_middleware.py +0 -0
  260. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/transforms/__init__.py +0 -0
  261. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/transforms/categorized_search.py +0 -0
  262. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  263. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/transport_security.py +0 -0
  264. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/update_check.py +0 -0
  265. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/utils/__init__.py +0 -0
  266. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/utils/config_hash.py +0 -0
  267. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/utils/config_write_lock.py +0 -0
  268. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/utils/data_paths.py +0 -0
  269. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/utils/domain_handlers.py +0 -0
  270. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/utils/entity_membership.py +0 -0
  271. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  272. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  273. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/utils/operation_manager.py +0 -0
  274. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/utils/python_sandbox.py +0 -0
  275. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/utils/skill_loader.py +0 -0
  276. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/utils/usage_logger.py +0 -0
  277. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/visibility/__init__.py +0 -0
  278. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/visibility/enforcement.py +0 -0
  279. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/visibility/model.py +0 -0
  280. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/visibility/persistence.py +0 -0
  281. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp/visibility/resolver.py +0 -0
  282. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  283. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  284. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  285. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  286. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/tests/__init__.py +0 -0
  287. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/tests/test_constants.py +0 -0
  288. {ha_mcp_dev-8.3.0.dev2442 → ha_mcp_dev-8.3.0.dev2445}/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: 8.3.0.dev2442
3
+ Version: 8.3.0.dev2445
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 = "8.3.0.dev2442"
7
+ version = "8.3.0.dev2445"
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
@@ -8,6 +8,7 @@ import logging
8
8
  import os
9
9
  import ssl
10
10
  import time
11
+ from collections.abc import Awaitable, Callable
11
12
  from dataclasses import dataclass
12
13
  from typing import Any, NoReturn
13
14
 
@@ -52,10 +53,23 @@ logger = logging.getLogger(__name__)
52
53
  _RETRYABLE_GATEWAY_STATUS = frozenset({502, 503, 504})
53
54
  _SAFE_METHODS = frozenset({"GET", "HEAD", "OPTIONS"})
54
55
  _MAX_REQUEST_ATTEMPTS = 3
55
- # Journald window requested for the Core error log on Supervisor-backed
56
- # installs. Both such branches of get_error_log() build their request from this
57
- # constant, so the window they ask for cannot drift apart.
58
- _ERROR_LOG_LINES = 20000
56
+ # Smallest log window this client requests. Supervisor's Range syntax addresses
57
+ # a newest-anchored window by its first entry, so a one-entry one is
58
+ # inexpressible (``entries=:0:1`` reads from the OLDEST entry instead);
59
+ # Supervisor coerces its own ``?lines=1`` to 2 (supervisor/api/host.py) and this
60
+ # client mirrors that on every route, so all install types serve the same slice.
61
+ # Shared with the tool layer, which sizes its fetch window against it.
62
+ MIN_LOG_WINDOW_LINES = 2
63
+ # Entries the has_more probe reads from just behind a full window. A one-entry
64
+ # probe compared one rendered line, which a boundary duplicate (the same
65
+ # timestamp-less traceback line logged twice) could false-match into "end of
66
+ # history". Comparing a block this deep requires that many CONSECUTIVE
67
+ # identical lines to straddle the boundary — and then the history a false stop
68
+ # skips is more of the same duplicates. Timestamps cannot close the gap
69
+ # instead: the colliding lines are exactly the ones without them, and
70
+ # journald's cursor/timestamp metadata exists only in the journal export
71
+ # format, which the hassio-proxy route cannot request.
72
+ _PROBE_ENTRIES = 8
59
73
 
60
74
 
61
75
  class HomeAssistantError(Exception):
@@ -96,7 +110,7 @@ class HomeAssistantAuthError(HomeAssistantError):
96
110
  auth errors as part of the local "this entity is not registered yet"
97
111
  polling logic. Sites that specifically need to catch both must list
98
112
  them explicitly (see ``_get_supervisor_log`` and
99
- ``_get_system_service_log`` in ``tools_utility.py``).
113
+ ``_get_system_service_log`` in ``log_sources_supervisor.py``).
100
114
  """
101
115
 
102
116
 
@@ -114,6 +128,19 @@ class HomeAssistantAPIError(HomeAssistantError):
114
128
  self.response_data = response_data
115
129
 
116
130
 
131
+ @dataclass(frozen=True)
132
+ class ErrorLogPage:
133
+ """One bounded window of the error log, plus whether older history exists.
134
+
135
+ ``has_more`` cannot be derived by the caller from ``text``: a journald
136
+ window that comes back full may or may not have anything behind it (see
137
+ ``_journald_error_log_page``), and only the fetching route knows which.
138
+ """
139
+
140
+ text: str
141
+ has_more: bool
142
+
143
+
117
144
  @dataclass(frozen=True)
118
145
  class SceneResolution:
119
146
  """Outcome of resolving a scene identifier via the entity registry.
@@ -579,8 +606,140 @@ class HomeAssistantClient:
579
606
  logger.debug("Checking configuration")
580
607
  return await self._request("POST", "/config/core/check_config")
581
608
 
582
- async def get_error_log(self) -> str:
583
- """Get Home Assistant error log.
609
+ @staticmethod
610
+ def _journald_range_header(lines: int, offset: int = 0) -> str:
611
+ """Build Supervisor's journald ``Range`` header for a bounded window.
612
+
613
+ Supervisor's log endpoints accept
614
+ ``Range: entries=<cursor>:<num_skip>:<num_entries>`` and build
615
+ ``entries=:-{N-1}:{N}`` themselves for a ``?lines=N`` request
616
+ (supervisor/api/host.py). ``offset`` moves the window that many entries
617
+ further back, so the returned slice ends ``offset`` entries before the
618
+ newest one.
619
+
620
+ A ``lines=1`` window is only expressible with a non-zero ``offset``: at
621
+ ``offset=0`` the header degenerates to ``entries=:0:1``, whose
622
+ non-negative skip reads from the OLDEST entry instead of the newest.
623
+ Callers anchoring at the newest entry pass ``lines`` already floored at
624
+ ``MIN_LOG_WINDOW_LINES``; the one-entry probe always carries an offset.
625
+ """
626
+ # Negate the computed skip rather than prefixing "-" so the degenerate
627
+ # skip of 0 renders as ":0:" (non-negative, reads from the oldest
628
+ # entry) instead of the ill-formed ":-0:".
629
+ return f"entries=:{-(offset + lines - 1)}:{lines}"
630
+
631
+ @staticmethod
632
+ def _container_error_log_page(text: str, lines: int, offset: int) -> ErrorLogPage:
633
+ """Window the whole-file ``/api/error_log`` body client-side.
634
+
635
+ Same tail semantics as the journald Range the Supervisor-backed
636
+ branches request, so ``get_error_log`` answers with the same slice of
637
+ history on every install type. Paging is exact here — the whole file is
638
+ in hand, so ``has_more`` is simply whether anything precedes the window.
639
+ An ``offset`` past the start of the file yields an empty final page.
640
+ """
641
+ all_lines = text.splitlines()
642
+ end = len(all_lines) - offset
643
+ if end <= 0:
644
+ return ErrorLogPage(text="", has_more=False)
645
+ start = max(end - lines, 0)
646
+ return ErrorLogPage(text="\n".join(all_lines[start:end]), has_more=start > 0)
647
+
648
+ async def _supervisor_core_logs(self, lines: int, offset: int) -> str:
649
+ """Fetch a journald window of HA Core's log straight from Supervisor."""
650
+ return await self._supervisor_logs_get("core", lines=lines, offset=offset)
651
+
652
+ async def _proxied_core_logs(self, lines: int, offset: int) -> str:
653
+ """Fetch the same window through HA Core's hassio proxy with the LLA."""
654
+ response = await self._raw_request(
655
+ "GET",
656
+ "/hassio/core/logs",
657
+ headers={
658
+ "Accept": "text/plain",
659
+ "Range": self._journald_range_header(lines, offset),
660
+ },
661
+ )
662
+ return response.text
663
+
664
+ async def _journald_error_log_page(
665
+ self,
666
+ lines: int,
667
+ offset: int,
668
+ fetch: Callable[[int, int], Awaitable[str]],
669
+ ) -> ErrorLogPage:
670
+ """Read one journald window and settle ``has_more`` with a 1-entry probe.
671
+
672
+ systemd's journal-gatewayd does not guard the negative-skip branch the
673
+ way it guards the positive one (``sd_journal_previous_skip(-n_skip + 1)``
674
+ returns no END_OF_STREAM): an offset past the start of the journal
675
+ CLAMPS to the oldest entry and still answers with a full window. So a
676
+ saturated window proves nothing about what lies behind it, and a
677
+ count-based ``has_more`` would stay true forever — an agent following
678
+ the pagination hint would loop on identical pages.
679
+
680
+ Hence: a short or empty window means the journal really is exhausted
681
+ (nothing clamps a window smaller), and a saturated one is settled by
682
+ asking for the ``_PROBE_ENTRIES`` entries that should sit just behind
683
+ it. If the journal has nothing there, the clamp starts the probe at
684
+ the same oldest entry the window starts with, so the probe block
685
+ equals the window's opening lines — identity means end of history. A
686
+ differing block proves deeper history (an under-full span behind the
687
+ window also clamps and misaligns, which correctly reads as "more").
688
+ The identity check also terminates the degraded case where an
689
+ intermediary strips the ``Range`` header (RFC 7233 permits it): both
690
+ requests then return Supervisor's default window and compare equal.
691
+
692
+ The saturation test counts lines only to decide whether to probe, never
693
+ to answer: one journald entry can be a multi-line traceback, so the line
694
+ count over-estimates entries and can only over-trigger the probe, which
695
+ then gives the real answer. In practice the two rarely diverge here:
696
+ docker's journald log driver — how HAOS captures Core's output —
697
+ ingests stdout line by line, one journal entry per rendered line, so
698
+ multi-line entries reach this endpoint only from native journal-API
699
+ writers.
700
+
701
+ Known imprecision, accepted: the identity check can still stop one
702
+ page early when the compared block — ``_PROBE_ENTRIES`` lines, or the
703
+ window's own length when that is smaller — is byte-identical across
704
+ the boundary (see the constant's comment for why timestamps cannot
705
+ disambiguate instead). The failure
706
+ direction is a missed page of exactly those duplicates — bounded,
707
+ unlike the infinite loop a count-based ``has_more`` produces on the
708
+ clamp.
709
+ """
710
+ text = await fetch(lines, offset)
711
+ if not text or len(text.splitlines()) < lines:
712
+ return ErrorLogPage(text=text, has_more=False)
713
+ probe = await fetch(_PROBE_ENTRIES, offset + lines)
714
+ probe_lines = probe.splitlines()
715
+ window_lines = text.splitlines()
716
+ # Compare the shared-length prefix: with a window smaller than the
717
+ # probe (lines < _PROBE_ENTRIES), an end-of-history clamp returns a
718
+ # probe LONGER than the window, and a full-length compare would read
719
+ # that as "more history" forever — the small-limit loop this method
720
+ # exists to prevent.
721
+ shared = min(len(probe_lines), len(window_lines))
722
+ return ErrorLogPage(
723
+ text=text,
724
+ has_more=shared > 0 and probe_lines[:shared] != window_lines[:shared],
725
+ )
726
+
727
+ async def get_error_log(self, lines: int, offset: int = 0) -> ErrorLogPage:
728
+ """Get a bounded window of the Home Assistant error log.
729
+
730
+ ``lines`` is the number of log lines to fetch and ``offset`` how many
731
+ newer lines to skip, counted back from the most recent one; both are
732
+ floored/served at ``MIN_LOG_WINDOW_LINES``. Returns the window text and
733
+ whether older history remains behind it. The window is mandatory: the
734
+ unconditional 20,000-line fetch this replaces made Supervisor assemble
735
+ a journald slice for 15+ minutes on real hardware (#2279).
736
+
737
+ The whole body runs under one ``asyncio.timeout(self.timeout)``. That
738
+ is the only bound that holds here: ``httpx.Timeout`` applies per I/O
739
+ operation, so a server trickling bytes while it assembles the body
740
+ resets the read timeout indefinitely and no per-attempt timeout ever
741
+ fires — the mechanism behind #2279's hang. The deadline covers the
742
+ install-class probe, the log fetch and the ``has_more`` probe together.
584
743
 
585
744
  Three-way branch depending on how this client reaches HA:
586
745
 
@@ -601,6 +760,17 @@ class HomeAssistantClient:
601
760
  - **External client → Container/pip HA** (neither of the above):
602
761
  keep the historical ``/api/error_log`` proxy path.
603
762
 
763
+ The two Supervisor-backed branches express the window as a journald
764
+ ``Range`` header (see ``_journald_range_header``) and settle
765
+ ``has_more`` with a one-entry probe (see ``_journald_error_log_page``).
766
+ HA Core's hassio proxy forwards that header for its log paths
767
+ (``PATHS_LOGS`` in ``homeassistant/components/hassio/http.py``); an
768
+ intermediary that stripped it would degrade to Supervisor's 100-line
769
+ default, never to an unbounded fetch, and the probe still terminates
770
+ paging. ``/api/error_log`` is a whole-file plain-text endpoint with no
771
+ window support, so the container branch reads the file and applies the
772
+ same window client-side, where paging is exact.
773
+
604
774
  The middle branch was discovered by the HAOS E2E tier (#1326): the
605
775
  test harness runs ha-mcp externally against a booted HAOS, hits
606
776
  the unregistered endpoint, and the old binary branch surfaced as
@@ -615,30 +785,36 @@ class HomeAssistantClient:
615
785
  HomeAssistantConnectionError: Network, timeout, or transport
616
786
  error.
617
787
  """
618
- if is_running_in_addon():
619
- logger.debug("Fetching error log via Supervisor direct (core service)")
620
- # An explicit `lines` is required: without it Supervisor applies its
621
- # 100-line default, which is far too short a slice to tell what keeps
622
- # repeating. `_get_supervisor_log` plumbs the same parameter for the
623
- # same reason (#1734).
624
- return await self._supervisor_logs_get("core", lines=_ERROR_LOG_LINES)
625
-
626
- if await self._is_supervised_install():
627
- logger.debug(
628
- "Fetching error log via HA Core /hassio/core/logs proxy (supervised)"
629
- )
630
- raw_response = await self._raw_request(
631
- "GET",
632
- f"/hassio/core/logs?lines={_ERROR_LOG_LINES}",
633
- headers={"Accept": "text/plain"},
634
- )
635
- return raw_response.text
788
+ window = max(lines, MIN_LOG_WINDOW_LINES)
789
+ try:
790
+ async with asyncio.timeout(self.timeout):
791
+ if is_running_in_addon():
792
+ logger.debug(
793
+ "Fetching error log via Supervisor direct (core service)"
794
+ )
795
+ return await self._journald_error_log_page(
796
+ window, offset, self._supervisor_core_logs
797
+ )
636
798
 
637
- logger.debug("Fetching error log via HA Core proxy (Container/pip)")
638
- raw_response = await self._raw_request(
639
- "GET", "/error_log", headers={"Accept": "text/plain"}
640
- )
641
- return raw_response.text
799
+ if await self._is_supervised_install():
800
+ logger.debug(
801
+ "Fetching error log via HA Core /hassio/core/logs proxy "
802
+ "(supervised)"
803
+ )
804
+ return await self._journald_error_log_page(
805
+ window, offset, self._proxied_core_logs
806
+ )
807
+
808
+ logger.debug("Fetching error log via HA Core proxy (Container/pip)")
809
+ raw_response = await self._raw_request(
810
+ "GET", "/error_log", headers={"Accept": "text/plain"}
811
+ )
812
+ return self._container_error_log_page(raw_response.text, window, offset)
813
+ except TimeoutError as e:
814
+ raise HomeAssistantConnectionError(
815
+ f"Timeout fetching the error log after {self.timeout}s: "
816
+ f"{str(e) or type(e).__name__}"
817
+ ) from e
642
818
 
643
819
  async def _is_supervised_install(self) -> bool:
644
820
  """Detect whether the target HA is a Supervised / HAOS install.
@@ -742,12 +918,30 @@ class HomeAssistantClient:
742
918
  return await self._get_addon_logs_via_supervisor(slug, lines=lines)
743
919
 
744
920
  logger.debug(f"Fetching addon logs for slug={slug} via HA Core proxy")
745
- response = await self._raw_request(
746
- "GET",
747
- f"/hassio/addons/{slug}/logs",
748
- headers={"Accept": "text/plain"},
749
- params={"lines": lines} if lines is not None else None,
750
- )
921
+ return await self._proxied_logs_get(f"addons/{slug}", lines)
922
+
923
+ async def _proxied_logs_get(self, path: str, lines: int | None) -> str:
924
+ """Fetch ``text/plain`` logs through HA Core's hassio proxy.
925
+
926
+ Wrapped in an overall wall-clock deadline for the same reason as the
927
+ error-log routes (#2279): ``httpx.Timeout`` applies per I/O operation,
928
+ so a server trickling bytes while assembling the body resets the read
929
+ timeout indefinitely, and the deadline is the only bound that holds.
930
+ It caps the whole ``_raw_request`` retry envelope.
931
+ """
932
+ try:
933
+ async with asyncio.timeout(self.timeout):
934
+ response = await self._raw_request(
935
+ "GET",
936
+ f"/hassio/{path}/logs",
937
+ headers={"Accept": "text/plain"},
938
+ params={"lines": lines} if lines is not None else None,
939
+ )
940
+ except TimeoutError as e:
941
+ raise HomeAssistantConnectionError(
942
+ f"Timeout fetching /hassio/{path}/logs from Home Assistant "
943
+ f"after {self.timeout}s: {str(e) or type(e).__name__}"
944
+ ) from e
751
945
  return response.text
752
946
 
753
947
  @staticmethod
@@ -772,7 +966,12 @@ class HomeAssistantClient:
772
966
  message = text_body.strip() or reason_phrase or "<empty body>"
773
967
  return message
774
968
 
775
- async def _supervisor_logs_get(self, path: str, lines: int | None = None) -> str:
969
+ async def _supervisor_logs_get(
970
+ self,
971
+ path: str,
972
+ lines: int | None = None,
973
+ offset: int | None = None,
974
+ ) -> str:
776
975
  """Fetch ``text/plain`` logs from a Supervisor REST endpoint.
777
976
 
778
977
  ``path`` is everything between ``http://supervisor/`` and ``/logs``:
@@ -782,11 +981,22 @@ class HomeAssistantClient:
782
981
  cli, multicast, observer}) for system-service logs
783
982
 
784
983
  ``lines`` maps to the endpoint's ``?lines=`` journald-window query
785
- param; omitted → Supervisor's 100-line default window.
984
+ param; omitted → Supervisor's 100-line default window. Adding
985
+ ``offset`` switches the same window to a ``Range: entries=`` header
986
+ (see ``_journald_range_header``), which is the only form that can
987
+ address an older window. The two forms are mutually exclusive on the
988
+ wire — Supervisor tests ``if "lines" in request.query`` first and
989
+ ignores a ``Range`` header whenever that query param is present — so
990
+ the offset form sends the window in the header and no query param,
991
+ rather than a pair that could contradict each other.
786
992
 
787
993
  Bypasses ``HomeAssistantClient.httpx_client`` because that client targets
788
994
  Home Assistant Core through ``http://supervisor/core/api``, while logs
789
- belong to Supervisor at ``http://supervisor``. Both clients use the same
995
+ belong to Supervisor at ``http://supervisor``. Plain HTTP here is the
996
+ platform contract, not a downgrade: Supervisor serves its API over
997
+ HTTP only (no TLS endpoint or IPC socket exists), on the internal
998
+ ``hassio`` docker network whose isolation is the boundary protecting
999
+ the token — see SECURITY.md § "Local network is the trusted zone". Both clients use the same
790
1000
  ``SUPERVISOR_TOKEN``; the Core proxy requires ``homeassistant_api``, while
791
1001
  system-service and arbitrary app-log paths require ``hassio_api`` and
792
1002
  ``hassio_role: manager``. The recognized-app-token exception is
@@ -815,11 +1025,19 @@ class HomeAssistantClient:
815
1025
  )
816
1026
 
817
1027
  relative_path = f"/{path}/logs"
1028
+ headers = {"Accept": "text/plain"}
1029
+ params = {"lines": lines} if lines is not None else None
1030
+ if offset is not None:
1031
+ # An offset window with no size asked for falls back to the
1032
+ # smallest one the Range syntax can express.
1033
+ size = lines if lines is not None else MIN_LOG_WINDOW_LINES
1034
+ headers["Range"] = self._journald_range_header(size, offset)
1035
+ params = None
818
1036
  logger.debug(
819
- "Fetching %s%s via Supervisor direct (lines=%s)",
1037
+ "Fetching %s%s via Supervisor direct (%s)",
820
1038
  get_supervisor_base_url(),
821
1039
  relative_path,
822
- lines,
1040
+ f"range={headers['Range']}" if offset is not None else f"lines={lines}",
823
1041
  )
824
1042
 
825
1043
  try:
@@ -830,8 +1048,8 @@ class HomeAssistantClient:
830
1048
  ) as client:
831
1049
  response = await client.get(
832
1050
  relative_path,
833
- headers={"Accept": "text/plain"},
834
- params={"lines": lines} if lines is not None else None,
1051
+ headers=headers,
1052
+ params=params,
835
1053
  )
836
1054
  except (TimeoutError, httpx.TimeoutException) as e:
837
1055
  raise HomeAssistantConnectionError(
@@ -924,13 +1142,7 @@ class HomeAssistantClient:
924
1142
  return await self._supervisor_logs_get(service, lines=lines)
925
1143
 
926
1144
  logger.debug(f"Fetching {service} logs via HA Core proxy")
927
- response = await self._raw_request(
928
- "GET",
929
- f"/hassio/{service}/logs",
930
- headers={"Accept": "text/plain"},
931
- params={"lines": lines} if lines is not None else None,
932
- )
933
- return response.text
1145
+ return await self._proxied_logs_get(service, lines)
934
1146
 
935
1147
  async def test_connection(self) -> tuple[bool, str | None]:
936
1148
  """
@@ -19,6 +19,7 @@ import logging
19
19
  import os
20
20
  import time
21
21
  import uuid
22
+ import weakref
22
23
  from collections.abc import Callable, Container
23
24
  from typing import TYPE_CHECKING, Any
24
25
 
@@ -323,6 +324,28 @@ async def _restart_addon(
323
324
  # ---- Settings info ----
324
325
 
325
326
 
327
+ # Keyed by server instance so a replaced worker drops out with its instance;
328
+ # the module-level dict itself survives entry reloads (modules stay cached).
329
+ _WORKER_INSTANCE_IDS: weakref.WeakKeyDictionary[Any, str] = weakref.WeakKeyDictionary()
330
+
331
+
332
+ def _worker_instance_id(server: HomeAssistantSmartMCPServer) -> str:
333
+ """Per-worker identity for the restart poll's embedded mode.
334
+
335
+ ``_PROCESS_INSTANCE_ID`` proves a process swap, but an embedded restart
336
+ reloads the config entry INSIDE the surviving HA process — modules stay
337
+ cached, so the process id never flips and the poll timed out after every
338
+ successful reload (#2279 feedback). A new server instance is constructed
339
+ per entry setup, so an id lazily pinned to the instance flips exactly
340
+ when the reload completes.
341
+ """
342
+ wid = _WORKER_INSTANCE_IDS.get(server)
343
+ if wid is None:
344
+ wid = uuid.uuid4().hex
345
+ _WORKER_INSTANCE_IDS[server] = wid
346
+ return wid
347
+
348
+
326
349
  async def _settings_info(
327
350
  server: HomeAssistantSmartMCPServer | None, is_sidecar: bool, _: Request
328
351
  ) -> JSONResponse:
@@ -355,6 +378,13 @@ async def _settings_info(
355
378
  "sidecar" if is_sidecar else _detect_installation_method()
356
379
  ),
357
380
  "instance_id": _PROCESS_INSTANCE_ID,
381
+ # Embedded restarts swap the server instance, not the process —
382
+ # the restart poll compares this field in that mode.
383
+ "worker_id": (
384
+ _worker_instance_id(server)
385
+ if server is not None
386
+ else _PROCESS_INSTANCE_ID
387
+ ),
358
388
  "started_at": _PROCESS_STARTED_AT,
359
389
  "version": version,
360
390
  }
@@ -184,6 +184,7 @@
184
184
  "status.stopped_connection": "Stopped (connection dropped)",
185
185
  "status.restarting": "Restarting…",
186
186
  "status.waiting_addon": "Waiting for app (add-on) to come back online…",
187
+ "status.waiting_server": "Waiting for the HA-MCP server to come back online…",
187
188
  "errors.javascript": "JS error: {message} @ {where}",
188
189
  "errors.async": "Async error: {message}",
189
190
  "errors.network": "Network error: {message}",
@@ -200,6 +201,7 @@
200
201
  "errors.restart_failed": "Restart failed",
201
202
  "errors.failed_detail": "Failed: {detail}",
202
203
  "errors.addon_not_back": "App (add-on) did not come back online. Reload the page manually.",
204
+ "errors.server_not_back": "The HA-MCP server did not come back online. Reload the page manually.",
203
205
  "errors.unparseable_suffix": " (response body unparseable)",
204
206
  "common.operation_failed": "{operation} failed: {detail}",
205
207
  "common.enabled": "enabled",
@@ -509,6 +509,7 @@ async function applyInfoChrome() {
509
509
  // worker onto the freshly installed code.
510
510
  const rbtn = document.getElementById('restartBtn');
511
511
  rbtn.style.display = '';
512
+ restartTargetEmbedded = true;
512
513
  rbtn.textContent = t('actions.restart_server', {}, 'Restart HA-MCP Server');
513
514
  if (noticeEl) {
514
515
  noticeEl.textContent = t(
@@ -653,6 +654,10 @@ function markRestartRequired() {
653
654
  // again); otherwise stays true through the restart cycle until the
654
655
  // page reloads.
655
656
  let restartInProgress = false;
657
+ // True when the restart button drives the embedded (custom component) server
658
+ // rather than the app (add-on) — the wait/give-up copy must name the right
659
+ // thing (issue #2279 feedback: the embedded flow said "app" throughout).
660
+ let restartTargetEmbedded = false;
656
661
 
657
662
  async function _fetchSettingsInfo() {
658
663
  // Read ``/api/settings/info`` once; return the parsed JSON or null
@@ -682,7 +687,8 @@ async function _probeAddonRestarted(previousInstanceId) {
682
687
  const info = await _fetchSettingsInfo();
683
688
  if (info) {
684
689
  if (previousInstanceId) {
685
- if (info.instance_id && info.instance_id !== previousInstanceId) {
690
+ const current = restartTargetEmbedded ? info.worker_id : info.instance_id;
691
+ if (current && current !== previousInstanceId) {
686
692
  return true;
687
693
  }
688
694
  // Same instance_id (or field missing on the response) — keep
@@ -706,7 +712,9 @@ async function _runRestartReloadCycle(previousInstanceId) {
706
712
  // instance and we reload before the new one is up.
707
713
  btn.textContent = t('status.restarting', {}, 'Restarting…');
708
714
  await new Promise(r => setTimeout(r, RESTART_PROBE_INITIAL_GRACE_MS));
709
- btn.textContent = t('status.waiting_addon', {}, 'Waiting for App (add-on) to come back online…');
715
+ btn.textContent = restartTargetEmbedded
716
+ ? t('status.waiting_server', {}, 'Waiting for the HA-MCP server to come back online…')
717
+ : t('status.waiting_addon', {}, 'Waiting for App (add-on) to come back online…');
710
718
  const restarted = await _probeAddonRestarted(previousInstanceId);
711
719
  if (restarted) {
712
720
  window.location.reload();
@@ -715,7 +723,9 @@ async function _runRestartReloadCycle(previousInstanceId) {
715
723
  // never actually fired (silent supervisor failure → instance_id
716
724
  // never flipped) OR supervisor is genuinely slower than the cap.
717
725
  // Surface a clear next-step instead of silently doing nothing.
718
- btn.textContent = t('errors.addon_not_back', {}, 'App (add-on) did not come back online. Reload the page manually.');
726
+ btn.textContent = restartTargetEmbedded
727
+ ? t('errors.server_not_back', {}, 'The HA-MCP server did not come back online. Reload the page manually.')
728
+ : t('errors.addon_not_back', {}, 'App (add-on) did not come back online. Reload the page manually.');
719
729
  btn.disabled = false;
720
730
  restartInProgress = false;
721
731
  }
@@ -733,7 +743,12 @@ async function restartAddon() {
733
743
  // null is fine — the probe degrades to the old "any 200 means up"
734
744
  // mode rather than refusing to reload.
735
745
  const info = await _fetchSettingsInfo();
736
- const previousInstanceId = info?.instance_id ?? null;
746
+ // Embedded restarts reload the config entry inside the surviving HA
747
+ // process, so instance_id never flips there; worker_id is pinned to the
748
+ // server instance and flips exactly when the reload completes.
749
+ const previousInstanceId = restartTargetEmbedded
750
+ ? (info?.worker_id ?? null)
751
+ : (info?.instance_id ?? null);
737
752
  try {
738
753
  const resp = await fetch('./api/settings/restart', {method: 'POST'});
739
754
  if (!resp.ok && resp.status < 500) {
@@ -769,6 +784,10 @@ async function restartAddon() {
769
784
  restartChannel.postMessage({
770
785
  type: 'restart-initiated',
771
786
  previousInstanceId,
787
+ // Receivers may get this before their own init has classified the
788
+ // deployment; without the sender's flag they would compare an
789
+ // embedded worker_id baseline against instance_id and time out.
790
+ targetEmbedded: restartTargetEmbedded,
772
791
  });
773
792
  }
774
793
  await _runRestartReloadCycle(previousInstanceId);
@@ -787,10 +806,15 @@ if (restartChannel) {
787
806
  restartInProgress = true;
788
807
  const btn = document.getElementById('restartBtn');
789
808
  if (btn) btn.disabled = true;
790
- // Use the originating tab's baseline ``instance_id`` so every
791
- // tab waits for the SAME ``instance_id`` flip before reloading.
792
- // Falls back to null → "any 200 = ready" mode if the originator
793
- // couldn't capture one.
809
+ // Use the originating tab's baseline so every tab waits for the
810
+ // SAME identity flip before reloading, and adopt its deployment
811
+ // classification too: this listener can fire before this tab's own
812
+ // init resolved it, and an embedded worker_id baseline compared
813
+ // against instance_id would never flip. Falls back to null →
814
+ // "any 200 = ready" mode if the originator couldn't capture one.
815
+ if (typeof data.targetEmbedded === 'boolean') {
816
+ restartTargetEmbedded = data.targetEmbedded;
817
+ }
794
818
  _runRestartReloadCycle(data.previousInstanceId ?? null);
795
819
  }
796
820
  });