ha-mcp-dev 8.4.3.dev2602__tar.gz → 8.4.3.dev2608__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 (313) hide show
  1. {ha_mcp_dev-8.4.3.dev2602/src/ha_mcp_dev.egg-info → ha_mcp_dev-8.4.3.dev2608}/PKG-INFO +2 -2
  2. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/README.md +1 -1
  3. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/pyproject.toml +1 -1
  4. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/server.py +13 -8
  5. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/component_api.py +95 -50
  6. ha_mcp_dev-8.4.3.dev2608/src/ha_mcp/tools/component_dashboard_edit.py +169 -0
  7. ha_mcp_dev-8.4.3.dev2608/src/ha_mcp/tools/dashboard_edit_errors.py +156 -0
  8. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_config_automations.py +62 -13
  9. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_config_dashboards.py +454 -144
  10. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/util_helpers.py +6 -0
  11. ha_mcp_dev-8.4.3.dev2608/src/ha_mcp/utils/dashboard_patch.py +149 -0
  12. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608/src/ha_mcp_dev.egg-info}/PKG-INFO +2 -2
  13. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp_dev.egg-info/SOURCES.txt +3 -0
  14. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/LICENSE +0 -0
  15. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/MANIFEST.in +0 -0
  16. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/setup.cfg +0 -0
  17. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/__init__.py +0 -0
  18. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/__main__.py +0 -0
  19. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_pypi_marker +0 -0
  20. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_rich_compat.py +0 -0
  21. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/__init__.py +0 -0
  22. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/requirements.txt +0 -0
  23. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/LICENSE +0 -0
  24. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/MANIFEST.sha256 +0 -0
  25. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/VENDORED +0 -0
  26. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/__init__.py +0 -0
  27. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/__main__.py +0 -0
  28. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/asyncio/__init__.py +0 -0
  29. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/asyncio/client.py +0 -0
  30. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/asyncio/connection.py +0 -0
  31. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/asyncio/messages.py +0 -0
  32. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/asyncio/router.py +0 -0
  33. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/asyncio/server.py +0 -0
  34. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/auth.py +0 -0
  35. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/cli.py +0 -0
  36. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/client.py +0 -0
  37. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/connection.py +0 -0
  38. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/datastructures.py +0 -0
  39. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/exceptions.py +0 -0
  40. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/extensions/__init__.py +0 -0
  41. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/extensions/base.py +0 -0
  42. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/extensions/permessage_deflate.py +0 -0
  43. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/frames.py +0 -0
  44. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/headers.py +0 -0
  45. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/http11.py +0 -0
  46. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/imports.py +0 -0
  47. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/legacy/__init__.py +0 -0
  48. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/legacy/auth.py +0 -0
  49. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/legacy/client.py +0 -0
  50. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/legacy/exceptions.py +0 -0
  51. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/legacy/framing.py +0 -0
  52. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/legacy/handshake.py +0 -0
  53. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/legacy/http.py +0 -0
  54. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/legacy/protocol.py +0 -0
  55. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/legacy/server.py +0 -0
  56. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/protocol.py +0 -0
  57. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/proxy.py +0 -0
  58. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/py.typed +0 -0
  59. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/server.py +0 -0
  60. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/streams.py +0 -0
  61. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/sync/__init__.py +0 -0
  62. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/sync/client.py +0 -0
  63. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/sync/connection.py +0 -0
  64. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/sync/messages.py +0 -0
  65. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/sync/router.py +0 -0
  66. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/sync/server.py +0 -0
  67. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/sync/utils.py +0 -0
  68. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/trio/__init__.py +0 -0
  69. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/trio/client.py +0 -0
  70. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/trio/connection.py +0 -0
  71. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/trio/messages.py +0 -0
  72. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/trio/router.py +0 -0
  73. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/trio/server.py +0 -0
  74. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/trio/utils.py +0 -0
  75. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/typing.py +0 -0
  76. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/uri.py +0 -0
  77. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/utils.py +0 -0
  78. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_vendor/websockets/version.py +0 -0
  79. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/_version.py +0 -0
  80. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/auth/__init__.py +0 -0
  81. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/auth/consent_form.py +0 -0
  82. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/auth/oidc_compat.py +0 -0
  83. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/auth/provider.py +0 -0
  84. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/backup_manager.py +0 -0
  85. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/browser_landing.py +0 -0
  86. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/client/__init__.py +0 -0
  87. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/client/rest_client.py +0 -0
  88. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/client/supervisor_client.py +0 -0
  89. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/client/websocket_client.py +0 -0
  90. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/client/websocket_listener.py +0 -0
  91. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/config.py +0 -0
  92. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  93. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  94. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
  95. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
  96. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  97. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/dashboard_screenshot/theme_guard.py +0 -0
  98. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/errors.py +0 -0
  99. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/ha_request_queue.py +0 -0
  100. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/hacs_auto_refresh.py +0 -0
  101. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/llm_exposure.py +0 -0
  102. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/log_filters.py +0 -0
  103. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/policy/__init__.py +0 -0
  104. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/policy/approval_queue.py +0 -0
  105. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/policy/editing.py +0 -0
  106. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/policy/evaluator.py +0 -0
  107. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/policy/handlers.py +0 -0
  108. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/policy/middleware.py +0 -0
  109. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/policy/model.py +0 -0
  110. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/policy/persistence.py +0 -0
  111. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/policy/value_sources.py +0 -0
  112. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/py.typed +0 -0
  113. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/read_only.py +0 -0
  114. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/redaction.py +0 -0
  115. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/renamed_tools.py +0 -0
  116. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  117. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  118. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  119. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  120. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  121. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  122. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  123. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  124. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  125. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  126. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/evals/area-target-over-entity-list/case.yaml +0 -0
  127. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/evals/arrive-home-automation/case.yaml +0 -0
  128. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/evals/assist-shortcut-card/case.yaml +0 -0
  129. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/evals/avg-temperature/case.yaml +0 -0
  130. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/evals/backup-deletion-confirmation/case.yaml +0 -0
  131. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/evals/blueprint-authoring/case.yaml +0 -0
  132. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/evals/button-event-entity/case.yaml +0 -0
  133. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/evals/device-id-trigger-migration/case.yaml +0 -0
  134. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/evals/helper-flow-fields-not-platform-yaml/case.yaml +0 -0
  135. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/evals/media-player-song/case.yaml +0 -0
  136. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/evals/motion-light-automation-mode/case.yaml +0 -0
  137. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/evals/rename-entity-impact-analysis/case.yaml +0 -0
  138. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/evals/template-attributes-yaml-exception/case.yaml +0 -0
  139. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/evals/timer-finished-automation/case.yaml +0 -0
  140. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/evals/vacuum-last-clean/case.yaml +0 -0
  141. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/scripts/check_eval_cases.py +0 -0
  142. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  143. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
  144. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  145. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/backups.md +0 -0
  146. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
  147. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  148. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  149. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  150. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  151. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  152. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  153. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  154. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
  155. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  156. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  157. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/__init__.py +0 -0
  158. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
  159. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
  160. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
  161. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
  162. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
  163. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
  164. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/_i18n.py +0 -0
  165. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/_locale_policy.py +0 -0
  166. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/_persistence.py +0 -0
  167. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
  168. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/_theme.py +0 -0
  169. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
  170. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/locales/README.md +0 -0
  171. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/locales/cs.json +0 -0
  172. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/locales/de.json +0 -0
  173. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/locales/en.json +0 -0
  174. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/locales/eo.json +0 -0
  175. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/locales/es.json +0 -0
  176. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/locales/fr.json +0 -0
  177. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/locales/it.json +0 -0
  178. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/locales/ko.json +0 -0
  179. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/locales/nl.json +0 -0
  180. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/locales/pl.json +0 -0
  181. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/locales/ru.json +0 -0
  182. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/locales/sv.json +0 -0
  183. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/locales/tlh.json +0 -0
  184. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/locales/zh-Hans.json +0 -0
  185. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/settings.css +0 -0
  186. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/settings.html +0 -0
  187. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/settings_ui/settings.js +0 -0
  188. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/smoke_test.py +0 -0
  189. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  190. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/strict_bps.py +0 -0
  191. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tool_dispatch.py +0 -0
  192. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/__init__.py +0 -0
  193. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/auto_backup.py +0 -0
  194. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/backup.py +0 -0
  195. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  196. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/best_practice_result.py +0 -0
  197. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/best_practice_variables.py +0 -0
  198. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/blueprint_sources.py +0 -0
  199. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/blueprint_substitute.py +0 -0
  200. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/blueprint_write.py +0 -0
  201. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/bulk_selector.py +0 -0
  202. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/component_config_entries.py +0 -0
  203. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/component_config_reads.py +0 -0
  204. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/component_devices.py +0 -0
  205. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/component_registries.py +0 -0
  206. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/component_registry_lookup.py +0 -0
  207. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/config_entry_flow.py +0 -0
  208. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/config_entry_flow_form.py +0 -0
  209. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/config_entry_flow_menu.py +0 -0
  210. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/config_entry_flow_walker.py +0 -0
  211. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/config_entry_identity.py +0 -0
  212. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/config_entry_reconfigure.py +0 -0
  213. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/config_entry_reload_watch.py +0 -0
  214. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/device_control.py +0 -0
  215. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/error_log_parsing.py +0 -0
  216. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/hacs_registration.py +0 -0
  217. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/helpers.py +0 -0
  218. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/integration_reconfigure.py +0 -0
  219. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/log_common.py +0 -0
  220. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/log_sources.py +0 -0
  221. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/log_sources_fault.py +0 -0
  222. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/log_sources_supervisor.py +0 -0
  223. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/radio/__init__.py +0 -0
  224. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/radio/base.py +0 -0
  225. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/radio/matter.py +0 -0
  226. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/radio/thread.py +0 -0
  227. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/radio/zigbee.py +0 -0
  228. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/radio/zwave.py +0 -0
  229. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/reference_validator.py +0 -0
  230. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/registry.py +0 -0
  231. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/renamed_tool_middleware.py +0 -0
  232. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  233. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  234. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  235. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
  236. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
  237. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  238. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/smart_search/_graph.py +0 -0
  239. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  240. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  241. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  242. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
  243. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_addons.py +0 -0
  244. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_areas.py +0 -0
  245. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_assist_pipeline.py +0 -0
  246. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  247. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  248. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_calendar.py +0 -0
  249. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_camera.py +0 -0
  250. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_categories.py +0 -0
  251. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_code.py +0 -0
  252. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  253. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  254. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  255. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  256. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_dev.py +0 -0
  257. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_energy.py +0 -0
  258. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_entities.py +0 -0
  259. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  260. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_groups.py +0 -0
  261. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_hacs.py +0 -0
  262. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_history.py +0 -0
  263. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_integrations.py +0 -0
  264. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_labels.py +0 -0
  265. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_logs.py +0 -0
  266. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_radio.py +0 -0
  267. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_registry.py +0 -0
  268. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_resources.py +0 -0
  269. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_search.py +0 -0
  270. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_security_policy.py +0 -0
  271. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_service.py +0 -0
  272. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_services.py +0 -0
  273. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_system.py +0 -0
  274. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_themes.py +0 -0
  275. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_todo.py +0 -0
  276. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_traces.py +0 -0
  277. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_updates.py +0 -0
  278. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_utility.py +0 -0
  279. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  280. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  281. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
  282. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/tools_zones.py +0 -0
  283. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/tools/validation_middleware.py +0 -0
  284. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/transforms/__init__.py +0 -0
  285. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/transforms/categorized_search.py +0 -0
  286. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  287. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/transport_security.py +0 -0
  288. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/update_check.py +0 -0
  289. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/utils/__init__.py +0 -0
  290. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/utils/config_hash.py +0 -0
  291. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/utils/config_write_lock.py +0 -0
  292. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/utils/data_paths.py +0 -0
  293. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/utils/device_registry_semantics.py +0 -0
  294. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/utils/domain_handlers.py +0 -0
  295. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/utils/entity_membership.py +0 -0
  296. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  297. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  298. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/utils/operation_manager.py +0 -0
  299. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/utils/python_sandbox.py +0 -0
  300. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/utils/skill_loader.py +0 -0
  301. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/utils/usage_logger.py +0 -0
  302. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/visibility/__init__.py +0 -0
  303. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/visibility/enforcement.py +0 -0
  304. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/visibility/model.py +0 -0
  305. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/visibility/persistence.py +0 -0
  306. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp/visibility/resolver.py +0 -0
  307. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  308. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  309. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  310. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  311. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/tests/__init__.py +0 -0
  312. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/tests/test_constants.py +0 -0
  313. {ha_mcp_dev-8.4.3.dev2602 → ha_mcp_dev-8.4.3.dev2608}/tests/test_env_manager.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 8.4.3.dev2602
