ha-mcp-dev 8.3.0.dev2374__tar.gz → 8.3.0.dev2379__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 (276) hide show
  1. {ha_mcp_dev-8.3.0.dev2374/src/ha_mcp_dev.egg-info → ha_mcp_dev-8.3.0.dev2379}/PKG-INFO +1 -1
  2. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/pyproject.toml +1 -1
  3. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/device_control.py +168 -13
  4. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/smart_search/_base.py +8 -1
  5. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/smart_search/_entities.py +220 -40
  6. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_search.py +15 -3
  7. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_service.py +109 -8
  8. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/utils/operation_manager.py +3 -3
  9. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/visibility/enforcement.py +22 -1
  10. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  11. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/LICENSE +0 -0
  12. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/MANIFEST.in +0 -0
  13. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/README.md +0 -0
  14. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/setup.cfg +0 -0
  15. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/__init__.py +0 -0
  16. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/__main__.py +0 -0
  17. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_pypi_marker +0 -0
  18. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_rich_compat.py +0 -0
  19. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/__init__.py +0 -0
  20. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/requirements.txt +0 -0
  21. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/LICENSE +0 -0
  22. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/MANIFEST.sha256 +0 -0
  23. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/VENDORED +0 -0
  24. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/__init__.py +0 -0
  25. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/__main__.py +0 -0
  26. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/asyncio/__init__.py +0 -0
  27. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/asyncio/client.py +0 -0
  28. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/asyncio/connection.py +0 -0
  29. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/asyncio/messages.py +0 -0
  30. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/asyncio/router.py +0 -0
  31. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/asyncio/server.py +0 -0
  32. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/auth.py +0 -0
  33. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/cli.py +0 -0
  34. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/client.py +0 -0
  35. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/connection.py +0 -0
  36. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/datastructures.py +0 -0
  37. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/exceptions.py +0 -0
  38. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/extensions/__init__.py +0 -0
  39. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/extensions/base.py +0 -0
  40. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/extensions/permessage_deflate.py +0 -0
  41. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/frames.py +0 -0
  42. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/headers.py +0 -0
  43. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/http11.py +0 -0
  44. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/imports.py +0 -0
  45. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/legacy/__init__.py +0 -0
  46. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/legacy/auth.py +0 -0
  47. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/legacy/client.py +0 -0
  48. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/legacy/exceptions.py +0 -0
  49. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/legacy/framing.py +0 -0
  50. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/legacy/handshake.py +0 -0
  51. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/legacy/http.py +0 -0
  52. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/legacy/protocol.py +0 -0
  53. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/legacy/server.py +0 -0
  54. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/protocol.py +0 -0
  55. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/proxy.py +0 -0
  56. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/py.typed +0 -0
  57. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/server.py +0 -0
  58. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/streams.py +0 -0
  59. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/sync/__init__.py +0 -0
  60. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/sync/client.py +0 -0
  61. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/sync/connection.py +0 -0
  62. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/sync/messages.py +0 -0
  63. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/sync/router.py +0 -0
  64. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/sync/server.py +0 -0
  65. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/sync/utils.py +0 -0
  66. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/trio/__init__.py +0 -0
  67. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/trio/client.py +0 -0
  68. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/trio/connection.py +0 -0
  69. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/trio/messages.py +0 -0
  70. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/trio/router.py +0 -0
  71. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/trio/server.py +0 -0
  72. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/trio/utils.py +0 -0
  73. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/typing.py +0 -0
  74. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/uri.py +0 -0
  75. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/utils.py +0 -0
  76. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_vendor/websockets/version.py +0 -0
  77. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/_version.py +0 -0
  78. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/auth/__init__.py +0 -0
  79. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/auth/consent_form.py +0 -0
  80. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/auth/oidc_compat.py +0 -0
  81. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/auth/provider.py +0 -0
  82. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/backup_manager.py +0 -0
  83. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/browser_landing.py +0 -0
  84. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/client/__init__.py +0 -0
  85. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/client/rest_client.py +0 -0
  86. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/client/supervisor_client.py +0 -0
  87. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/client/websocket_client.py +0 -0
  88. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/client/websocket_listener.py +0 -0
  89. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/config.py +0 -0
  90. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  91. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  92. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
  93. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
  94. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  95. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/dashboard_screenshot/theme_guard.py +0 -0
  96. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/errors.py +0 -0
  97. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/hacs_auto_refresh.py +0 -0
  98. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/llm_exposure.py +0 -0
  99. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/policy/__init__.py +0 -0
  100. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/policy/approval_queue.py +0 -0
  101. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/policy/editing.py +0 -0
  102. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/policy/evaluator.py +0 -0
  103. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/policy/handlers.py +0 -0
  104. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/policy/middleware.py +0 -0
  105. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/policy/model.py +0 -0
  106. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/policy/persistence.py +0 -0
  107. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/policy/value_sources.py +0 -0
  108. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/py.typed +0 -0
  109. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/read_only.py +0 -0
  110. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/redaction.py +0 -0
  111. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/renamed_tools.py +0 -0
  112. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  113. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  114. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  115. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  116. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  117. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  118. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  119. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  120. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  121. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  122. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  123. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  124. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
  125. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  126. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
  127. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  128. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  129. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  130. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  131. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  132. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  133. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  134. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
  135. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  136. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  137. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/server.py +0 -0
  138. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/__init__.py +0 -0
  139. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
  140. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
  141. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
  142. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
  143. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
  144. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
  145. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/_i18n.py +0 -0
  146. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/_persistence.py +0 -0
  147. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
  148. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/_theme.py +0 -0
  149. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
  150. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/locales/README.md +0 -0
  151. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/locales/cs.json +0 -0
  152. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/locales/de.json +0 -0
  153. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/locales/en.json +0 -0
  154. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/locales/eo.json +0 -0
  155. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/locales/es.json +0 -0
  156. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/locales/fr.json +0 -0
  157. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/locales/it.json +0 -0
  158. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/locales/nl.json +0 -0
  159. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/locales/pl.json +0 -0
  160. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/locales/ru.json +0 -0
  161. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/locales/sv.json +0 -0
  162. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/locales/zh-Hans.json +0 -0
  163. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/settings.css +0 -0
  164. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/settings.html +0 -0
  165. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/settings_ui/settings.js +0 -0
  166. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/smoke_test.py +0 -0
  167. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  168. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/strict_bps.py +0 -0
  169. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/__init__.py +0 -0
  170. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/auto_backup.py +0 -0
  171. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/backup.py +0 -0
  172. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  173. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/best_practice_result.py +0 -0
  174. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/best_practice_variables.py +0 -0
  175. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/component_api.py +0 -0
  176. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/component_config_entries.py +0 -0
  177. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/component_config_reads.py +0 -0
  178. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/component_devices.py +0 -0
  179. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/component_registries.py +0 -0
  180. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/component_registry_lookup.py +0 -0
  181. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/config_entry_flow.py +0 -0
  182. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/config_entry_flow_form.py +0 -0
  183. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/config_entry_flow_menu.py +0 -0
  184. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/config_entry_flow_walker.py +0 -0
  185. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/config_entry_identity.py +0 -0
  186. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/config_entry_reconfigure.py +0 -0
  187. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/config_entry_reload_watch.py +0 -0
  188. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/error_log_parsing.py +0 -0
  189. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/hacs_registration.py +0 -0
  190. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/helpers.py +0 -0
  191. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/integration_reconfigure.py +0 -0
  192. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/radio/__init__.py +0 -0
  193. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/radio/base.py +0 -0
  194. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/radio/matter.py +0 -0
  195. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/radio/thread.py +0 -0
  196. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/radio/zigbee.py +0 -0
  197. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/radio/zwave.py +0 -0
  198. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/reference_validator.py +0 -0
  199. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/registry.py +0 -0
  200. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/renamed_tool_middleware.py +0 -0
  201. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  202. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  203. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
  204. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  205. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  206. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  207. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  208. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
  209. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_addons.py +0 -0
  210. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_areas.py +0 -0
  211. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_assist_pipeline.py +0 -0
  212. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  213. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  214. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_calendar.py +0 -0
  215. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_camera.py +0 -0
  216. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_categories.py +0 -0
  217. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_code.py +0 -0
  218. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  219. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  220. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  221. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  222. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  223. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  224. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_dev.py +0 -0
  225. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_energy.py +0 -0
  226. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_entities.py +0 -0
  227. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  228. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_groups.py +0 -0
  229. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_hacs.py +0 -0
  230. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_history.py +0 -0
  231. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_integrations.py +0 -0
  232. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_labels.py +0 -0
  233. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_radio.py +0 -0
  234. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_registry.py +0 -0
  235. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_resources.py +0 -0
  236. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_security_policy.py +0 -0
  237. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_services.py +0 -0
  238. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_system.py +0 -0
  239. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_themes.py +0 -0
  240. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_todo.py +0 -0
  241. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_traces.py +0 -0
  242. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_updates.py +0 -0
  243. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_utility.py +0 -0
  244. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  245. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  246. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
  247. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/tools_zones.py +0 -0
  248. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/util_helpers.py +0 -0
  249. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/tools/validation_middleware.py +0 -0
  250. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/transforms/__init__.py +0 -0
  251. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/transforms/categorized_search.py +0 -0
  252. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  253. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/transport_security.py +0 -0
  254. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/update_check.py +0 -0
  255. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/utils/__init__.py +0 -0
  256. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/utils/config_hash.py +0 -0
  257. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/utils/config_write_lock.py +0 -0
  258. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/utils/data_paths.py +0 -0
  259. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/utils/domain_handlers.py +0 -0
  260. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  261. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  262. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/utils/python_sandbox.py +0 -0
  263. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/utils/skill_loader.py +0 -0
  264. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/utils/usage_logger.py +0 -0
  265. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/visibility/__init__.py +0 -0
  266. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/visibility/model.py +0 -0
  267. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/visibility/persistence.py +0 -0
  268. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp/visibility/resolver.py +0 -0
  269. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  270. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  271. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  272. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  273. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  274. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/tests/__init__.py +0 -0
  275. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/tests/test_constants.py +0 -0
  276. {ha_mcp_dev-8.3.0.dev2374 → ha_mcp_dev-8.3.0.dev2379}/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.dev2374
