ha-mcp-dev 8.3.0.dev2379__tar.gz → 8.3.0.dev2382__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.dev2379/src/ha_mcp_dev.egg-info → ha_mcp_dev-8.3.0.dev2382}/PKG-INFO +1 -1
  2. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/pyproject.toml +1 -1
  3. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/device_control.py +166 -109
  4. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/smart_search/_base.py +6 -1
  5. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/smart_search/_entities.py +22 -10
  6. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/tools_search.py +10 -7
  7. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/tools_service.py +26 -6
  8. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  9. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/LICENSE +0 -0
  10. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/MANIFEST.in +0 -0
  11. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/README.md +0 -0
  12. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/setup.cfg +0 -0
  13. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/__init__.py +0 -0
  14. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/__main__.py +0 -0
  15. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_pypi_marker +0 -0
  16. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_rich_compat.py +0 -0
  17. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/__init__.py +0 -0
  18. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/requirements.txt +0 -0
  19. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/LICENSE +0 -0
  20. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/MANIFEST.sha256 +0 -0
  21. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/VENDORED +0 -0
  22. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/__init__.py +0 -0
  23. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/__main__.py +0 -0
  24. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/asyncio/__init__.py +0 -0
  25. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/asyncio/client.py +0 -0
  26. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/asyncio/connection.py +0 -0
  27. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/asyncio/messages.py +0 -0
  28. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/asyncio/router.py +0 -0
  29. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/asyncio/server.py +0 -0
  30. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/auth.py +0 -0
  31. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/cli.py +0 -0
  32. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/client.py +0 -0
  33. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/connection.py +0 -0
  34. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/datastructures.py +0 -0
  35. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/exceptions.py +0 -0
  36. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/extensions/__init__.py +0 -0
  37. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/extensions/base.py +0 -0
  38. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/extensions/permessage_deflate.py +0 -0
  39. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/frames.py +0 -0
  40. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/headers.py +0 -0
  41. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/http11.py +0 -0
  42. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/imports.py +0 -0
  43. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/legacy/__init__.py +0 -0
  44. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/legacy/auth.py +0 -0
  45. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/legacy/client.py +0 -0
  46. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/legacy/exceptions.py +0 -0
  47. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/legacy/framing.py +0 -0
  48. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/legacy/handshake.py +0 -0
  49. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/legacy/http.py +0 -0
  50. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/legacy/protocol.py +0 -0
  51. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/legacy/server.py +0 -0
  52. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/protocol.py +0 -0
  53. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/proxy.py +0 -0
  54. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/py.typed +0 -0
  55. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/server.py +0 -0
  56. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/streams.py +0 -0
  57. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/sync/__init__.py +0 -0
  58. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/sync/client.py +0 -0
  59. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/sync/connection.py +0 -0
  60. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/sync/messages.py +0 -0
  61. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/sync/router.py +0 -0
  62. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/sync/server.py +0 -0
  63. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/sync/utils.py +0 -0
  64. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/trio/__init__.py +0 -0
  65. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/trio/client.py +0 -0
  66. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/trio/connection.py +0 -0
  67. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/trio/messages.py +0 -0
  68. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/trio/router.py +0 -0
  69. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/trio/server.py +0 -0
  70. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/trio/utils.py +0 -0
  71. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/typing.py +0 -0
  72. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/uri.py +0 -0
  73. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/utils.py +0 -0
  74. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_vendor/websockets/version.py +0 -0
  75. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/_version.py +0 -0
  76. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/auth/__init__.py +0 -0
  77. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/auth/consent_form.py +0 -0
  78. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/auth/oidc_compat.py +0 -0
  79. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/auth/provider.py +0 -0
  80. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/backup_manager.py +0 -0
  81. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/browser_landing.py +0 -0
  82. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/client/__init__.py +0 -0
  83. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/client/rest_client.py +0 -0
  84. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/client/supervisor_client.py +0 -0
  85. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/client/websocket_client.py +0 -0
  86. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/client/websocket_listener.py +0 -0
  87. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/config.py +0 -0
  88. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  89. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  90. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
  91. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
  92. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  93. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/dashboard_screenshot/theme_guard.py +0 -0
  94. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/errors.py +0 -0
  95. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/hacs_auto_refresh.py +0 -0
  96. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/llm_exposure.py +0 -0
  97. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/policy/__init__.py +0 -0
  98. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/policy/approval_queue.py +0 -0
  99. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/policy/editing.py +0 -0
  100. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/policy/evaluator.py +0 -0
  101. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/policy/handlers.py +0 -0
  102. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/policy/middleware.py +0 -0
  103. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/policy/model.py +0 -0
  104. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/policy/persistence.py +0 -0
  105. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/policy/value_sources.py +0 -0
  106. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/py.typed +0 -0
  107. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/read_only.py +0 -0
  108. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/redaction.py +0 -0
  109. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/renamed_tools.py +0 -0
  110. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  111. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  112. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  113. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  114. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  115. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  116. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  117. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  118. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  119. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  120. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  121. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  122. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
  123. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  124. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
  125. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  126. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  127. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  128. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  129. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  130. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  131. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  132. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
  133. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  134. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  135. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/server.py +0 -0
  136. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/settings_ui/__init__.py +0 -0
  137. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
  138. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
  139. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
  140. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
  141. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
  142. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
  143. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/settings_ui/_i18n.py +0 -0
  144. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/settings_ui/_persistence.py +0 -0
  145. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
  146. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/settings_ui/_theme.py +0 -0
  147. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
  148. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/settings_ui/locales/README.md +0 -0
  149. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/settings_ui/locales/cs.json +0 -0
  150. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/settings_ui/locales/de.json +0 -0
  151. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/settings_ui/locales/en.json +0 -0
  152. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/settings_ui/locales/eo.json +0 -0
  153. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/settings_ui/locales/es.json +0 -0
  154. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/settings_ui/locales/fr.json +0 -0
  155. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/settings_ui/locales/it.json +0 -0
  156. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/settings_ui/locales/nl.json +0 -0
  157. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/settings_ui/locales/pl.json +0 -0
  158. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/settings_ui/locales/ru.json +0 -0
  159. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/settings_ui/locales/sv.json +0 -0
  160. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/settings_ui/locales/zh-Hans.json +0 -0
  161. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/settings_ui/settings.css +0 -0
  162. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/settings_ui/settings.html +0 -0
  163. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/settings_ui/settings.js +0 -0
  164. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/smoke_test.py +0 -0
  165. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  166. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/strict_bps.py +0 -0
  167. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/__init__.py +0 -0
  168. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/auto_backup.py +0 -0
  169. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/backup.py +0 -0
  170. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  171. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/best_practice_result.py +0 -0
  172. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/best_practice_variables.py +0 -0
  173. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/component_api.py +0 -0
  174. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/component_config_entries.py +0 -0
  175. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/component_config_reads.py +0 -0
  176. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/component_devices.py +0 -0
  177. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/component_registries.py +0 -0
  178. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/component_registry_lookup.py +0 -0
  179. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/config_entry_flow.py +0 -0
  180. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/config_entry_flow_form.py +0 -0
  181. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/config_entry_flow_menu.py +0 -0
  182. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/config_entry_flow_walker.py +0 -0
  183. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/config_entry_identity.py +0 -0
  184. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/config_entry_reconfigure.py +0 -0
  185. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/config_entry_reload_watch.py +0 -0
  186. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/error_log_parsing.py +0 -0
  187. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/hacs_registration.py +0 -0
  188. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/helpers.py +0 -0
  189. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/integration_reconfigure.py +0 -0
  190. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/radio/__init__.py +0 -0
  191. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/radio/base.py +0 -0
  192. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/radio/matter.py +0 -0
  193. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/radio/thread.py +0 -0
  194. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/radio/zigbee.py +0 -0
  195. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/radio/zwave.py +0 -0
  196. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/reference_validator.py +0 -0
  197. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/registry.py +0 -0
  198. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/renamed_tool_middleware.py +0 -0
  199. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  200. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  201. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
  202. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  203. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  204. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  205. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  206. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
  207. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/tools_addons.py +0 -0
  208. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/tools_areas.py +0 -0
  209. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/tools_assist_pipeline.py +0 -0
  210. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  211. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  212. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/tools_calendar.py +0 -0
  213. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/tools_camera.py +0 -0
  214. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/tools_categories.py +0 -0
  215. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/tools_code.py +0 -0
  216. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  217. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  218. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  219. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  220. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  221. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  222. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/tools_dev.py +0 -0
  223. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/tools_energy.py +0 -0
  224. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/tools_entities.py +0 -0
  225. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  226. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/tools_groups.py +0 -0
  227. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/tools_hacs.py +0 -0
  228. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/tools_history.py +0 -0
  229. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/tools_integrations.py +0 -0
  230. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/tools_labels.py +0 -0
  231. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/tools_radio.py +0 -0
  232. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/tools_registry.py +0 -0
  233. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/tools_resources.py +0 -0
  234. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/tools_security_policy.py +0 -0
  235. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/tools_services.py +0 -0
  236. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/tools_system.py +0 -0
  237. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/tools_themes.py +0 -0
  238. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/tools_todo.py +0 -0
  239. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/tools_traces.py +0 -0
  240. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/tools_updates.py +0 -0
  241. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/tools_utility.py +0 -0
  242. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  243. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  244. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
  245. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/tools_zones.py +0 -0
  246. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/util_helpers.py +0 -0
  247. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/tools/validation_middleware.py +0 -0
  248. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/transforms/__init__.py +0 -0
  249. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/transforms/categorized_search.py +0 -0
  250. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  251. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/transport_security.py +0 -0
  252. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/update_check.py +0 -0
  253. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/utils/__init__.py +0 -0
  254. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/utils/config_hash.py +0 -0
  255. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/utils/config_write_lock.py +0 -0
  256. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/utils/data_paths.py +0 -0
  257. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/utils/domain_handlers.py +0 -0
  258. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  259. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  260. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/utils/operation_manager.py +0 -0
  261. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/utils/python_sandbox.py +0 -0
  262. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/utils/skill_loader.py +0 -0
  263. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/utils/usage_logger.py +0 -0
  264. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/visibility/__init__.py +0 -0
  265. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/visibility/enforcement.py +0 -0
  266. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/visibility/model.py +0 -0
  267. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/visibility/persistence.py +0 -0
  268. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp/visibility/resolver.py +0 -0
  269. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  270. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  271. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  272. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  273. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  274. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/tests/__init__.py +0 -0
  275. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/tests/test_constants.py +0 -0
  276. {ha_mcp_dev-8.3.0.dev2379 → ha_mcp_dev-8.3.0.dev2382}/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.dev2379
