ha-mcp-dev 8.4.3.dev2581__tar.gz → 8.4.3.dev2587__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 (310) hide show
  1. {ha_mcp_dev-8.4.3.dev2581/src/ha_mcp_dev.egg-info → ha_mcp_dev-8.4.3.dev2587}/PKG-INFO +1 -1
  2. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/pyproject.toml +1 -1
  3. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/config.py +13 -0
  4. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/settings_ui/locales/en.json +2 -0
  5. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/settings_ui/settings.js +1 -0
  6. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/tools_history.py +406 -22
  7. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/util_helpers.py +5 -5
  8. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  9. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/LICENSE +0 -0
  10. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/MANIFEST.in +0 -0
  11. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/README.md +0 -0
  12. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/setup.cfg +0 -0
  13. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/__init__.py +0 -0
  14. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/__main__.py +0 -0
  15. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_pypi_marker +0 -0
  16. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_rich_compat.py +0 -0
  17. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/__init__.py +0 -0
  18. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/requirements.txt +0 -0
  19. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/LICENSE +0 -0
  20. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/MANIFEST.sha256 +0 -0
  21. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/VENDORED +0 -0
  22. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/__init__.py +0 -0
  23. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/__main__.py +0 -0
  24. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/asyncio/__init__.py +0 -0
  25. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/asyncio/client.py +0 -0
  26. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/asyncio/connection.py +0 -0
  27. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/asyncio/messages.py +0 -0
  28. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/asyncio/router.py +0 -0
  29. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/asyncio/server.py +0 -0
  30. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/auth.py +0 -0
  31. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/cli.py +0 -0
  32. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/client.py +0 -0
  33. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/connection.py +0 -0
  34. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/datastructures.py +0 -0
  35. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/exceptions.py +0 -0
  36. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/extensions/__init__.py +0 -0
  37. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/extensions/base.py +0 -0
  38. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/extensions/permessage_deflate.py +0 -0
  39. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/frames.py +0 -0
  40. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/headers.py +0 -0
  41. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/http11.py +0 -0
  42. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/imports.py +0 -0
  43. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/legacy/__init__.py +0 -0
  44. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/legacy/auth.py +0 -0
  45. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/legacy/client.py +0 -0
  46. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/legacy/exceptions.py +0 -0
  47. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/legacy/framing.py +0 -0
  48. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/legacy/handshake.py +0 -0
  49. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/legacy/http.py +0 -0
  50. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/legacy/protocol.py +0 -0
  51. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/legacy/server.py +0 -0
  52. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/protocol.py +0 -0
  53. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/proxy.py +0 -0
  54. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/py.typed +0 -0
  55. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/server.py +0 -0
  56. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/streams.py +0 -0
  57. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/sync/__init__.py +0 -0
  58. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/sync/client.py +0 -0
  59. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/sync/connection.py +0 -0
  60. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/sync/messages.py +0 -0
  61. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/sync/router.py +0 -0
  62. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/sync/server.py +0 -0
  63. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/sync/utils.py +0 -0
  64. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/trio/__init__.py +0 -0
  65. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/trio/client.py +0 -0
  66. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/trio/connection.py +0 -0
  67. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/trio/messages.py +0 -0
  68. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/trio/router.py +0 -0
  69. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/trio/server.py +0 -0
  70. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/trio/utils.py +0 -0
  71. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/typing.py +0 -0
  72. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/uri.py +0 -0
  73. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/utils.py +0 -0
  74. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_vendor/websockets/version.py +0 -0
  75. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/_version.py +0 -0
  76. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/auth/__init__.py +0 -0
  77. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/auth/consent_form.py +0 -0
  78. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/auth/oidc_compat.py +0 -0
  79. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/auth/provider.py +0 -0
  80. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/backup_manager.py +0 -0
  81. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/browser_landing.py +0 -0
  82. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/client/__init__.py +0 -0
  83. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/client/rest_client.py +0 -0
  84. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/client/supervisor_client.py +0 -0
  85. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/client/websocket_client.py +0 -0
  86. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/client/websocket_listener.py +0 -0
  87. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  88. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  89. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
  90. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
  91. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  92. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/dashboard_screenshot/theme_guard.py +0 -0
  93. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/errors.py +0 -0
  94. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/ha_request_queue.py +0 -0
  95. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/hacs_auto_refresh.py +0 -0
  96. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/llm_exposure.py +0 -0
  97. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/log_filters.py +0 -0
  98. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/policy/__init__.py +0 -0
  99. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/policy/approval_queue.py +0 -0
  100. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/policy/editing.py +0 -0
  101. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/policy/evaluator.py +0 -0
  102. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/policy/handlers.py +0 -0
  103. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/policy/middleware.py +0 -0
  104. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/policy/model.py +0 -0
  105. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/policy/persistence.py +0 -0
  106. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/policy/value_sources.py +0 -0
  107. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/py.typed +0 -0
  108. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/read_only.py +0 -0
  109. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/redaction.py +0 -0
  110. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/renamed_tools.py +0 -0
  111. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  112. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  113. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  114. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  115. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  116. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  117. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  118. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  119. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  120. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  121. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/resources/skills-vendor/evals/area-target-over-entity-list/case.yaml +0 -0
  122. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/resources/skills-vendor/evals/arrive-home-automation/case.yaml +0 -0
  123. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/resources/skills-vendor/evals/assist-shortcut-card/case.yaml +0 -0
  124. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/resources/skills-vendor/evals/avg-temperature/case.yaml +0 -0
  125. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/resources/skills-vendor/evals/backup-deletion-confirmation/case.yaml +0 -0
  126. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/resources/skills-vendor/evals/blueprint-authoring/case.yaml +0 -0
  127. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/resources/skills-vendor/evals/button-event-entity/case.yaml +0 -0
  128. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/resources/skills-vendor/evals/device-id-trigger-migration/case.yaml +0 -0
  129. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/resources/skills-vendor/evals/helper-flow-fields-not-platform-yaml/case.yaml +0 -0
  130. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/resources/skills-vendor/evals/media-player-song/case.yaml +0 -0
  131. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/resources/skills-vendor/evals/motion-light-automation-mode/case.yaml +0 -0
  132. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/resources/skills-vendor/evals/rename-entity-impact-analysis/case.yaml +0 -0
  133. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/resources/skills-vendor/evals/template-attributes-yaml-exception/case.yaml +0 -0
  134. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/resources/skills-vendor/evals/timer-finished-automation/case.yaml +0 -0
  135. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/resources/skills-vendor/evals/vacuum-last-clean/case.yaml +0 -0
  136. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/resources/skills-vendor/scripts/check_eval_cases.py +0 -0
  137. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  138. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
  139. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  140. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/backups.md +0 -0
  141. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
  142. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  143. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  144. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  145. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  146. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  147. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  148. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  149. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
  150. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  151. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  152. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/server.py +0 -0
  153. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/settings_ui/__init__.py +0 -0
  154. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
  155. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
  156. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
  157. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
  158. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
  159. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
  160. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/settings_ui/_i18n.py +0 -0
  161. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/settings_ui/_locale_policy.py +0 -0
  162. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/settings_ui/_persistence.py +0 -0
  163. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
  164. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/settings_ui/_theme.py +0 -0
  165. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
  166. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/settings_ui/locales/README.md +0 -0
  167. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/settings_ui/locales/cs.json +0 -0
  168. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/settings_ui/locales/de.json +0 -0
  169. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/settings_ui/locales/eo.json +0 -0
  170. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/settings_ui/locales/es.json +0 -0
  171. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/settings_ui/locales/fr.json +0 -0
  172. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/settings_ui/locales/it.json +0 -0
  173. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/settings_ui/locales/ko.json +0 -0
  174. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/settings_ui/locales/nl.json +0 -0
  175. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/settings_ui/locales/pl.json +0 -0
  176. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/settings_ui/locales/ru.json +0 -0
  177. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/settings_ui/locales/sv.json +0 -0
  178. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/settings_ui/locales/tlh.json +0 -0
  179. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/settings_ui/locales/zh-Hans.json +0 -0
  180. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/settings_ui/settings.css +0 -0
  181. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/settings_ui/settings.html +0 -0
  182. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/smoke_test.py +0 -0
  183. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  184. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/strict_bps.py +0 -0
  185. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tool_dispatch.py +0 -0
  186. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/__init__.py +0 -0
  187. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/auto_backup.py +0 -0
  188. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/backup.py +0 -0
  189. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  190. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/best_practice_result.py +0 -0
  191. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/best_practice_variables.py +0 -0
  192. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/blueprint_sources.py +0 -0
  193. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/blueprint_substitute.py +0 -0
  194. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/blueprint_write.py +0 -0
  195. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/bulk_selector.py +0 -0
  196. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/component_api.py +0 -0
  197. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/component_config_entries.py +0 -0
  198. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/component_config_reads.py +0 -0
  199. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/component_devices.py +0 -0
  200. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/component_registries.py +0 -0
  201. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/component_registry_lookup.py +0 -0
  202. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/config_entry_flow.py +0 -0
  203. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/config_entry_flow_form.py +0 -0
  204. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/config_entry_flow_menu.py +0 -0
  205. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/config_entry_flow_walker.py +0 -0
  206. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/config_entry_identity.py +0 -0
  207. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/config_entry_reconfigure.py +0 -0
  208. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/config_entry_reload_watch.py +0 -0
  209. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/device_control.py +0 -0
  210. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/error_log_parsing.py +0 -0
  211. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/hacs_registration.py +0 -0
  212. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/helpers.py +0 -0
  213. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/integration_reconfigure.py +0 -0
  214. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/log_common.py +0 -0
  215. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/log_sources.py +0 -0
  216. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/log_sources_fault.py +0 -0
  217. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/log_sources_supervisor.py +0 -0
  218. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/radio/__init__.py +0 -0
  219. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/radio/base.py +0 -0
  220. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/radio/matter.py +0 -0
  221. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/radio/thread.py +0 -0
  222. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/radio/zigbee.py +0 -0
  223. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/radio/zwave.py +0 -0
  224. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/reference_validator.py +0 -0
  225. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/registry.py +0 -0
  226. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/renamed_tool_middleware.py +0 -0
  227. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  228. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  229. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  230. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
  231. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
  232. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  233. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/smart_search/_graph.py +0 -0
  234. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  235. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  236. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  237. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
  238. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/tools_addons.py +0 -0
  239. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/tools_areas.py +0 -0
  240. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/tools_assist_pipeline.py +0 -0
  241. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  242. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  243. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/tools_calendar.py +0 -0
  244. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/tools_camera.py +0 -0
  245. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/tools_categories.py +0 -0
  246. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/tools_code.py +0 -0
  247. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  248. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  249. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  250. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  251. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  252. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  253. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/tools_dev.py +0 -0
  254. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/tools_energy.py +0 -0
  255. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/tools_entities.py +0 -0
  256. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  257. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/tools_groups.py +0 -0
  258. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/tools_hacs.py +0 -0
  259. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/tools_integrations.py +0 -0
  260. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/tools_labels.py +0 -0
  261. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/tools_logs.py +0 -0
  262. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/tools_radio.py +0 -0
  263. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/tools_registry.py +0 -0
  264. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/tools_resources.py +0 -0
  265. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/tools_search.py +0 -0
  266. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/tools_security_policy.py +0 -0
  267. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/tools_service.py +0 -0
  268. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/tools_services.py +0 -0
  269. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/tools_system.py +0 -0
  270. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/tools_themes.py +0 -0
  271. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/tools_todo.py +0 -0
  272. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/tools_traces.py +0 -0
  273. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/tools_updates.py +0 -0
  274. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/tools_utility.py +0 -0
  275. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  276. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  277. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
  278. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/tools_zones.py +0 -0
  279. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/tools/validation_middleware.py +0 -0
  280. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/transforms/__init__.py +0 -0
  281. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/transforms/categorized_search.py +0 -0
  282. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  283. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/transport_security.py +0 -0
  284. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/update_check.py +0 -0
  285. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/utils/__init__.py +0 -0
  286. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/utils/config_hash.py +0 -0
  287. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/utils/config_write_lock.py +0 -0
  288. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/utils/data_paths.py +0 -0
  289. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/utils/device_registry_semantics.py +0 -0
  290. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/utils/domain_handlers.py +0 -0
  291. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/utils/entity_membership.py +0 -0
  292. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  293. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  294. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/utils/operation_manager.py +0 -0
  295. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/utils/python_sandbox.py +0 -0
  296. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/utils/skill_loader.py +0 -0
  297. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/utils/usage_logger.py +0 -0
  298. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/visibility/__init__.py +0 -0
  299. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/visibility/enforcement.py +0 -0
  300. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/visibility/model.py +0 -0
  301. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/visibility/persistence.py +0 -0
  302. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp/visibility/resolver.py +0 -0
  303. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  304. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  305. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  306. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  307. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  308. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/tests/__init__.py +0 -0
  309. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/tests/test_constants.py +0 -0
  310. {ha_mcp_dev-8.4.3.dev2581 → ha_mcp_dev-8.4.3.dev2587}/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.4.3.dev2581