3
+ Version: 8.4.3.dev2608
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
@@ -239,7 +239,7 @@ Spend less time configuring, more time enjoying your smart home.
239
239
  |----------|--------------|
240
240
  | **🔍 Search** | Fuzzy entity search, deep config search, system overview |
241
241
  | **🏠 Control** | Any service, bulk device control, real-time states |
242
- | **🔧 Manage** | Automations, scripts, helpers, dashboards, areas, zones, groups, calendars, blueprints |
242
+ | **🔧 Manage** | Automations, scripts, helpers, [dashboards](docs/dashboard-edits.md), areas, zones, groups, calendars, blueprints |
243
243
  | **📊 Monitor** | History, statistics, camera snapshots, automation traces, ZHA devices |
244
244
  | **💾 System** | Backup/restore, updates, apps, device registry |
245
245
  | **🔒 Safety** | Read Only Mode toggle, per-tool enable/disable, tool security policies (user approval), automatic edit backups |
@@ -207,7 +207,7 @@ Spend less time configuring, more time enjoying your smart home.
207
207
  |----------|--------------|
208
208
  | **🔍 Search** | Fuzzy entity search, deep config search, system overview |
209
209
  | **🏠 Control** | Any service, bulk device control, real-time states |
210
- | **🔧 Manage** | Automations, scripts, helpers, dashboards, areas, zones, groups, calendars, blueprints |
210
+ | **🔧 Manage** | Automations, scripts, helpers, [dashboards](docs/dashboard-edits.md), areas, zones, groups, calendars, blueprints |
211
211
  | **📊 Monitor** | History, statistics, camera snapshots, automation traces, ZHA devices |