3
+ Version: 8.3.0.dev2382
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.dev2379"
7
+ version = "8.3.0.dev2382"
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
@@ -42,10 +42,23 @@ from .helpers import (
42
42
  safe_info,
43
43
  safe_progress,
44
44
  )
45
+ from .tools_service import BulkControlOperation
45
46
  from .util_helpers import _SERVICE_TO_STATE
46
47
 
47
48
  logger = logging.getLogger(__name__)
48
49
 
50
+ # The bulk row contract is declared once, on the advertised transport schema.
51
+ # Reading its keys back here keeps the runtime batch validator from drifting
52
+ # out of sync with what models are told they may send.
53
+ _BULK_OPERATION_KEYS: frozenset[str] = frozenset(BulkControlOperation.__annotations__)
54
+
55
+ # The row shape models get wrong most often is a service-style row, so this
56
+ # leads every suggestion list a rejected bulk batch produces.
57
+ _BULK_ROW_SHAPE_SUGGESTION = (
58
+ "Use entity_id and action (e.g. action='off', not service='turn_off'); "
59
+ "send no domain or service keys and put action data in parameters"
60
+ )
61
+
49
62
  # The ha_mcp_tools/bulk_call_service WS command (Phase 3, D5a): the BATCH write
50
63
  # capability. When the component advertises ``bulk_call_service`` the consumer