3
+ Version: 8.4.3.dev2587
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.4.3.dev2581"
7
+ version = "8.4.3.dev2587"
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
@@ -100,6 +100,12 @@ class Settings(BaseSettings):
100
100
  10, alias="HAMCP_INDIVIDUAL_FETCH_BATCH_SIZE"
101
101
  )
102
102
 
103
+ # Optional preflight bounds for recorder queries. Disabled by default to
104
+ # preserve the established ha_get_history request contract.
105
+ enable_history_query_guardrails: bool = Field(
106
+ False, alias="HAMCP_ENABLE_HISTORY_QUERY_GUARDRAILS"
107
+ )
108
+
103
109
  # Backup tool configuration
104
110
  backup_hint: str = Field("normal", alias="BACKUP_HINT")
105
111
 
@@ -894,6 +900,13 @@ ADVANCED_SETTINGS_FIELDS: tuple[AdvancedField, ...] = (
894
900
  True,
895
901
  ),
896
902
  # Operations.
903
+ AdvancedField(
904
+ "enable_history_query_guardrails",
905
+ "HAMCP_ENABLE_HISTORY_QUERY_GUARDRAILS",
906
+ bool,
907
+ "operations",
908
+ True,
909
+ ),
897
910
  AdvancedField(
898
911
  "ha_tool_concurrency", "HA_TOOL_CONCURRENCY", int, "operations", True
899
912
  ),