3
+ Version: 8.3.0.dev2379
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.dev2374"
7
+ version = "8.3.0.dev2379"
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 @@ and async operation verification through WebSocket monitoring.
8
8
  import asyncio
9
9
  import json
10
10
  import logging
11
+ import math
11
12
  import time
12
13
  from typing import Any, ClassVar
13
14
 
@@ -88,7 +89,7 @@ class DeviceControlTools:
88
89
  entity_id: str,
89
90
  action: str,
90
91
  parameters: dict[str, Any] | None = None,
91
- timeout_seconds: int = 10,
92
+ timeout_seconds: float = 10,
92
93
  validate_first: bool = True,
93
94
  ) -> dict[str, Any]:
94
95
  """
@@ -347,7 +348,13 @@ class DeviceControlTools:
347
348
  }
348
349
 
349
350
  _DOMAIN_PARAMS: ClassVar[dict[str, list[str]]] = {
350
- "light": ["brightness", "color_temp_kelvin", "rgb_color", "effect"],
351
+ "light": [
352
+ "brightness",
353
+ "brightness_pct",
354
+ "color_temp_kelvin",
355
+ "rgb_color",
356
+ "effect",
357
+ ],
351
358
  "climate": ["temperature", "target_temp_high", "target_temp_low", "hvac_mode"],
352
359
  "cover": ["position", "tilt_position"],
353
360
  "media_player": ["volume_level", "media_content_id", "media_content_type"],
@@ -462,7 +469,7 @@ class DeviceControlTools:
462
469
  return expected if expected else None
463
470
 
464
471
  async def get_device_operation_status(
465
- self, operation_id: str, timeout_seconds: int = 10
472
+ self, operation_id: str, timeout_seconds: float = 10
466
473
  ) -> dict[str, Any]:
467
474
  """Check status of a device operation, waiting up to ``timeout_seconds`` for completion.