51
64
  # resolves every op's domain/service server-side (D6), sends one register-before-fire
@@ -347,6 +360,10 @@ class DeviceControlTools:
347
360
  "set": "set_temperature",
348
361
  }
349
362
 
363
+ # Silent allowlist: _add_domain_params copies only these keys into the
364
+ # service call and drops everything else without an error, and a domain
365
+ # absent from this table loses every parameter. Extend it when adding
366
+ # support for a new parameter, or the parameter vanishes at dispatch.
350
367
  _DOMAIN_PARAMS: ClassVar[dict[str, list[str]]] = {
351
368
  "light": [
352
369
  "brightness",
@@ -474,10 +491,10 @@ class DeviceControlTools:
474
491
  """Check status of a device operation, waiting up to ``timeout_seconds`` for completion.
475
492
 
476
493
  Polls the in-memory operation registry (mutated by the WebSocket
477
- listener as state changes arrive) every 0.2s while the operation is
478
- pending, up to ``timeout_seconds``. Returns the final structured status
479
- — completed/failed/timeout/pending — produced by
480
- ``control_device_smart``.
494
+ listener as state changes arrive) while the operation is pending, at
495
+ 0.2s intervals except for a final shorter poll that lands exactly on
496
+ the deadline. Returns the final structured status —
497
+ completed/failed/timeout/pending — produced by ``control_device_smart``.
481
498
  """
482
499
  operation = get_operation_from_memory(operation_id)
483
500
 
@@ -607,31 +624,58 @@ class DeviceControlTools:
607
624
  @staticmethod
608
625
  def _normalize_bulk_parameters(
609
626
  value: Any,
610
- ) -> tuple[dict[str, Any] | None, ErrorCode | None]:
611
- """Normalize optional bulk parameters and identify invalid values."""
627
+ ) -> tuple[dict[str, Any] | None, ErrorCode | None, str]:
628
+ """Normalize a present ``parameters`` value and identify invalid ones.
629
+
630
+ Returns ``(parameters, error_code, detail)``. ``detail`` carries the
631
+ decoder's own reason and offset for a JSON failure — without it the
632
+ caller can only say "invalid JSON", which does not tell the sender
633
+ where its string went wrong. It is empty for every other outcome.
634
+ """
612
635
  if isinstance(value, str):
613
636
  try:
614
637
  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
638
+ except json.JSONDecodeError as exc:
639
+ return (
640
+ None,
641
+ ErrorCode.VALIDATION_INVALID_JSON,
642
+ f"{exc.msg} at position {exc.pos}",
643
+ )
644
+ except RecursionError:
645
+ # json.loads recurses per nesting level; a pathologically
646
+ # nested string must skip the row, not abort the batch.
647
+ return (
648
+ None,
649
+ ErrorCode.VALIDATION_INVALID_JSON,
650
+ "JSON nested too deeply",
651
+ )
652
+ if not isinstance(value, dict):
653
+ # A present-but-null value is malformed, same as timeout_seconds
654
+ # and validate_first; an absent key never reaches this helper.
655
+ return None, ErrorCode.VALIDATION_INVALID_PARAMETER, ""
656
+ return value, None, ""
620
657
 
621
658
  @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
659
+ def _normalize_bulk_timeout(value: Any) -> float | None:
660
+ """Normalize a non-negative finite timeout, or ``None`` when invalid.
661
+
662
+ Bools are rejected before ``float()`` sees them: ``float(True)`` is 1.0,
663
+ which would silently accept ``timeout_seconds: true`` as a one-second
664
+ wait. Strings are rejected for the same reason — the advertised schema
665
+ is ``strict``, so coercing ``"10"`` here would silently accept what the
666
+ schema tells the model is invalid. ``None`` is invalid here too — the
667
+ caller only calls this when the key is present, and a present-but-null
668
+ timeout is a malformed row.
669
+ """
670
+ if isinstance(value, (bool, str)) or value is None:
671
+ return None
626
672
  try:
627
673
  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
- )
674
+ except (TypeError, ValueError, OverflowError):
675
+ # OverflowError: an int too large for float must skip the row,
676
+ # not abort the batch through the generic exception handler.
677
+ return None
678
+ return timeout if math.isfinite(timeout) and timeout >= 0 else None
635
679
 