@@ -404,6 +404,8 @@
404
404
  "advanced.individual_config_timeout.help": "Timeout for each automation/script/scene config fetch. Range 1–600. Restart required.",
405
405
  "advanced.individual_fetch_batch_size.label": "Config fetch batch size",
406
406
  "advanced.individual_fetch_batch_size.help": "Concurrent per-ID config fetches. Lower toward 1 on serial HA servers. Range 1–100. Restart required.",
407
+ "advanced.enable_history_query_guardrails.label": "History query guardrails",
408
+ "advanced.enable_history_query_guardrails.help": "Reject recorder queries that exceed conservative entity and time-range budgets. Disabled by default.",
407
409
  "advanced.ha_tool_concurrency.label": "Home Assistant tool concurrency",
408
410
  "advanced.ha_tool_concurrency.help": "Optional limit on outer Home Assistant tool calls across all sessions. Internal REST, WebSocket, and per-tool fan-out concurrency is unchanged. 0 keeps unlimited behavior. A call waiting for capacity fails after 60 seconds. Range 0–32. Restart required.",
409
411
  "advanced.backup_hint.label": "Backup-hint level",
@@ -3875,6 +3875,7 @@ const ADVANCED_FIELD_META = {
3875
3875
  scene_config_time_budget: { label: "Scene config time budget (s)", help: "Max seconds deep search spends fetching scene configs before returning a partial result. Range 1–600. Restart required." },
3876
3876
  individual_config_timeout: { label: "Per-request config fetch timeout (s)", help: "Timeout for each individual automation/script/scene config fetch during deep search. On HA servers that serve config reads serially, raise this and/or lower the batch size so queued requests don't time out. Values above the HA request timeout (HA_TIMEOUT, default 30) have no extra effect — the HTTP client gives up first. Range 1–600. Restart required." },
3877
3877
  individual_fetch_batch_size: { label: "Config fetch batch size", help: "How many per-id config fetches deep search issues concurrently. Lower toward 1 on HA servers that serve config reads serially (symptom: 'timed out' partial-result warnings). Range 1–100. Restart required." },
3878
+ enable_history_query_guardrails: { label: "History query guardrails", help: "Reject recorder queries that exceed conservative entity and time-range budgets. Disabled by default." },
3878
3879
  backup_hint: { label: "Backup-hint level", help: "Tunes how strongly the LLM is prompted to take a full-HA snapshot before risky writes." },
3879
3880
  dashboard_screenshot_engine_url: { label: "Dashboard screenshot engine URL", help: "Base URL of the screenshot engine (e.g. http://puppet:10000). Leave blank to auto-discover the Puppet App (add-on) via the Supervisor (HA OS / Supervised). Only used when the Dashboard Screenshot beta feature is enabled. Takes effect without a restart." },
3880
3881
  enable_websocket: { label: "Enable WebSocket", help: "WebSocket-based state monitoring. Disabling falls back to polling; many tools degrade. Restart required." },
@@ -10,8 +10,9 @@ ha_get_history -- Retrieve historical data with source-selectable mode:
10
10
  """
11
11
 
12
12
  import logging
13
+ import math
13
14
  import re
14
- from datetime import UTC, datetime, timedelta
15
+ from datetime import UTC, datetime, timedelta, tzinfo
15
16
  from typing import Annotated, Any, Literal
16
17
 
17
18
  from fastmcp import Context
@@ -19,6 +20,7 @@ from fastmcp.exceptions import ToolError
19
20
  from fastmcp.tools import tool
20
21
  from pydantic import Field
21
22
 
23
+ from ..config import get_global_settings
22
24
  from ..errors import ErrorCode, create_error_response, create_validation_error
23
25
  from .helpers import (
24
26
  exception_to_structured_error,
@@ -32,13 +34,22 @@ from .util_helpers import (
32
34
  JSON_STRING_COERCION,
33
35
  add_timezone_metadata,
34
36
  build_pagination_metadata,
37
+ fetch_ha_timezone,
35
38
  is_connection_error_message,
36
39
  parse_string_list_param,
37
40
  project_fields,
41
+ resolve_local_timezone,
38
42
  )
39
43
 
40
44
  logger = logging.getLogger(__name__)
41
45
 
46
+ _RELATIVE_TIME_UNIT_SECONDS = {
47
+ "h": 60 * 60,
48
+ "d": 24 * 60 * 60,
49
+ "w": 7 * 24 * 60 * 60,
50
+ "m": 30 * 24 * 60 * 60,
51
+ }
52
+
42
53
 
43
54
  def _convert_timestamp(value: Any) -> str | None:
44
55
  """Convert a timestamp value to ISO format string.
@@ -61,19 +72,27 @@ def _convert_timestamp(value: Any) -> str | None:
61
72
  return None
62
73
 
63
74
 
64
- def parse_relative_time(time_str: str | None, default_hours: int = 24) -> datetime:
75
+ def parse_relative_time(
76
+ time_str: str | None,
77
+ default_hours: int = 24,
78
+ *,
79
+ reference_time: datetime | None = None,
80
+ ) -> datetime:
65
81
  """
66
82
  Parse a time string that can be either ISO format or relative (e.g., '24h', '7d').
67
83
 
68
84
  Args:
69
85
  time_str: Time string in ISO format or relative format (e.g., "24h", "7d", "2w", "1m" where 1m = 30 days)
70
86
  default_hours: Default hours to go back if time_str is None
87
+ reference_time: Reference datetime for relative values and defaults.
71
88
 
72
89
  Returns:
73
- datetime object in UTC
90
+ A datetime with the parsed value's timezone, or ``reference_time``'s
91
+ timezone for relative values.
74
92
  """
93
+ now = reference_time or datetime.now(UTC)
75
94
  if time_str is None:
76
- return datetime.now(UTC) - timedelta(hours=default_hours)
95
+ return now - timedelta(hours=default_hours)
77
96
 
78
97
  # Check for relative time format
79
98
  relative_pattern = r"^(\d+)([hdwm])$"
@@ -82,16 +101,12 @@ def parse_relative_time(time_str: str | None, default_hours: int = 24) -> dateti
82
101
  if match:
83
102
  value = int(match.group(1))
84
103
  unit = match.group(2)
85
-
86
- if unit == "h":
87
- return datetime.now(UTC) - timedelta(hours=value)
88
- elif unit == "d":
89
- return datetime.now(UTC) - timedelta(days=value)
90
- elif unit == "w":
91
- return datetime.now(UTC) - timedelta(weeks=value)
92
- elif unit == "m":
93
- # Approximate month as 30 days
94
- return datetime.now(UTC) - timedelta(days=value * 30)
104
+ try:
105
+ return now - timedelta(seconds=value * _RELATIVE_TIME_UNIT_SECONDS[unit])
106
+ except OverflowError as exc:
107
+ raise ValueError(
108
+ f"Invalid time format: {time_str} is out of range"
109
+ ) from exc
95
110
 
96
111
  # Try parsing as ISO format
97
112
  try:
@@ -103,7 +118,7 @@ def parse_relative_time(time_str: str | None, default_hours: int = 24) -> dateti
103
118
  if dt.tzinfo is None:
104
119
  dt = dt.replace(tzinfo=UTC)
105
120
  return dt
106
- except ValueError as e:
121
+ except (ValueError, OverflowError) as e:
107
122
  raise ValueError(
108
123
  f"Invalid time format: {time_str}. Use ISO format or relative (e.g., '24h', '7d', '2w', '1m')"
109
124
  ) from e
@@ -116,6 +131,74 @@ _DEFAULT_START_HOURS_BY_SOURCE: dict[str, int] = {"history": 24, "statistics": 3
116
131
  _DEFAULT_HISTORY_LIMIT = 100
117
132
  _MAX_HISTORY_LIMIT = 1000
118
133
 
134
+ # Home Assistant's recorder WebSocket APIs have no server-side row limit. These
135
+ # estimated safety budgets bound recorder scan work; they are not measured HA
136
+ # limits and deliberately favor ordinary default-window requests.
137
+ _MAX_HISTORY_ENTITIES = 10
138
+ _MAX_HISTORY_ENTITY_HOURS = 240.0
139
+ _MAX_STATISTICS_ENTITIES = 25
140
+ _MAX_SHORT_TERM_STATISTICS_ROWS = 10_000
141
+ _MAX_LONG_TERM_STATISTICS_ROWS = 20_000
142
+ _CALENDAR_STATISTICS_PERIODS = frozenset({"day", "week", "month", "year"})
143
+ _VALID_STATISTICS_PERIODS = frozenset(
144
+ {"5minute", "hour", *_CALENDAR_STATISTICS_PERIODS}
145
+ )
146
+
147
+
148
+ async def _get_statistics_timezone(client: Any) -> tzinfo:
149
+ """Resolve HA's timezone without accepting an unsafe UTC fallback."""
150
+ timezone_name, fetch_failed = await fetch_ha_timezone(client)
151
+ if fetch_failed:
152
+ raise_tool_error(
153
+ create_error_response(
154
+ ErrorCode.CONNECTION_FAILED,
155
+ "Could not fetch the Home Assistant timezone required for safe calendar statistics estimation",
156
+ suggestions=[
157
+ "Check the Home Assistant connection, then retry.",
158
+ "Use period='hour' or period='5minute', which do not require calendar alignment.",
159
+ ],
160
+ )
161
+ )
162
+ timezone, resolved_timezone_name = resolve_local_timezone(timezone_name)
163
+ if resolved_timezone_name != timezone_name:
164
+ raise_tool_error(
165
+ create_error_response(
166
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
167
+ "Home Assistant reports a timezone that could not be resolved for safe calendar statistics estimation",
168
+ context={"home_assistant_timezone": timezone_name},
169
+ suggestions=[
170
+ "Configure a valid Home Assistant timezone, then retry.",
171
+ "Use period='hour' or period='5minute', which do not require calendar alignment.",
172
+ ],
173
+ )
174
+ )
175
+ return timezone
176
+
177
+
178
+ async def _prepare_guardrail_query(
179
+ client: Any,
180
+ *,
181
+ enabled: bool,
182
+ source: str,
183
+ period: str,
184
+ start_dt: datetime,
185
+ end_dt: datetime,
186
+ end_time_was_provided: bool,
187
+ ) -> tzinfo:
188
+ """Validate guarded ranges and resolve calendar statistics timezone."""
189
+ _validate_time_range(
190
+ start_dt,
191
+ end_dt,
192
+ end_time_was_provided=end_time_was_provided,
193
+ reject_zero_length=enabled,
194
+ )
195
+ if not enabled:
196
+ return UTC
197
+ if source != "statistics" or period not in _CALENDAR_STATISTICS_PERIODS:
198
+ return UTC
199
+ _validate_calendar_statistics_range(start_dt, end_dt, period)
200
+ return await _get_statistics_timezone(client)
201
+
119
202
 
120
203
  class HistoryTools:
121
204
  """Historical data access tools for Home Assistant."""
@@ -243,7 +326,7 @@ class HistoryTools:
243
326
  ctx: Context | None = None,
244
327
  ) -> dict[str, Any]:
245
328
  """
246
- Retrieve historical data from Home Assistant's recorder.
329
+ Get historical data from Home Assistant's recorder.
247
330
 
248
331
  **Sources:**
249
332
  - "history" (default): Raw state changes, ~10 day retention, full resolution
@@ -269,11 +352,16 @@ class HistoryTools:
269
352
  All data is fetched from HA before slicing; limit/offset are client-side.
270
353
  With multiple entity_ids, offset must be 0 — use a single entity_id for offset > 0.
271
354
  Use has_more and next_offset from the response to paginate.
355
+ Administrators can optionally enable recorder workload guardrails in Advanced
356
+ settings. When enabled, oversized entity/time workloads are rejected before the
357
+ recorder query is issued; narrow the time range or entity list to stay within
358
+ the budget. Calendar statistics may first read HA's configured timezone so the
359
+ estimate follows local calendar boundaries.
272
360
 
273
361
  **Example -- history (default):**
274
362
  ```python
275
363
  ha_get_history(entity_ids="sensor.bedroom_temperature", start_time="24h")
276
- ha_get_history(entity_ids=["sensor.temperature", "sensor.humidity"], start_time="7d", limit=500)
364
+ ha_get_history(entity_ids=["sensor.temperature", "sensor.humidity"], start_time="3d", limit=500)
277
365
  # Default order="desc" returns newest states first.
278
366
  # To paginate oldest-first, use order="asc":
279
367
  ha_get_history(entity_ids="sensor.temperature", start_time="7d", limit=100, offset=100, order="asc")
@@ -321,6 +409,27 @@ class HistoryTools:
321
409
 
322
410
  # Parse time parameters
323
411
  start_dt, end_dt = _parse_time_range(start_time, end_time, default_hours)
412
+ query_settings = get_global_settings()
413
+ statistics_timezone = await _prepare_guardrail_query(
414
+ self._client,
415
+ enabled=query_settings.enable_history_query_guardrails,
416
+ source=source,
417
+ period=period,
418
+ start_dt=start_dt,
419
+ end_dt=end_dt,
420
+ end_time_was_provided=end_time is not None,
421
+ )
422
+ _validate_query_workload(
423
+ source=source,
424
+ entity_ids=entity_id_list,
425
+ start_dt=start_dt,
426
+ end_dt=end_dt,
427
+ minimal_response=minimal_response,
428
+ significant_changes_only=significant_changes_only,
429
+ period=period,
430
+ enforce_budget=query_settings.enable_history_query_guardrails,
431
+ statistics_timezone=statistics_timezone,
432
+ )
324
433
 
325
434
  await safe_info(
326
435
  ctx,
@@ -459,9 +568,14 @@ def _parse_time_range(
459
568
  default_hours: int,
460
569
  ) -> tuple[datetime, datetime]:
461
570
  """Parse start_time and end_time into datetime objects."""
571
+ reference_time = datetime.now(UTC)
462
572
  try:
463
- start_dt = parse_relative_time(start_time, default_hours=default_hours)
464
- except ValueError as e:
573
+ start_dt = parse_relative_time(
574
+ start_time,
575
+ default_hours=default_hours,
576
+ reference_time=reference_time,
577
+ )
578
+ except (ValueError, OverflowError) as e:
465
579
  raise_tool_error(
466
580
  create_error_response(
467
581
  ErrorCode.VALIDATION_INVALID_PARAMETER,
@@ -476,8 +590,12 @@ def _parse_time_range(
476
590
 
477
591
  if end_time:
478
592
  try:
479
- end_dt = parse_relative_time(end_time, default_hours=0)
480
- except ValueError as e:
593
+ end_dt = parse_relative_time(
594
+ end_time,
595
+ default_hours=0,
596
+ reference_time=reference_time,
597
+ )
598
+ except (ValueError, OverflowError) as e:
481
599
  raise_tool_error(
482
600
  create_error_response(
483
601
  ErrorCode.VALIDATION_INVALID_PARAMETER,
@@ -487,11 +605,277 @@ def _parse_time_range(
487
605
  )
488
606
  )
489
607
  else:
490
- end_dt = datetime.now(UTC)
608
+ end_dt = reference_time
491
609
 
492
610
  return start_dt, end_dt
493
611
 
494
612
 
613
+ def _validate_time_range(
614
+ start_dt: datetime,
615
+ end_dt: datetime,
616
+ *,
617
+ end_time_was_provided: bool = True,
618
+ reject_zero_length: bool = True,
619
+ ) -> None:
620
+ """Reject reversed ranges and, when requested, zero-length ranges."""
621
+ if end_dt < start_dt or (reject_zero_length and end_dt == start_dt):
622
+ if not end_time_was_provided and start_dt > end_dt:
623
+ message = "start_time must not be in the future when end_time is omitted"
624
+ suggestions = ["Choose a start_time at or before the current time."]
625
+ else:
626
+ message = "end_time must be later than start_time"
627
+ suggestions = ["Choose an end_time later than start_time."]
628
+ raise_tool_error(
629
+ create_error_response(
630
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
631
+ message,
632
+ context={
633
+ "start_time": start_dt.isoformat(),
634
+ "end_time": end_dt.isoformat(),
635
+ },
636
+ suggestions=suggestions,
637
+ )
638
+ )
639
+
640
+
641
+ def _validate_calendar_statistics_range(
642
+ start_dt: datetime, end_dt: datetime, period: str
643
+ ) -> None:
644
+ """Reject boundary years that calendar alignment cannot advance safely."""
645
+ if start_dt.year <= datetime.min.year or end_dt.year >= datetime.max.year:
646
+ raise_tool_error(
647
+ create_error_response(
648
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
649
+ f"Time range is outside the safe calendar alignment bounds for period='{period}'",
650
+ context={
651
+ "start_time": start_dt.isoformat(),
652
+ "end_time": end_dt.isoformat(),
653
+ "period": period,
654
+ },
655
+ suggestions=[
656
+ "Choose start_time and end_time between years 2 and 9998."
657
+ ],
658
+ )
659
+ )
660
+
661
+
662
+ def _validate_query_workload(
663
+ *,
664
+ source: str,
665
+ entity_ids: list[str],
666
+ start_dt: datetime,
667
+ end_dt: datetime,
668
+ minimal_response: bool,
669
+ significant_changes_only: bool,
670
+ period: str,
671
+ enforce_budget: bool,
672
+ statistics_timezone: tzinfo = UTC,
673
+ ) -> None:
674
+ """Reject recorder requests likely to monopolize Home Assistant resources."""
675
+ if not enforce_budget:
676
+ return
677
+
678
+ _validate_time_range(start_dt, end_dt)
679
+ if source == "history":
680
+ violation = _history_workload_violation(
681
+ entity_ids,
682
+ start_dt,
683
+ end_dt,
684
+ minimal_response,
685
+ significant_changes_only,
686
+ )
687
+ else:
688
+ violation = _statistics_workload_violation(
689
+ entity_ids, start_dt, end_dt, period, statistics_timezone
690
+ )
691
+
692
+ if violation is None:
693
+ return
694
+ context, suggestions = violation
695
+
696
+ suggestions.append(
697
+ "An administrator can disable history query guardrails in Advanced settings after evaluating the workload risk."
698
+ )
699
+ raise_tool_error(
700
+ create_error_response(
701
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
702
+ "Recorder query exceeds the safe workload budget",
703
+ context=context,
704
+ suggestions=suggestions,
705
+ )
706
+ )
707
+
708
+
709
+ def _history_workload_violation(
710
+ entity_ids: list[str],
711
+ start_dt: datetime,
712
+ end_dt: datetime,
713
+ minimal_response: bool,
714
+ significant_changes_only: bool,
715
+ ) -> tuple[dict[str, Any], list[str]] | None:
716
+ """Return raw-history violation details, or None when within budget."""
717
+ # Estimated multipliers account for the larger recorder payload when
718
+ # filtering and attribute minimization are disabled; they are not measured
719
+ # row counts.
720
+ detail_weight = 1
721
+ if not significant_changes_only:
722
+ detail_weight *= 4
723
+ if not minimal_response:
724
+ detail_weight *= 2
725
+ entity_count = len(entity_ids)
726
+ estimated_entity_hours = (
727
+ (end_dt - start_dt).total_seconds() / 3600 * entity_count * detail_weight
728
+ )
729
+ if (
730
+ entity_count <= _MAX_HISTORY_ENTITIES
731
+ and estimated_entity_hours <= _MAX_HISTORY_ENTITY_HOURS
732
+ ):
733
+ return None
734
+ context = {
735
+ "entity_count": entity_count,
736
+ "max_entities": _MAX_HISTORY_ENTITIES,
737
+ "estimated_entity_hours": round(estimated_entity_hours, 2),
738
+ "max_entity_hours": _MAX_HISTORY_ENTITY_HOURS,
739
+ "detail_weight": detail_weight,
740
+ "minimal_response": minimal_response,
741
+ "significant_changes_only": significant_changes_only,
742
+ }
743
+ suggestions = ["Query fewer entities or use a shorter time range."]
744
+ if not significant_changes_only:
745
+ suggestions.append("Set significant_changes_only=true to reduce recorder work.")
746
+ if not minimal_response:
747
+ suggestions.append("Set minimal_response=true to omit full state attributes.")
748
+ suggestions.append(
749
+ "Use source='statistics' for long ranges when the entities support long-term statistics."
750
+ )
751
+ return context, suggestions
752
+
753
+
754
+ def _statistics_workload_violation(
755
+ entity_ids: list[str],
756
+ start_dt: datetime,
757
+ end_dt: datetime,
758
+ period: str,
759
+ statistics_timezone: tzinfo,
760
+ ) -> tuple[dict[str, Any], list[str]] | None:
761
+ """Return statistics violation details, or None when within budget."""
762
+ if period not in _VALID_STATISTICS_PERIODS:
763
+ raise_tool_error(
764
+ create_error_response(
765
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
766
+ f"Invalid statistics period: {period}",
767
+ context={"period": period},
768
+ suggestions=[
769
+ "Use one of: '5minute', 'hour', 'day', 'week', 'month', 'year'."
770
+ ],
771
+ )
772
+ )
773
+ scan_start, scan_end = _statistics_scan_window(
774
+ start_dt, end_dt, period, statistics_timezone
775
+ )
776
+ scan_granularity_seconds = 300 if period == "5minute" else 3600
777
+ max_statistics_rows = (
778
+ _MAX_SHORT_TERM_STATISTICS_ROWS
779
+ if period == "5minute"
780
+ else _MAX_LONG_TERM_STATISTICS_ROWS
781
+ )
782
+ entity_count = len(entity_ids)
783
+ estimated_rows = (
784
+ math.ceil((scan_end - scan_start).total_seconds() / scan_granularity_seconds)
785
+ * entity_count
786
+ )
787
+ if (
788
+ entity_count <= _MAX_STATISTICS_ENTITIES
789
+ and estimated_rows <= max_statistics_rows
790
+ ):
791
+ return None
792
+ context = {
793
+ "entity_count": entity_count,
794
+ "max_entities": _MAX_STATISTICS_ENTITIES,
795
+ "estimated_rows": estimated_rows,
796
+ "max_estimated_rows": max_statistics_rows,
797
+ "period": period,
798
+ "scan_granularity_minutes": scan_granularity_seconds // 60,
799
+ "scan_start_time": scan_start.isoformat(),
800
+ "scan_end_time": scan_end.isoformat(),
801
+ }
802
+ suggestions = ["Query fewer entities or use a shorter time range."]
803
+ if period == "5minute":
804
+ suggestions.append("Use period='hour' to scan the long-term statistics table.")
805
+ return context, suggestions
806
+
807
+
808
+ def _statistics_scan_window(
809
+ start_dt: datetime,
810
+ end_dt: datetime,
811
+ period: str,
812
+ local_timezone: tzinfo,
813
+ ) -> tuple[datetime, datetime]:
814
+ """Mirror Core's calendar alignment before its statistics table query."""
815
+ if period not in _CALENDAR_STATISTICS_PERIODS:
816
+ return start_dt, end_dt
817
+
818
+ local_start = start_dt.astimezone(local_timezone)
819
+ local_end = end_dt.astimezone(local_timezone)
820
+
821
+ if period == "day":
822
+ scan_start = local_start.replace(hour=0, minute=0, second=0, microsecond=0)
823
+ scan_end = local_end.replace(
824
+ hour=0, minute=0, second=0, microsecond=0
825
+ ) + timedelta(days=1)
826
+ elif period == "week":
827
+ scan_start = local_start.replace(
828
+ hour=0, minute=0, second=0, microsecond=0
829
+ ) - timedelta(days=local_start.weekday())
830
+ scan_end = (
831
+ local_end.replace(hour=0, minute=0, second=0, microsecond=0)
832
+ - timedelta(days=local_end.weekday())
833
+ + timedelta(days=7)
834
+ )
835
+ elif period == "month":
836
+ scan_start = local_start.replace(
837
+ day=1, hour=0, minute=0, second=0, microsecond=0
838
+ )
839
+ scan_end = _next_month_start(local_end)
840
+ else:
841
+ scan_start = local_start.replace(
842
+ month=1, day=1, hour=0, minute=0, second=0, microsecond=0
843
+ )
844
+ scan_end = local_end.replace(
845
+ year=local_end.year + 1,
846
+ month=1,
847
+ day=1,
848
+ hour=0,
849
+ minute=0,
850
+ second=0,
851
+ microsecond=0,
852
+ )
853
+
854
+ return scan_start.astimezone(UTC), scan_end.astimezone(UTC)
855
+
856
+
857
+ def _next_month_start(value: datetime) -> datetime:
858
+ """Return local midnight on the first day of the following month."""
859
+ if value.month == 12:
860
+ return value.replace(
861
+ year=value.year + 1,
862
+ month=1,
863
+ day=1,
864
+ hour=0,
865
+ minute=0,
866
+ second=0,
867
+ microsecond=0,
868
+ )
869
+ return value.replace(
870
+ month=value.month + 1,
871
+ day=1,
872
+ hour=0,
873
+ minute=0,
874
+ second=0,
875
+ microsecond=0,
876
+ )
877
+
878
+
495
879
  def _raise_recorder_ws_failure(
496
880
  kind: str,
497
881
  error_msg: str,
@@ -712,7 +712,7 @@ _TIMESTAMP_METADATA_FIELDS = {
712
712
  }
713
713
 
714
714
 
715
- async def _fetch_ha_timezone(client: Any) -> tuple[str, bool]:
715
+ async def fetch_ha_timezone(client: Any) -> tuple[str, bool]:
716
716
  """Fetch the HA timezone, preferring the ``ha_mcp_tools`` component's cached
717
717
  ``info`` handshake over a fresh ``/api/config`` REST call.
718
718
 
@@ -757,7 +757,7 @@ async def _fetch_ha_timezone(client: Any) -> tuple[str, bool]:
757
757
  return "UTC", True
758
758
 
759
759
 
760
- def _resolve_local_timezone(ha_timezone: str) -> tuple[_TZInfo, str]:
760
+ def resolve_local_timezone(ha_timezone: str) -> tuple[_TZInfo, str]:
761
761
  """Resolve *ha_timezone* to a ``ZoneInfo``, falling back to UTC if unknown.
762
762
 
763
763
  Returns ``(local_tz, ha_timezone)``. ``ha_timezone`` is normalized to
@@ -805,7 +805,7 @@ async def add_timezone_metadata(
805
805
  ) -> dict[str, Any]:
806
806
  """Add Home Assistant timezone to tool responses and convert timestamps to local time.
807
807
 
808
- Resolves the Home Assistant time zone via ``_fetch_ha_timezone`` (which
808
+ Resolves the Home Assistant time zone via ``fetch_ha_timezone`` (which
809
809
  prefers the ``ha_mcp_tools`` component's cached handshake and falls back to
810
810
  ``/api/config``), converts every ``last_changed``, ``last_updated``,
811
811
  ``last_reported``, ``when``, and ``last_triggered`` field found anywhere in
@@ -824,7 +824,7 @@ async def add_timezone_metadata(
824
824
  if not include_metadata:
825
825
  return data
826
826
 
827
- ha_timezone, fetch_failed = await _fetch_ha_timezone(client)
827
+ ha_timezone, fetch_failed = await fetch_ha_timezone(client)
828
828
 
829
829
  if fetch_failed:
830
830
  return {
@@ -836,7 +836,7 @@ async def add_timezone_metadata(
836
836
  },
837
837
  }
838
838
 
839
- local_tz, ha_timezone = _resolve_local_timezone(ha_timezone)
839
+ local_tz, ha_timezone = resolve_local_timezone(ha_timezone)
840
840
  converted_data = _convert_timestamp_fields(data, local_tz)
841
841
 
842
842
  return {
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 8.4.3.dev2581
3
+ Version: 8.4.3.dev2587
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