468
475
 
@@ -495,9 +502,10 @@ class DeviceControlTools:
495
502
  if operation.status.value == "pending" and timeout_seconds > 0:
496
503
  deadline = time.monotonic() + timeout_seconds
497
504
  while operation.status.value == "pending":
498
- if time.monotonic() >= deadline:
505
+ remaining = deadline - time.monotonic()
506
+ if remaining <= 0:
499
507
  break
500
- await asyncio.sleep(0.2)
508
+ await asyncio.sleep(min(0.2, remaining))
501
509
  refreshed = get_operation_from_memory(operation_id)
502
510
  if refreshed is None:
503
511
  raise_tool_error(
@@ -597,8 +605,52 @@ class DeviceControlTools:
597
605
  }
598
606
 
599
607
  @staticmethod
608
+ def _normalize_bulk_parameters(
609
+ value: Any,
610
+ ) -> tuple[dict[str, Any] | None, ErrorCode | None]:
611
+ """Normalize optional bulk parameters and identify invalid values."""
612
+ if isinstance(value, str):
613
+ try:
614
+ value = json.loads(value)
615
+ except json.JSONDecodeError:
616
+ return None, ErrorCode.VALIDATION_INVALID_JSON
617
+ if value is not None and not isinstance(value, dict):
618
+ return None, ErrorCode.VALIDATION_INVALID_PARAMETER
619
+ return value, None
620
+
621
+ @staticmethod
622
+ def _normalize_bulk_timeout(value: Any) -> tuple[float | None, bool]:
623
+ """Normalize an optional non-negative finite timeout."""
624
+ if value is None:
625
+ return None, True
626
+ try:
627
+ timeout = float(value)
628
+ except (TypeError, ValueError):
629
+ return None, False
630
+ return (
631
+ (timeout, True)
632
+ if math.isfinite(timeout) and timeout >= 0
633
+ else (None, False)
634
+ )
635
+
636
+ @staticmethod
637
+ def _normalized_bulk_operation(
638
+ operation: dict[str, Any],
639
+ parameters: dict[str, Any] | None,
640
+ timeout: float | None,
641
+ ) -> dict[str, Any]:
642
+ """Return one normalized valid bulk operation."""
643
+ normalized = dict(operation)
644
+ if parameters is not None:
645
+ normalized["parameters"] = parameters
646
+ if timeout is not None:
647
+ normalized["timeout_seconds"] = timeout
648
+ return normalized
649
+
650
+ @classmethod
600
651
  def _validate_bulk_operations(
601
- operations: list[dict[str, Any]],
652
+ cls,
653
+ operations: list[Any],
602
654
  skipped_operations: list[dict[str, Any]],
603
655
  ) -> list[tuple[int, dict[str, Any], str, str]]:
604
656
  valid: list[tuple[int, dict[str, Any], str, str]] = []
@@ -614,6 +666,33 @@ class DeviceControlTools:
614
666
  skipped_operations.append(err_response)
615
667
  continue
616
668
 
669
+ allowed_keys = {
670
+ "entity_id",
671
+ "action",
672
+ "parameters",
673
+ "timeout_seconds",
674
+ "validate_first",
675
+ }
676
+ obsolete_keys = sorted(set(op) - allowed_keys)
677
+ if obsolete_keys:
678
+ key_list = ", ".join(obsolete_keys)
679
+ error = (
680
+ f"Operation at index {i} contains unsupported key(s): {key_list}"
681
+ )
682
+ logger.warning(f"Bulk control: {error}")
683
+ err_response = create_error_response(
684
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
685
+ error,
686
+ suggestions=[
687
+ "Use entity_id and action; put action data inside parameters"
688
+ ],
689
+ context={"index": i, "unsupported_keys": obsolete_keys},
690
+ )
691
+ err_response["index"] = i
692
+ err_response["operation"] = op
693
+ skipped_operations.append(err_response)
694
+ continue
695
+
617
696
  entity_id = op.get("entity_id")
618
697
  action = op.get("action")
619
698
  missing = [f for f in ("entity_id", "action") if not op.get(f)]
@@ -627,13 +706,85 @@ class DeviceControlTools:
627
706
  err_response["index"] = i
628
707
  err_response["operation"] = op
629
708
  skipped_operations.append(err_response)
630
- else:
631
- valid.append((i, op, str(entity_id), str(action)))
709
+ continue
710
+
711
+ if not isinstance(entity_id, str) or not isinstance(action, str):
712
+ error = f"Operation at index {i} requires string entity_id and action fields"
713
+ logger.warning(f"Bulk control: {error}")
714
+ err_response = create_error_response(
715
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
716
+ error,
717
+ context={"index": i},
718
+ )
719
+ err_response["index"] = i
720
+ err_response["operation"] = op
721
+ skipped_operations.append(err_response)
722
+ continue
723
+
724
+ parameters, parameter_error = cls._normalize_bulk_parameters(
725
+ op.get("parameters")
726
+ )
727
+ if parameter_error is not None:
728
+ error = (
729
+ f"Operation at index {i} has invalid JSON parameters"
730
+ if parameter_error == ErrorCode.VALIDATION_INVALID_JSON
731
+ else f"Operation at index {i} parameters must be a JSON object"
732
+ )
733
+ logger.warning(f"Bulk control: {error}")
734
+ err_response = create_error_response(
735
+ parameter_error,
736
+ error,
737
+ context={"index": i},
738
+ )
739
+ err_response["index"] = i
740
+ err_response["operation"] = op
741
+ skipped_operations.append(err_response)
742
+ continue
743
+
744
+ timeout = None
745
+ timeout_valid = "timeout_seconds" not in op
746
+ if not timeout_valid:
747
+ timeout, timeout_valid = cls._normalize_bulk_timeout(
748
+ op["timeout_seconds"]
749
+ )
750
+ timeout_valid = timeout_valid and timeout is not None
751
+ if not timeout_valid:
752
+ error = (
753
+ f"Operation at index {i} timeout_seconds must be a "
754
+ "non-negative number"
755
+ )
756
+ logger.warning(f"Bulk control: {error}")
757
+ err_response = create_error_response(
758
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
759
+ error,
760
+ context={"index": i},
761
+ )
762
+ err_response["index"] = i
763
+ err_response["operation"] = op
764
+ skipped_operations.append(err_response)
765
+ continue
766
+
767
+ if "validate_first" in op and type(op["validate_first"]) is not bool:
768
+ error = f"Operation at index {i} validate_first must be a boolean"
769
+ logger.warning(f"Bulk control: {error}")
770
+ err_response = create_error_response(
771
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
772
+ error,
773
+ context={"index": i},
774
+ )
775
+ err_response["index"] = i
776
+ err_response["operation"] = op
777
+ skipped_operations.append(err_response)
778
+ continue
779
+
780
+ normalized_op = cls._normalized_bulk_operation(op, parameters, timeout)
781
+ valid.append((i, normalized_op, entity_id, action))
782
+
632
783
  return valid