212
212
  | **💾 System** | Backup/restore, updates, apps, device registry |
213
213
  | **🔒 Safety** | Read Only Mode toggle, per-tool enable/disable, tool security policies (user approval), automatic edit backups |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ha-mcp-dev"
7
- version = "8.4.3.dev2602"
7
+ version = "8.4.3.dev2608"
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
@@ -727,7 +727,8 @@ class HomeAssistantSmartMCPServer:
727
727
  "ha_config_get_automation), or `take_control_of_blueprint` "
728
728
  "to convert a blueprint-backed automation into an editable "
729
729
  "standalone one (the UI's Take control action). Omit "
730
- "`identifier` to create a new automation.\n\n"
730
+ "`identifier` to create a new automation. Reusing an identifier targets "
731
+ "the same automation; changing its alias requires config_hash from a prior read.\n\n"
731
732
  "For schema details, examples, and native-vs-template "
732
733
  "guidance, see ha_get_skill_guide or your locally "
733
734
  "installed skills."
@@ -806,18 +807,22 @@ class HomeAssistantSmartMCPServer:
806
807
  "matched cards surgically. (3) get — no search params "
807
808
  "returns the full Lovelace config plus a stable "
808
809
  "`config_hash`. Use `url_path='default'` for the main "
809
- "dashboard.\n\n"
810
+ "dashboard. For known JSON Pointer paths, use "
811
+ "ha_config_set_dashboard(patch=..., config_hash=...).\n\n"
810
812
  "For card-type taxonomy and search workflow examples, see "