636
680
  @staticmethod
637
681
  def _normalized_bulk_operation(
@@ -647,6 +691,28 @@ class DeviceControlTools:
647
691
  normalized["timeout_seconds"] = timeout
648
692
  return normalized
649
693
 
694
+ @staticmethod
695
+ def _skip_bulk_operation(
696
+ skipped_operations: list[dict[str, Any]],
697
+ op: Any,
698
+ code: ErrorCode,
699
+ error: str,
700
+ context: dict[str, Any],
701
+ suggestions: list[str] | None = None,
702
+ ) -> None:
703
+ """Log one rejected bulk row and record its structured skip entry.
704
+
705
+ ``create_error_response`` lifts every ``context`` key to the top level,
706
+ so ``index`` needs no separate write; ``operation`` is not a context
707
+ key and is attached here so callers can see the row they sent.
708
+ """
709
+ logger.warning(f"Bulk control: {error}")
710
+ err_response = create_error_response(
711
+ code, error, suggestions=suggestions, context=context
712
+ )
713
+ err_response["operation"] = op
714
+ skipped_operations.append(err_response)
715
+
650
716
  @classmethod
651
717
  def _validate_bulk_operations(
652
718
  cls,
@@ -656,41 +722,28 @@ class DeviceControlTools:
656
722
  valid: list[tuple[int, dict[str, Any], str, str]] = []
657
723
  for i, op in enumerate(operations):
658
724
  if not isinstance(op, dict):
659
- error = f"Operation at index {i} is not a dict: {type(op).__name__}"
660
- logger.warning(f"Bulk control: {error}")
661
- err_response = create_error_response(
662
- ErrorCode.VALIDATION_MISSING_PARAMETER, error, context={"index": i}
725
+ cls._skip_bulk_operation(
726
+ skipped_operations,
727
+ op,
728
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
729
+ f"Operation at index {i} is not a dict: {type(op).__name__}",
730
+ {"index": i},
663
731
  )
664
- err_response["index"] = i
665
- err_response["operation"] = op
666
- skipped_operations.append(err_response)
667
732
  continue
668
733
 
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)
734
+ # str() before sorting: a direct Python caller can pass non-string
735
+ # keys, and mixed key types make sorted()/join() raise mid-batch.
736
+ obsolete_keys = sorted(str(key) for key in set(op) - _BULK_OPERATION_KEYS)
677
737
  if obsolete_keys:
678
738
  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(
739
+ cls._skip_bulk_operation(
740
+ skipped_operations,
741
+ op,
684
742
  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},
743
+ f"Operation at index {i} contains unsupported key(s): {key_list}",
744
+ {"index": i, "unsupported_keys": obsolete_keys},
745
+ suggestions=[_BULK_ROW_SHAPE_SUGGESTION],
690
746
  )
691
- err_response["index"] = i
692
- err_response["operation"] = op
693
- skipped_operations.append(err_response)
694
747
  continue
695
748
 
696
749
  entity_id = op.get("entity_id")
@@ -698,83 +751,70 @@ class DeviceControlTools:
698
751
  missing = [f for f in ("entity_id", "action") if not op.get(f)]
699
752
 
700
753
  if missing:
701
- error = f"Operation at index {i} missing required fields: {', '.join(missing)}"
702
- logger.warning(f"Bulk control: {error}")
703
- err_response = create_error_response(
704
- ErrorCode.VALIDATION_MISSING_PARAMETER, error, context={"index": i}
754
+ cls._skip_bulk_operation(
755
+ skipped_operations,
756
+ op,
757
+ ErrorCode.VALIDATION_MISSING_PARAMETER,
758
+ f"Operation at index {i} missing required fields: "
759
+ f"{', '.join(missing)}",
760
+ {"index": i},
705
761
  )
706
- err_response["index"] = i
707
- err_response["operation"] = op
708
- skipped_operations.append(err_response)
709
762
  continue
710
763
 
711
764
  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(
765
+ cls._skip_bulk_operation(
766
+ skipped_operations,
767
+ op,
715
768
  ErrorCode.VALIDATION_INVALID_PARAMETER,
716
- error,
717
- context={"index": i},
769
+ f"Operation at index {i} requires string entity_id and "
770
+ "action fields",
771
+ {"index": i},
718
772
  )
719
- err_response["index"] = i
720
- err_response["operation"] = op
721
- skipped_operations.append(err_response)
722
773
  continue
723
774
 
724
- parameters, parameter_error = cls._normalize_bulk_parameters(
725
- op.get("parameters")
775
+ parameters, parameter_error, parameter_detail = (
776
+ cls._normalize_bulk_parameters(op["parameters"])
777
+ if "parameters" in op
778
+ else (None, None, "")
726
779
  )
727
780
  if parameter_error is not None:
728
- error = (
729
- f"Operation at index {i} has invalid JSON parameters"
781
+ parameter_message = (
782
+ f"Operation at index {i} has invalid JSON parameters: "
783
+ f"{parameter_detail}"
730
784
  if parameter_error == ErrorCode.VALIDATION_INVALID_JSON
731
785
  else f"Operation at index {i} parameters must be a JSON object"
732
786
  )
733
- logger.warning(f"Bulk control: {error}")
734
- err_response = create_error_response(
787
+ cls._skip_bulk_operation(
788
+ skipped_operations,
789
+ op,
735
790
  parameter_error,
736
- error,
737
- context={"index": i},
791
+ parameter_message,
792
+ {"index": i},
738
793
  )
739
- err_response["index"] = i
740
- err_response["operation"] = op
741
- skipped_operations.append(err_response)
742
794
  continue
743
795
 
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
796
+ timeout: float | None = None
797
+ if "timeout_seconds" in op:
798
+ timeout = cls._normalize_bulk_timeout(op["timeout_seconds"])
799
+ if timeout is None:
800
+ cls._skip_bulk_operation(
801
+ skipped_operations,
802
+ op,
803
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
804
+ f"Operation at index {i} timeout_seconds must be a "
805
+ "non-negative number",
806
+ {"index": i},
807
+ )
808
+ continue
766
809
 
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(
810
+ if "validate_first" in op and not isinstance(op["validate_first"], bool):
811
+ cls._skip_bulk_operation(
812
+ skipped_operations,
813
+ op,
771
814
  ErrorCode.VALIDATION_INVALID_PARAMETER,
772
- error,
773
- context={"index": i},
815
+ f"Operation at index {i} validate_first must be a boolean",
816
+ {"index": i},
774
817
  )
775
- err_response["index"] = i
776
- err_response["operation"] = op
777
- skipped_operations.append(err_response)
778
818
  continue
779
819
 
780
820
  normalized_op = cls._normalized_bulk_operation(op, parameters, timeout)
@@ -824,8 +864,24 @@ class DeviceControlTools:
824
864
  f"mode={'parallel' if parallel else 'sequential'}",
825
865
  )
826
866
  if not valid_operations:
827
- return self._build_bulk_response(
828
- operations, results, operation_ids, skipped_operations, parallel
867
+ # Nothing dispatched, so nothing to fail soft for: the batch
868
+ # carve-out from "all tool-level failures raise" exists to
869
+ # protect successful siblings, and there are none. Raising
870
+ # before the component probe keeps the no-dispatch guarantee.
871
+ raise_tool_error(
872
+ create_error_response(
873
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
874
+ f"All {len(operations)} operation(s) failed validation; "
875
+ "nothing was dispatched",
876
+ suggestions=[
877
+ _BULK_ROW_SHAPE_SUGGESTION,
878
+ "Each operation requires 'entity_id' and 'action'",
879
+ "Check skipped_details for the per-operation reason",
880
+ "Example: {'entity_id': 'light.living_room', "
881
+ + "'action': 'on'}",
882
+ ],
883
+ context={"skipped_details": skipped_operations},
884
+ )
829
885
  )
830
886
 
831
887
  # Route through the component's bulk_call_service capability when
@@ -1439,6 +1495,7 @@ class DeviceControlTools:
1439
1495
  if skipped_operations:
1440
1496
  response["skipped_details"] = skipped_operations
1441
1497
  response["suggestions"] = [
1498
+ _BULK_ROW_SHAPE_SUGGESTION,
1442
1499
  "Some operations were skipped due to validation errors",
1443
1500
  "Each operation requires 'entity_id' and 'action' fields",
1444
1501
  "Check skipped_details for specific errors",
@@ -40,9 +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
- payload = result.get("result", [])
43
+ # No default for a missing key: a legitimate empty registry is
44
+ # "result": [], so an absent result is a malformed envelope and
45
+ # must degrade loudly, not read as available-and-empty.
46
+ payload = result.get("result")
44
47
  if isinstance(payload, list):
45
48
  registry = payload
49
+ elif payload is None and "result" not in result:
50
+ cause = "malformed result payload: result key missing"
46
51
  else:
47
52
  cause = (
48
53
  "malformed result payload: expected list, "
@@ -473,13 +473,15 @@ class EntitySearchMixin(_SearchBase):
473
473
  area_registry = self._parse_area_registry(results[1], registry_warnings)
474
474
  entity_reg_map = self._parse_entity_reg_map(results[2], registry_warnings)
475
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)
476
+ # Availability is read back OUT of the parse rather than re-derived
477
+ # from the raw payload: the parser already decides what counts as
478
+ # usable floor data, and a second predicate here drifted from it
479
+ # once. An empty warnings list means the parser accepted the
480
+ # payload; anything else disables the fuzzy floor/area fallback.
481
+ floor_warnings: list[str] = []
482
+ floor_registry = self._parse_floor_registry(results[4], floor_warnings)
483
+ floor_registry_available = not floor_warnings
484
+ registry_warnings.extend(floor_warnings)
483
485
  degraded_warnings = registry_warnings + visibility_warnings
484
486
 
485
487
  matched_area_ids, resolution_warnings = self._resolve_area_query(
@@ -657,8 +659,15 @@ class EntitySearchMixin(_SearchBase):
657
659
 
658
660
  if not floor_registry_available:
659
661
  # Without the floor registry, fuzzy area matching could silently
660
- # recreate the original floor->partial-area misresolution.
661
- return set(), []
662
+ # recreate the original floor->partial-area misresolution. The
663
+ # fetch warning names the outage; this one names what the outage
664
+ # cost the caller, so "no match" is not read as "no such area".
665
+ return set(), [
666
+ f"Floor data is unavailable, so '{area_query}' was matched only "
667
+ "against exact area IDs, names, and aliases; close-spelling "
668
+ "matching was skipped. Retry, or pass an exact area_id from "
669
+ "ha_list_floors_areas."
670
+ ]
662
671
 
663
672
  fuzzy_floor_ids, floor_score = cls._match_fuzzy_registry_ids(
664
673
  floor_registry, "floor_id", query_lower
@@ -768,7 +777,10 @@ class EntitySearchMixin(_SearchBase):
768
777
  """Resolve the query to area IDs: exact id/name/alias, then fuzzy.
769
778
 
770
779
  Exact matches delegate to the shared registry matcher so area and floor
771
- precedence use identical case-insensitive semantics.
780
+ precedence use identical case-insensitive semantics. Exact must win
781
+ outright: a query like "bedroom_kids" partial-matches its parent
782
+ "bedroom" at ratio 100, so falling through to the fuzzy pass would
783
+ aggregate every sibling area's entities under an exact hit.
772
784
  """
773
785
  exact_area_ids = cls._match_exact_registry_ids(
774
786
  area_registry, "area_id", area_query_lower
@@ -321,12 +321,11 @@ 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
324
+ _merge_partial_reason(
325
+ response,
326
+ "; ".join(
327
+ f"{error['surface']}: {error['error']}" for error in errors_local
328
+ ),
330
329
  )
331
330
 
332
331
 
@@ -2217,7 +2216,11 @@ class SearchTools:
2217
2216
  raise outcome
2218
2217
  if isinstance(outcome, Exception):
2219
2218
  partial = True
2220
- errors.append({"surface": label, "error": str(outcome)})
2219
+ # ``str(asyncio.TimeoutError())`` is "" — fall back to the type
2220
+ # name so partial_reason never reads "entities: ".
2221
+ errors.append(
2222
+ {"surface": label, "error": str(outcome) or type(outcome).__name__}
2223
+ )
2221
2224
  logger.warning("ha_search %s branch failed: %r", label, outcome)
2222
2225
  continue
2223
2226
  _apply_search_outcome(response, label, outcome)
@@ -83,7 +83,10 @@ class BulkControlOperation(TypedDict):
83
83
  Field(
84
84
  description=(
85
85
  "Optional action parameters, e.g. {'brightness_pct': 30} "
86
- "when action='on'."
86
+ "when action='on'. Each domain has a fixed allowlist of "
87
+ "supported keys; keys outside it are ignored rather than "
88
+ "rejected. Use ha_call_service for parameters this tool "
89
+ "does not carry."
87
90
  )
88
91
  ),
89
92
  ]
@@ -94,6 +97,10 @@ class BulkControlOperation(TypedDict):
94
97
  Field(
95
98
  ge=0,
96
99
  allow_inf_nan=False,
100
+ # ``strict`` for the same reason validate_first carries it:
101
+ # lax mode coerces ``true`` to 1.0, so a bool would land
102
+ # downstream as a silent one-second timeout.
103
+ strict=True,
97
104
  description=(
98
105
  "Optional confirmation timeout. On the component path, all "
99
106
  "operations share the maximum requested wait (default 10s, "
@@ -108,8 +115,10 @@ class BulkControlOperation(TypedDict):
108
115
  Field(
109
116
  strict=True,
110
117
  description=(
111
- "Validate that the entity exists before dispatch; default true. "
112
- "The action is always validated."
118
+ "Report an ENTITY_NOT_FOUND failure when the target entity "
119
+ "does not exist; default true. On the component batch path "
120
+ "this is detected from the captured pre-state rather than by "
121
+ "preventing dispatch. The action is always validated."
113
122
  ),
114
123
  ),
115
124
  ]
@@ -1531,7 +1540,8 @@ class ServiceTools:
1531
1540
 
1532
1541
  Caveats: put every target in ``operations`` and call the tool once. Parallel
1533
1542
  execution is the default, and invalid items are reported without aborting
1534
- valid operations in the same batch.
1543
+ valid operations in the same batch. A batch in which every item fails
1544
+ validation dispatches nothing and fails the call.
1535
1545
  """
1536
1546
  parallel_bool = parallel
1537
1547
 
@@ -1559,14 +1569,24 @@ class ServiceTools:
1559
1569
  )
1560
1570
 
1561
1571
  operations_list: list[Any] = []
1562
- for operation in parsed_operations:
1572
+ for index, operation in enumerate(parsed_operations):
1563
1573
  try:
1564
1574
  operations_list.append(
1565
1575
  _BULK_CONTROL_OPERATION_ADAPTER.validate_python(operation)
1566
1576
  )
1567
- except ValidationError:
1577
+ except ValidationError as exc:
1568
1578
  # Preserve malformed rows for the runtime batch validator, which
1569
1579
  # reports them in skipped_details instead of rejecting the call.
1580
+ # The schema's reason is richer than the runtime validator's and
1581
+ # is the only place it survives, so log it here.
1582
+ # include_input=False: a malformed row can carry sensitive
1583
+ # values (lock or alarm codes in a mistyped field), and
1584
+ # str(exc) would write them to persistent server logs.
1585
+ logger.warning(
1586
+ "ha_bulk_control operation %d failed schema validation: %s",
1587
+ index,
1588
+ exc.errors(include_url=False, include_input=False),
1589
+ )
1570
1590
  operations_list.append(operation)
1571
1591
 
1572
1592
  result = await self._device_tools.bulk_device_control(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 8.3.0.dev2379
3
+ Version: 8.3.0.dev2382
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