633
784
 
634
785
  async def bulk_device_control(
635
786
  self,
636
- operations: list[dict[str, Any]],
787
+ operations: list[Any],
637
788
  parallel: bool = True,
638
789
  ctx: Context | None = None,
639
790
  ) -> dict[str, Any]:
@@ -672,6 +823,10 @@ class DeviceControlTools:
672
823
  f"{len(skipped_operations)} skipped, "
673
824
  f"mode={'parallel' if parallel else 'sequential'}",
674
825
  )
826
+ if not valid_operations:
827
+ return self._build_bulk_response(
828
+ operations, results, operation_ids, skipped_operations, parallel
829
+ )
675
830
 
676
831
  # Route through the component's bulk_call_service capability when
677
832
  # advertised (D5a): one register-before-fire batch frame confirms every
@@ -904,7 +1059,7 @@ class DeviceControlTools:
904
1059
 
905
1060
  async def _bulk_via_component(
906
1061
  self,
907
- operations: list[dict[str, Any]],
1062
+ operations: list[Any],
908
1063
  valid_operations: list[tuple[int, dict[str, Any], str, str]],
909
1064
  skipped_operations: list[dict[str, Any]],
910
1065
  parallel: bool,
@@ -1111,7 +1266,7 @@ class DeviceControlTools:
1111
1266
 
1112
1267
  def _build_ambiguous_bulk_response(
1113
1268
  self,
1114
- operations: list[dict[str, Any]],
1269
+ operations: list[Any],
1115
1270
  valid_operations: list[tuple[int, dict[str, Any], str, str]],
1116
1271
  rows: list[dict[str, Any]],
1117
1272
  skipped_operations: list[dict[str, Any]],
@@ -1246,7 +1401,7 @@ class DeviceControlTools:
1246
1401
 
1247
1402
  def _build_bulk_response(
1248
1403
  self,
1249
- operations: list[dict[str, Any]],
1404
+ operations: list[Any],
1250
1405
  results: list[dict[str, Any]],
1251
1406
  operation_ids: list[str],
1252
1407
  skipped_operations: list[dict[str, Any]],
@@ -1293,7 +1448,7 @@ class DeviceControlTools:
1293
1448
  return response
1294
1449
 
1295
1450
  async def get_bulk_operation_status(
1296
- self, operation_ids: list[str], timeout_seconds: int = 10
1451
+ self, operation_ids: list[str], timeout_seconds: float = 10
1297
1452
  ) -> dict[str, Any]:
1298
1453
  """
1299
1454
  Check status of multiple operations.
@@ -40,7 +40,14 @@ class _SearchBase:
40
40
  logger.debug(f"Could not fetch {label}: {result}")
41
41
  cause = str(result) or type(result).__name__
42
42
  elif isinstance(result, dict) and result.get("success"):
43
- registry = result.get("result", [])
43
+ payload = result.get("result", [])
44
+ if isinstance(payload, list):
45
+ registry = payload
46
+ else:
47
+ cause = (
48
+ "malformed result payload: expected list, "
49
+ f"got {type(payload).__name__}"
50
+ )
44
51
  elif isinstance(result, dict):
45
52
  cause = str(result.get("error") or "request failed")
46
53
  else:
@@ -4,7 +4,7 @@ import asyncio
4
4
  import logging
5
5
  from typing import Any
6
6
 
7
- from ...utils.fuzzy_search import calculate_partial_ratio
7
+ from ...utils.fuzzy_search import calculate_partial_ratio, calculate_ratio
8
8
  from ...visibility.resolver import (
9
9
  device_registry_needed_for_visibility,
10
10
  load_hidden_set,
@@ -405,9 +405,10 @@ class EntitySearchMixin(_SearchBase):
405
405
  """
406
406
  Get entities grouped by area/room using the HA registries for accurate area resolution.
407
407
 
408
- Uses entity registry, device registry, and area registry to determine
409
- which area each entity belongs to. Fuzzy matches the query against
410
- area names, IDs, and area-registry aliases to find the target area(s).
408
+ Uses the entity, device, area, and floor registries to determine which
409
+ area each entity belongs to. Resolution tries an exact area first,
410
+ then an exact or unambiguous close-spelling floor (expanded to every
411
+ area on that floor), and finally falls back to fuzzy area matching.
411
412
 
412
413
  Args:
413
414
  area_query: Area/room name (or alias) to search for
@@ -419,6 +420,10 @@ class EntitySearchMixin(_SearchBase):
419
420
 
420
421
  Returns:
421
422
  Dictionary with area-grouped entities
423
+
424
+ Raises:
425
+ ToolError: If mandatory Home Assistant state retrieval fails or an
426
+ unexpected search error occurs.
422
427
  """
423
428
  try:
424
429
  # Fetch all registries and states in parallel
@@ -433,6 +438,9 @@ class EntitySearchMixin(_SearchBase):
433
438
  self.client.send_websocket_message(
434
439
  {"type": "config/device_registry/list"}
435
440
  ),
441
+ self.client.send_websocket_message(
442
+ {"type": "config/floor_registry/list"}
443
+ ),
436
444
  return_exceptions=True,
437
445
  )
438
446
 
@@ -465,11 +473,22 @@ class EntitySearchMixin(_SearchBase):
465
473
  area_registry = self._parse_area_registry(results[1], registry_warnings)
466
474
  entity_reg_map = self._parse_entity_reg_map(results[2], registry_warnings)
467
475
  device_area_map = self._parse_device_area_map(results[3], registry_warnings)
476
+ floor_result = results[4]
477
+ floor_registry_available = (
478
+ isinstance(floor_result, dict)
479
+ and floor_result.get("success") is True
480
+ and isinstance(floor_result.get("result"), list)
481
+ )
482
+ floor_registry = self._parse_floor_registry(floor_result, registry_warnings)
468
483
  degraded_warnings = registry_warnings + visibility_warnings
469
484
 
470
- area_query_lower = area_query.lower().strip()
471
- matched_area_ids = self._match_area_ids(area_registry, area_query_lower)
472
-
485
+ matched_area_ids, resolution_warnings = self._resolve_area_query(
486
+ area_registry,
487
+ floor_registry,
488
+ area_query,
489
+ floor_registry_available=floor_registry_available,
490
+ )
491
+ degraded_warnings.extend(resolution_warnings)
473
492
  if not matched_area_ids:
474
493
  return merge_visibility_warnings(
475
494
  {
@@ -536,6 +555,18 @@ class EntitySearchMixin(_SearchBase):
536
555
  area_registry[area_id] = area
537
556
  return area_registry
538
557
 
558
+ @classmethod
559
+ def _parse_floor_registry(
560
+ cls, result: Any, warnings: list[str] | None = None
561
+ ) -> dict[str, dict[str, Any]]:
562
+ """Parse the floor registry into ``floor_id -> floor info``."""
563
+ floor_registry: dict[str, dict[str, Any]] = {}
564
+ for floor in cls._extract_registry_list(result, "floor registry", warnings):
565
+ floor_id = floor.get("floor_id", "")
566
+ if floor_id:
567
+ floor_registry[floor_id] = floor
568
+ return floor_registry
569
+
539
570
  @classmethod
540
571
  def _parse_entity_reg_map(
541
572
  cls, result: Any, warnings: list[str] | None = None
@@ -565,54 +596,203 @@ class EntitySearchMixin(_SearchBase):
565
596
  return device_area_map
566
597
 
567
598
  @staticmethod
599
+ def _match_exact_registry_ids(
600
+ registry: dict[str, dict[str, Any]],
601
+ id_key: str,
602
+ query_lower: str,
603
+ ) -> set[str]:
604
+ """Return exact case-insensitive id, name, or alias matches."""
605
+ matches: set[str] = set()
606
+ for registry_id, info in registry.items():
607
+ item_id = str(info.get(id_key) or registry_id)
608
+ name = str(info.get("name") or "")
609
+ aliases = info.get("aliases", []) or []
610
+ if (
611
+ query_lower == item_id.lower()
612
+ or query_lower == name.lower()
613
+ or any(
614
+ query_lower == alias.lower()
615
+ for alias in aliases
616
+ if isinstance(alias, str)
617
+ )
618
+ ):
619
+ matches.add(registry_id)
620
+ return matches
621
+
622
+ @classmethod
623
+ def _resolve_area_query(
624
+ cls,
625
+ area_registry: dict[str, dict[str, Any]],
626
+ floor_registry: dict[str, dict[str, Any]],
627
+ area_query: str,
628
+ *,
629
+ floor_registry_available: bool,
630
+ ) -> tuple[set[str], list[str]]:
631
+ """Resolve exact area first, then exact/fuzzy floor, then fuzzy area."""
632
+ query_lower = area_query.lower().strip()
633
+ exact_area_ids = cls._match_exact_registry_ids(
634
+ area_registry, "area_id", query_lower
635
+ )
636
+ exact_floor_ids = cls._match_exact_registry_ids(
637
+ floor_registry, "floor_id", query_lower
638
+ )
639
+ if exact_area_ids:
640
+ warnings = []
641
+ if exact_floor_ids:
642
+ warnings.append(
643
+ f"'{area_query}' matches both an area and a floor; "
644
+ "the exact area match was used."
645
+ )
646
+ return exact_area_ids, warnings
647
+
648
+ if exact_floor_ids:
649
+ area_ids, warning = cls._expand_floor_ids(
650
+ area_registry,
651
+ floor_registry,
652
+ exact_floor_ids,
653
+ area_query,
654
+ fuzzy_match=False,
655
+ )
656
+ return area_ids, [warning]
657
+
658
+ if not floor_registry_available:
659
+ # Without the floor registry, fuzzy area matching could silently
660
+ # recreate the original floor->partial-area misresolution.
661
+ return set(), []
662
+
663
+ fuzzy_floor_ids, floor_score = cls._match_fuzzy_registry_ids(
664
+ floor_registry, "floor_id", query_lower
665
+ )
666
+ _, area_score = cls._match_fuzzy_registry_ids(
667
+ area_registry, "area_id", query_lower
668
+ )
669
+ if len(fuzzy_floor_ids) > 1 and floor_score > area_score:
670
+ floor_names = sorted(
671
+ str(
672
+ floor_registry[floor_id].get("name")
673
+ or floor_registry[floor_id].get("floor_id")
674
+ or floor_id
675
+ )
676
+ for floor_id in fuzzy_floor_ids
677
+ )
678
+ return set(), [
679
+ f"'{area_query}' ambiguously matches multiple floors: "
680
+ f"{', '.join(floor_names)}. Use an exact floor name or ID."
681
+ ]
682
+ if len(fuzzy_floor_ids) == 1 and floor_score > area_score:
683
+ area_ids, warning = cls._expand_floor_ids(
684
+ area_registry,
685
+ floor_registry,
686
+ fuzzy_floor_ids,
687
+ area_query,
688
+ fuzzy_match=True,
689
+ )
690
+ return area_ids, [warning]
691
+ return cls._match_area_ids(area_registry, query_lower), []
692
+
693
+ @staticmethod
694
+ def _match_fuzzy_registry_ids(
695
+ registry: dict[str, dict[str, Any]],
696
+ id_key: str,
697
+ query_lower: str,
698
+ ) -> tuple[set[str], int]:
699
+ """Return best whole-string registry matches and their score.
700
+
701
+ Whole-string similarity distinguishes a typo in a floor name from an
702
+ area name that merely contains the floor name as one component.
703
+ """
704
+ best_ids: set[str] = set()
705
+ best_score = 0
706
+ for registry_id, info in registry.items():
707
+ values = [
708
+ str(info.get(id_key) or registry_id),
709
+ str(info.get("name") or ""),
710
+ *(
711
+ alias
712
+ for alias in (info.get("aliases", []) or [])
713
+ if isinstance(alias, str)
714
+ ),
715
+ ]
716
+ score = max(
717
+ (
718
+ calculate_ratio(query_lower, value.lower())
719
+ for value in values
720
+ if value
721
+ ),
722
+ default=0,
723
+ )
724
+ if score > best_score:
725
+ best_ids = {registry_id}
726
+ best_score = score
727
+ elif score == best_score:
728
+ best_ids.add(registry_id)
729
+ return (best_ids, best_score) if best_score >= 80 else (set(), best_score)
730
+
731
+ @staticmethod
732
+ def _expand_floor_ids(
733
+ area_registry: dict[str, dict[str, Any]],
734
+ floor_registry: dict[str, dict[str, Any]],
735
+ floor_ids: set[str],
736
+ area_query: str,
737
+ *,
738
+ fuzzy_match: bool,
739
+ ) -> tuple[set[str], str]:
740
+ """Expand floor IDs to their areas and build the model-facing warning."""
741
+ area_ids = {
742
+ area_id
743
+ for area_id, area in area_registry.items()
744
+ if area.get("floor_id") in floor_ids
745
+ }
746
+ floor_names = [
747
+ str(
748
+ floor_registry[floor_id].get("name")
749
+ or floor_registry[floor_id].get("floor_id")
750
+ or ""
751
+ )
752
+ for floor_id in sorted(floor_ids)
753
+ ]
754
+ floor_label = ", ".join(name for name in floor_names if name)
755
+ floor_suffix = f" ({floor_label})" if floor_label else ""
756
+ match_suffix = " from a close spelling" if fuzzy_match else ""
757
+ warning = (
758
+ f"'{area_query}' is a floor, not an area — expanded to "
759
+ f"{len(area_ids)} area(s) on the matched floor(s){floor_suffix}"
760
+ f"{match_suffix}."
761
+ )
762
+ return area_ids, warning
763
+
764
+ @classmethod
568
765
  def _match_area_ids(
569
- area_registry: dict[str, dict[str, Any]], area_query_lower: str
766
+ cls, area_registry: dict[str, dict[str, Any]], area_query_lower: str
570
767
  ) -> set[str]:
571
- """Resolve the query to area_ids: exact id/name/alias match, else fuzzy.
572
-
573
- Two-pass: pass 1 collects exact id / name / alias matches; if any are
574
- found, fuzzy aggregation is skipped entirely. This makes ``area_filter``
575
- honor a literal area_id from ``ha_list_floors_areas`` — a query like
576
- ``"bedroom_kids"`` would otherwise also fuzzy-match its parent
577
- ``"bedroom"`` (partial_ratio=100) and aggregate sibling areas' entities.
578
- Aliases (per-area registry, used by HA voice config) mirror the
579
- entity-side enrichment in smart_entity_search.
768
+ """Resolve the query to area IDs: exact id/name/alias, then fuzzy.
769
+
770
+ Exact matches delegate to the shared registry matcher so area and floor
771
+ precedence use identical case-insensitive semantics.
580
772
  """
581
- exact_area_ids: set[str] = set()
582
- fuzzy_area_ids: set[str] = set()
773
+ exact_area_ids = cls._match_exact_registry_ids(
774
+ area_registry, "area_id", area_query_lower
775
+ )
776
+ if exact_area_ids:
777
+ return exact_area_ids
583
778
 
779
+ fuzzy_area_ids: set[str] = set()
584
780
  for area_id, area_info in area_registry.items():
585
- area_name = area_info.get("name", "")
781
+ area_name = str(area_info.get("name") or "")
586
782
  area_aliases = area_info.get("aliases", []) or []
587
- # Exact match on area_id, name, or any alias (case-insensitive)
588
- if (
589
- area_query_lower == area_id.lower()
590
- or area_query_lower == area_name.lower()
591
- or any(
592
- area_query_lower == a.lower()
593
- for a in area_aliases
594
- if isinstance(a, str)
595
- )
596
- ):
597
- exact_area_ids.add(area_id)
598
- continue
599
- # Fuzzy match on area name, id, or any alias
600
783
  name_score = calculate_partial_ratio(area_query_lower, area_name.lower())
601
784
  id_score = calculate_partial_ratio(area_query_lower, area_id.lower())
602
785
  alias_score = max(
603
786
  (
604
- calculate_partial_ratio(area_query_lower, a.lower())
605
- for a in area_aliases
606
- if isinstance(a, str)
787
+ calculate_partial_ratio(area_query_lower, alias.lower())
788
+ for alias in area_aliases
789
+ if isinstance(alias, str)
607
790
  ),
608
791
  default=0,
609
792
  )
610
793
  if max(name_score, id_score, alias_score) >= 80:
611
794
  fuzzy_area_ids.add(area_id)
612
-
613
- # Exact matches win — fuzzy aggregation only runs when no area_query is
614
- # itself an area_id / name / alias.
615
- return exact_area_ids or fuzzy_area_ids
795
+ return fuzzy_area_ids
616
796
 
617
797
  @staticmethod
618
798
  def _resolve_entity_areas(
@@ -321,6 +321,13 @@ def _finalize_partial_state(
321
321
  if partial_local:
322
322
  response["partial"] = True
323
323
  response["errors"].extend(errors_local)
324
+ local_reason = "; ".join(
325
+ f"{error['surface']}: {error['error']}" for error in errors_local
326
+ )
327
+ existing_reason = response.get("partial_reason")
328
+ response["partial_reason"] = (
329
+ f"{existing_reason}; {local_reason}" if existing_reason else local_reason
330
+ )
324
331
 
325
332
 
326
333
  def _compute_eligibility(
@@ -1691,8 +1698,10 @@ class SearchTools:
1691
1698
  Field(
1692
1699
  default=None,
1693
1700
  description=(
1694
- "Narrow entity-registry results to an area (id or name). "
1695
- "Does not affect configuration search."
1701
+ "Narrow entity-registry results to an area (id, name, or alias), "
1702
+ "an exact floor (id, name, or alias), or an unambiguous "
1703
+ "close-spelling floor match; a floor match expands to all areas "
1704
+ "on that floor. Does not affect configuration search."
1696
1705
  ),
1697
1706
  ),
1698
1707
  ] = None,
@@ -2255,7 +2264,10 @@ class SearchTools:
2255
2264
  str | None,
2256
2265
  Field(
2257
2266
  default=None,
2258
- description="Limit to entities in a specific area (area ID or name).",
2267
+ description=(
2268
+ "Limit to an area, or an exact/close floor name to expand "
2269
+ "to every area on that floor."
2270
+ ),
2259
2271
  ),
2260
2272
  ] = None,
2261
2273
  limit: Annotated[