811
813
  "ha_get_skill_guide."
812
814
  ),
813
815
  "ha_config_set_dashboard": (
814
816
  "Create or update a Home Assistant dashboard.\n\n"
815
- "Supports two modes: full `config` replacement (new "
816
- "dashboards or full restructures), or surgical "
817
- "`python_transform` on an existing dashboard (requires "
818
- "`config_hash` from ha_config_get_dashboard; recommended "
819
- "for edits). Use `url_path` of 'default' or 'lovelace' "
820
- "to target the built-in dashboard.\n\n"
817
+ "Three modes: full `config` replacement for new dashboards or "
818
+ "restructures; `patch` for known JSON Pointer paths with literal "
819
+ "values (add/remove/replace/test, up to 100 operations; use `-` "
820
+ "to append to arrays); `python_transform` for loops and pattern-based "
821
+ "edits. Both edit modes require `config_hash` from "
822
+ "ha_config_get_dashboard. Choose one mode for content changes; "
823
+ "omit all three for metadata-only calls. With patch, change "
824
+ "sidebar metadata in a separate call. Use `url_path` of 'default' "
825
+ "or 'lovelace' for the built-in dashboard.\n\n"
821
826
  "For card types, layout patterns, and python_transform "
822
827
  "security rules, see "
823
828
  "ha_get_skill_guide or your locally installed skills."
@@ -67,20 +67,18 @@ DEVICE_REGISTRY_CHILD_SEMANTICS = "device_registry_child_semantics"
67
67
  # time, so a component installed / upgraded mid-session is re-probed and
68
68
  # adopted instead of being pinned to "absent" for the whole process lifetime
69
69
  # (the cache key is the process-lifetime REST client — see ``_CAPS_CACHE``).
70
- # Positive entries never expire on this timer; they are dropped only by
70
+ # Valid positive entries never expire on this timer; they are dropped only by
71
71
  # ``invalidate_caps`` (a supposedly-supported command coming back
72
72
  # ``unknown_command``). This ABSENT-negative window covers a definitive
73
73
  # "component responded, no usable surface" verdict (``unknown_command`` /
74
- # malformed / unsupported schema_version).
74
+ # unsupported schema_version). Inconclusive failures use the short window below.
75
75
  _NEGATIVE_CACHE_TTL_S = 300.0
76
76
 
77
- # A separate, much SHORTER window for a TRANSIENT negative: the probe couldn't
78
- # reach the component at all (WS down / connect failure), which is not a verdict
79
- # about the component, only about the transport. Caching it briefly stops a
80
- # WS-broken install from re-paying the slow connect attempt on every tool call
81
- # (issue #1813 Phase 2, review-5 M8) while still self-healing quickly once the
82
- # socket recovers. Kept short precisely because the transport may come back at
83
- # any moment — unlike the absent-negative, which reflects a stable answer.
77
+ # A separate, SHORTER window for inconclusive discovery: transport failures,
78
+ # info handler errors, unexpected faults, and malformed replies. Caching these
79
+ # briefly avoids repeating a broken probe on every tool call (issue #1813 Phase 2,
80
+ # review-5 M8), while allowing strict writers to recover promptly after discovery
81
+ # works again. These failures do not establish that a capability is absent.
84
82
  _TRANSIENT_NEGATIVE_CACHE_TTL_S = 30.0
85
83
 
86
84
 
@@ -121,29 +119,47 @@ class ComponentCaps:
121
119
  # Weak-keyed by client so the negotiated caps self-evict when the client is
122
120
  # garbage-collected. The key is the process-lifetime REST client, NOT the WS
123
121
  # connection: an HA restart drops the socket but reuses the same pooled client,
124
- # so a positive entry persists for the whole process (dropped only by
122
+ # so a valid positive entry persists for the whole process (dropped only by
125
123
  # ``invalidate_caps`` on an ``unknown_command``). A ``None`` value is a cached
126
124
  # *negative* ("probed, no usable WS surface"); absence means "not yet probed".
127
125
  # A negative carries an expiry in EXACTLY ONE of the two timestamp maps below —
128
126
  # ``_NEGATIVE_CACHE_TS`` (definitive absent, long) or ``_TRANSIENT_NEGATIVE_TS``
129
- # (transport-only, short) — so a component installed/upgraded mid-session, or one
127
+ # (inconclusive failure, short) — so a component installed/upgraded mid-session, or one
130
128
  # that just became reachable again, is eventually re-probed instead of pinned.
131
129
  _CAPS_CACHE: weakref.WeakKeyDictionary[Any, ComponentCaps | None] = (
132
130
  weakref.WeakKeyDictionary()
133
131
  )
134
132
  # Monotonic timestamp a definitive ABSENT-negative was stored, keyed by the same
135
- # client. Only populated for ``None`` values; positive entries never appear here.
133
+ # client. Inconclusive failures never use this window.
136
134
  _NEGATIVE_CACHE_TS: weakref.WeakKeyDictionary[Any, float] = weakref.WeakKeyDictionary()
137
- # Monotonic timestamp a TRANSIENT (transport-failure) negative was stored. Kept
135
+ # Monotonic timestamp an inconclusive probe failure was stored. Also expires
136
+ # malformed snapshots retained for permissive readers. Kept
138
137
  # separate from ``_NEGATIVE_CACHE_TS`` so the two negative kinds carry different
139
138
  # TTLs; a client is stamped in at most one of the two at a time (the store helpers
140
139
  # clear the other).
141
140
  _TRANSIENT_NEGATIVE_TS: weakref.WeakKeyDictionary[Any, float] = (
142
141
  weakref.WeakKeyDictionary()
143
142
  )
143
+ # Preserve failed discovery separately so write callers can refuse a downgrade
144
+ # even when a best-effort read populated the negative cache first. Store only
145
+ # text, not exceptions whose traceback would retain clients through weak keys.
146
+ _PROBE_FAILURES: weakref.WeakKeyDictionary[Any, str] = weakref.WeakKeyDictionary()
144
147
  _CAPS_LOCKS: weakref.WeakKeyDictionary[Any, asyncio.Lock] = weakref.WeakKeyDictionary()
145
148
 
146
149
 
150
+ class ComponentDiscoveryError(RuntimeError):
151
+ """Discovery failed without establishing which component commands are usable."""
152
+
153
+
154
+ def _checked_caps(
155
+ client: Any, caps: ComponentCaps | None, strict: bool
156
+ ) -> ComponentCaps | None:
157
+ """Keep read fallbacks while exposing failed discovery to write callers."""
158
+ if strict and (failure := _PROBE_FAILURES.get(client)):
159
+ raise ComponentDiscoveryError(failure)
160
+ return caps
161
+
162
+
147
163
  def _get_caps_lock(client: Any) -> asyncio.Lock:
148
164
  """Per-client lock so concurrent first-callers probe ``info`` exactly once."""
149
165
  lock = _CAPS_LOCKS.get(client)
@@ -156,28 +172,29 @@ def _get_caps_lock(client: Any) -> asyncio.Lock:
156
172
  def _live_cache_entry(client: Any) -> tuple[bool, ComponentCaps | None]:
157
173
  """Return ``(hit, caps)`` for a still-valid cache entry, else ``(False, None)``.
158
174
 
159
- A positive entry is a hit for the process lifetime. A negative (``None``)
175
+ A valid positive entry is a hit for the process lifetime. Malformed replies
176
+ retained for permissive readers expire like transient failures. A negative (``None``)
160
177
  entry is a hit only within its window of when it was stored (monotonic clock):
161
178
  ``_NEGATIVE_CACHE_TTL_S`` for a definitive absent-negative, the shorter
162
- ``_TRANSIENT_NEGATIVE_CACHE_TTL_S`` for a transport-failure negative. Once the
179
+ ``_TRANSIENT_NEGATIVE_CACHE_TTL_S`` for an inconclusive failure. Once the
163
180
  relevant window lapses it reports a miss so the caller re-probes and can adopt a
164
181
  component that appeared — or became reachable — mid-session.
165
182
  """
166
183
  if client not in _CAPS_CACHE:
167
184
  return False, None
168
185
  cached = _CAPS_CACHE[client]
169
- if cached is not None:
186
+ if cached is not None and client not in _PROBE_FAILURES:
170
187
  return True, cached
171
188
  now = _monotonic()
172
189
  stored_at = _NEGATIVE_CACHE_TS.get(client)
173
190
  if stored_at is not None and (now - stored_at) < _NEGATIVE_CACHE_TTL_S:
174
- return True, None
191
+ return True, cached
175
192
  transient_at = _TRANSIENT_NEGATIVE_TS.get(client)
176
193
  if (
177
194
  transient_at is not None
178
195
  and (now - transient_at) < _TRANSIENT_NEGATIVE_CACHE_TTL_S
179
196
  ):
180
- return True, None
197
+ return True, cached
181
198
  return False, None
182
199
 
183
200
 
@@ -185,9 +202,10 @@ def _store_caps(client: Any, caps: ComponentCaps | None) -> None:
185
202
  """Cache a positive result or a definitive ABSENT-negative for ``client``.
186
203
 
187
204
  A ``None`` here is the long-window absent-negative (``info`` responded
188
- ``unknown_command`` / malformed / unsupported schema). Clears any prior
205
+ ``unknown_command`` / unsupported schema). Clears any prior
189
206
  transient-negative stamp so a client never carries both negative kinds.
190
207
  """
208
+ _PROBE_FAILURES.pop(client, None)
191
209
  _CAPS_CACHE[client] = caps
192
210
  _TRANSIENT_NEGATIVE_TS.pop(client, None)
193
211
  if caps is None:
@@ -196,17 +214,16 @@ def _store_caps(client: Any, caps: ComponentCaps | None) -> None:
196
214
  _NEGATIVE_CACHE_TS.pop(client, None)
197
215
 
198
216
 
199
- def _store_transient_negative(client: Any) -> None:
200
- """Cache a SHORT transient negative after a transport-failure probe.
217
+ def _store_transient_negative(client: Any, caps: ComponentCaps | None = None) -> None:
218
+ """Cache an inconclusive probe for the short recovery window.
201
219
 
202
- The probe could not reach the component (WS down / connect failure), so cache
203
- ``None`` with the short ``_TRANSIENT_NEGATIVE_CACHE_TTL_S`` window: repeated
204
- calls inside it skip re-paying the slow connect and go straight to legacy
205
- (issue #1813 Phase 2, review-5 M8), then the next call past the window re-probes
206
- and adopts a now-reachable component (self-healing). Clears any prior
207
- absent-negative stamp so a client never carries both negative kinds.
220
+ Read callers retain legacy fallback or a permissively parsed snapshot.
221
+ Strict writers reject the accompanying probe failure until the next probe
222
+ succeeds. Preserve parsed capabilities from malformed replies for readers,
223
+ while expiring them on the same short timer as failed probes. Clear any
224
+ prior absent-negative stamp so a client never carries both cache windows.
208
225
  """
209
- _CAPS_CACHE[client] = None
226
+ _CAPS_CACHE[client] = caps
210
227
  _NEGATIVE_CACHE_TS.pop(client, None)
211
228
  _TRANSIENT_NEGATIVE_TS[client] = _monotonic()
212
229
 
@@ -214,8 +231,8 @@ def _store_transient_negative(client: Any) -> None:
214
231
  def _parse_caps(response: Any) -> ComponentCaps | None:
215
232
  """Map an ``ha_mcp_tools/info`` response into ``ComponentCaps``.
216
233
 
217
- Returns ``None`` for a malformed payload — the command responded, so this
218
- is still a stable negative worth caching, just not a usable capability set.
234
+ Parse permissively for existing read callers. A missing result returns
235
+ ``None``; strict callers separately reject malformed discovery.
219
236
  """
220
237
  result = response.get("result") if isinstance(response, dict) else None
221
238
  if not isinstance(result, dict):
@@ -249,17 +266,25 @@ def _parse_caps(response: Any) -> ComponentCaps | None:
249
266
  )
250
267
 
251
268
 
252
- async def get_component_caps(client: Any) -> ComponentCaps | None:
269
+ async def get_component_caps(
270
+ client: Any, *, strict: bool = False
271
+ ) -> ComponentCaps | None:
253
272
  """Return the cached (or freshly probed) capabilities of the component.
254
273
 
255
274
  One ``ha_mcp_tools/info`` probe per client, cached. ``None`` means "no
256
275
  usable component WS surface" — the caller falls back to its legacy path.
257
276
 
277
+ With ``strict=True``, failed or malformed discovery raises
278
+ ``ComponentDiscoveryError``, including failures cached by earlier read tools.
279
+ A definitive unknown command, unsupported schema, or valid capability list
280
+ without the requested command still permits legacy routing. The default
281
+ retains best-effort read behavior. Inconclusive failures expire after 30
282
+ seconds for both readers and writers.
283
+
258
284
  Cache-on-failure semantics follow the error taxonomy:
259
285
 
260
- - ``HomeAssistantCommandError`` (``info`` is ``unknown_command`` on an old
261
- component, or the handler raised): cache ``None`` with an expiry. The
262
- negative is re-probed after ``_NEGATIVE_CACHE_TTL_S`` so a component
286
+ - ``HomeAssistantCommandError`` with ``unknown_command`` (old or absent
287
+ component): cache ``None`` with an expiry. The negative is re-probed after ``_NEGATIVE_CACHE_TTL_S`` so a component
263
288
  installed / upgraded mid-session (the REST client — the cache key — is not
264
289
  recreated on an HA restart) is eventually adopted instead of pinned absent.
265
290
  - ``HomeAssistantConnectionError`` / ``HomeAssistantCommandTimeout`` (WS
@@ -268,12 +293,12 @@ async def get_component_caps(client: Any) -> ComponentCaps | None:
268
293
  negative (``_TRANSIENT_NEGATIVE_CACHE_TTL_S``) so repeated calls on a
269
294
  WS-broken install skip the slow connect and go straight to legacy, then
270
295
  re-probe once the window lapses (self-healing). The consuming tool's legacy
271
- path serves the request meanwhile.
296
+ path serves read requests meanwhile; strict writers refuse the edit.
272
297
  - No credentials on the client (a bare test double with no ``base_url`` /
273
298
  ``token``): nothing to probe; return ``None`` without caching.
274
- - Any other unexpected exception: logged at debug, cached as the same short
275
- transient negative (dominated by the connect-establishment failure above; a
276
- genuine runtime fault re-probes after the window).
299
+ - Other command errors, malformed replies, or unexpected exceptions: cache
300
+ for the same short transient window. None establishes capability absence;
301
+ strict writers refuse the edit until a subsequent probe succeeds.
277
302
 
278
303
  A probe whose ``schema_version`` is not ``SUPPORTED_SCHEMA_VERSION`` is
279
304
  treated as no-caps (cached negative, logged once): the server can't trust
@@ -281,12 +306,12 @@ async def get_component_caps(client: Any) -> ComponentCaps | None:
281
306
  """
282
307
  hit, caps = _live_cache_entry(client)
283
308
  if hit:
284
- return caps
309
+ return _checked_caps(client, caps, strict)
285
310
 
286
311
  async with _get_caps_lock(client):
287
312
  hit, caps = _live_cache_entry(client)
288
313
  if hit:
289
- return caps
314
+ return _checked_caps(client, caps, strict)
290
315
 
291
316
  base_url = getattr(client, "base_url", None)
292
317
  token = getattr(client, "token", None)
@@ -305,23 +330,29 @@ async def get_component_caps(client: Any) -> ComponentCaps | None:
305
330
  url=base_url, token=token, verify_ssl=verify_ssl
306
331
  )
307
332
  response = await ws.send_command(INFO_COMMAND)
308
- except HomeAssistantCommandError:
309
- # Old component (unknown_command) or an info handler bug: the
310
- # component can't serve WS commands. Cache the negative.
311
- _store_caps(client, None)
312
- return None
333
+ except HomeAssistantCommandError as exc:
334
+ if is_unknown_command(exc):
335
+ _store_caps(client, None)
336
+ else:
337
+ _store_transient_negative(client)
338
+ _PROBE_FAILURES[client] = f"{INFO_COMMAND} failed: {exc}"
339
+ return _checked_caps(client, None, strict)
313
340
  except (HomeAssistantConnectionError, HomeAssistantCommandTimeout):
314
341
  logger.debug(
315
342
  "%s probe skipped: WS unavailable", INFO_COMMAND, exc_info=True
316
343
  )
317
344
  _store_transient_negative(client)
318
- return None
345
+ _PROBE_FAILURES[client] = (
346
+ f"{INFO_COMMAND} probe could not reach Home Assistant"
347
+ )
348
+ return _checked_caps(client, None, strict)
319
349
  except Exception:
320
- # Broad on purpose: the probe must never promote an unexpected
321
- # fault into a tool failure. Short transient cache, self-healing.
350
+ # Read callers retain best-effort fallback; strict write callers
351
+ # must not confuse an unexpected fault with capability absence.
322
352
  logger.debug("%s probe failed unexpectedly", INFO_COMMAND, exc_info=True)
323
353
  _store_transient_negative(client)
324
- return None
354
+ _PROBE_FAILURES[client] = f"{INFO_COMMAND} probe failed unexpectedly"
355
+ return _checked_caps(client, None, strict)
325
356
 
326
357
  caps = _parse_caps(response)
327
358
  if caps is not None and caps.schema_version != SUPPORTED_SCHEMA_VERSION:
@@ -333,7 +364,20 @@ async def get_component_caps(client: Any) -> ComponentCaps | None:
333
364
  )
334
365
  caps = None
335
366
  _store_caps(client, caps)
336
- return caps
367
+ result = response.get("result") if isinstance(response, dict) else None
368
+ if (
369
+ not isinstance(response, dict)
370
+ or response.get("success") is not True
371
+ or not isinstance(result, dict)
372
+ or type(result.get("schema_version")) is not int
373
+ or not isinstance(result.get("capabilities"), list)
374
+ or not all(isinstance(cap, str) for cap in result["capabilities"])
375
+ ):
376
+ # Keep permissive parsing for existing read callers, but expire
377
+ # malformed positive snapshots too so strict callers can recover.
378
+ _store_transient_negative(client, caps)
379
+ _PROBE_FAILURES[client] = f"Malformed {INFO_COMMAND} response"
380
+ return _checked_caps(client, caps, strict)
337
381
 
338
382
 
339
383
  def component_supports(caps: ComponentCaps | None, capability: str) -> bool:
@@ -358,6 +402,7 @@ def invalidate_caps(client: Any) -> None:
358
402
  ``unknown_command`` (e.g. the component was downgraded mid-session), so the
359
403
  stale positive doesn't keep routing to a dead command.
360
404
  """
405
+ _PROBE_FAILURES.pop(client, None)
361
406
  _CAPS_CACHE.pop(client, None)
362
407
  _NEGATIVE_CACHE_TS.pop(client, None)
363
408
  _TRANSIENT_NEGATIVE_TS.pop(client, None)
@@ -0,0 +1,169 @@
1
+ """Capability-negotiated dashboard writes with no retry after a sent command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ from typing import Any, NoReturn
8
+
9
+ from ..client.rest_client import HomeAssistantCommandError, HomeAssistantCommandNotSent
10
+ from ..client.websocket_client import get_websocket_client
11
+ from .component_api import (
12
+ component_supports,
13
+ get_component_caps,
14
+ invalidate_caps,
15
+ is_unknown_command,
16
+ )
17
+ from .dashboard_edit_errors import raise_dashboard_edit_error
18
+
19
+ WS_DASHBOARD_EDIT = "ha_mcp_tools/dashboard_edit"
20
+ _LOGGER = logging.getLogger(__name__)
21
+
22
+
23
+ def _invalid_success_fields(result: dict[str, Any]) -> list[str]:
24
+ """Identify invalid consumed fields without copying dashboard values into logs."""
25
+ verified = result.get("post_write_verified")
26
+ config_hash = result.get("config_hash")
27
+ size = result.get("previous_config_size")
28
+ warnings = result.get("warnings", [])
29
+ checks = {
30
+ "success": result.get("success") is True,
31
+ "config": isinstance(result.get("config"), dict),
32
+ "config_hash": "config_hash" in result
33
+ and (isinstance(config_hash, str) if verified else config_hash is None),
34
+ "post_write_verified": isinstance(verified, bool),
35
+ "write_committed": isinstance(result.get("write_committed"), bool),
36
+ "previous_config_size": isinstance(size, int)
37
+ and not isinstance(size, bool)
38
+ and size >= 0,
39
+ "warnings": isinstance(warnings, list)
40
+ and all(isinstance(warning, str) for warning in warnings),
41
+ "unchanged": isinstance(result.get("unchanged", False), bool),
42
+ }
43
+ return [name for name, valid in checks.items() if not valid]
44
+
45
+
46
+ def _raise_malformed_reply(
47
+ url_path: str | None, action: str, fields: list[str]
48
+ ) -> NoReturn:
49
+ """Diagnose protocol skew without logging the config, templates, or card values."""
50
+ details = ", ".join(fields)
51
+ _LOGGER.warning("Malformed dashboard_edit response; invalid fields: %s", details)
52
+ raise_dashboard_edit_error(
53
+ url_path,
54
+ "write_outcome_unknown",
55
+ f"Malformed response fields: {details}",
56
+ None,
57
+ action,
58
+ )
59
+
60
+
61
+ def _validate_edit_result(
62
+ raw: Any, url_path: str | None, action: str, *, hash_supplied: bool = False
63
+ ) -> dict[str, Any]:
64
+ """An incomplete response cannot establish whether the write happened."""
65
+ if not isinstance(raw, dict):
66
+ _raise_malformed_reply(url_path, action, ["envelope"])
67
+ if raw.get("success") is not True:
68
+ _raise_malformed_reply(url_path, action, ["envelope.success"])
69
+ result = raw.get("result")
70
+ if not isinstance(result, dict):
71
+ _raise_malformed_reply(url_path, action, ["result"])
72
+ if result.get("success") is False:
73
+ error = result.get("error")
74
+ if not isinstance(error, dict) or not isinstance(error.get("code"), str):
75
+ _raise_malformed_reply(url_path, action, ["error.code"])
76
+ committed = result.get("write_committed")
77
+ if committed is not False and committed is not None:
78
+ _raise_malformed_reply(url_path, action, ["write_committed"])
79
+ raise_dashboard_edit_error(
80
+ url_path,
81
+ error["code"],
82
+ str(error.get("message", error["code"])),
83
+ committed,
84
+ action,
85
+ hash_supplied=hash_supplied,
86
+ )
87
+ invalid_fields = _invalid_success_fields(result)
88
+ if invalid_fields:
89
+ _raise_malformed_reply(url_path, action, invalid_fields)
90
+ return result
91
+
92
+
93
+ def _handle_command_failure(
94
+ exc: BaseException, client: Any, url_path: str | None, action: str
95
+ ) -> None:
96
+ """Only a missing command allows fallback; other failures terminate the edit."""
97
+ if isinstance(exc, HomeAssistantCommandNotSent):
98
+ raise_dashboard_edit_error(url_path, "write_not_sent", str(exc), False, action)
99
+ if isinstance(exc, Exception) and is_unknown_command(exc):
100
+ invalidate_caps(client)
101
+ return
102
+ # Core's schema/admin gates reject these before this command's prep runs.
103
+ # Other command errors can originate after dispatch and remain ambiguous.
104
+ if isinstance(exc, HomeAssistantCommandError) and exc.code in {
105
+ "unauthorized",
106
+ "invalid_format",
107
+ }:
108
+ raise_dashboard_edit_error(url_path, exc.code, str(exc), False, action)
109
+ _LOGGER.warning("Dashboard edit command failed after dispatch", exc_info=exc)
110
+ raise_dashboard_edit_error(
111
+ url_path,
112
+ "write_outcome_unknown",
113
+ str(exc) or "Dashboard edit interrupted before its response was received",
114
+ None,
115
+ action,
116
+ )
117
+
118
+
119
+ async def edit_dashboard_via_component(
120
+ client: Any,
121
+ url_path: str | None,
122
+ *,
123
+ action: str = "set",
124
+ expected_hash: str | None = None,
125
+ config: dict[str, Any] | None = None,
126
+ patch: list[dict[str, Any]] | None = None,
127
+ ) -> dict[str, Any] | None:
128
+ """Return an authoritative edit result, or None for an unavailable command.
129
+
130
+ Legacy fallback requires confirmed absence of a compatible capability or
131
+ HA's definitive unknown_command. Never use the REST client's retrying
132
+ WebSocket bridge for a native write.
133
+ ``action`` labels local error responses only; it is not a command argument.
134
+ """
135
+ # Clients without connection credentials cannot probe the component.
136
+ if not getattr(client, "base_url", None) or not getattr(client, "token", None):
137
+ return None
138
+ try:
139
+ caps = await get_component_caps(client, strict=True)
140
+ if not component_supports(caps, "dashboard_edit"):
141
+ return None
142
+ ws = await get_websocket_client(
143
+ url=client.base_url,
144
+ token=client.token,
145
+ verify_ssl=getattr(client, "verify_ssl", None),
146
+ )
147
+ except Exception as exc:
148
+ _LOGGER.warning(
149
+ "Unable to prepare the dashboard edit connection", exc_info=True
150
+ )
151
+ raise_dashboard_edit_error(url_path, "load_failed", str(exc), False, action)
152
+
153
+ kwargs: dict[str, Any] = {"url_path": url_path}
154
+ if expected_hash is not None:
155
+ kwargs["expected_hash"] = expected_hash
156
+ if config is not None:
157
+ kwargs["config"] = config
158
+ if patch is not None:
159
+ kwargs["patch"] = patch
160
+ try:
161
+ raw = await ws.send_command(WS_DASHBOARD_EDIT, **kwargs)
162
+ except (asyncio.CancelledError, Exception) as exc:
163
+ # Pre-dispatch cancellation still propagates; after dispatch it cannot
164
+ # establish whether Core saved unless an explicit rejection says so.
165
+ _handle_command_failure(exc, client, url_path, action)
166
+ return None
167
+ return _validate_edit_result(
168
+ raw, url_path, action, hash_supplied=expected_hash is not None
169
+ )