ha-mcp-dev 8.1.1.dev2289__tar.gz → 8.1.1.dev2292__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 (263) hide show
  1. {ha_mcp_dev-8.1.1.dev2289/src/ha_mcp_dev.egg-info → ha_mcp_dev-8.1.1.dev2292}/PKG-INFO +1 -1
  2. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/pyproject.toml +1 -1
  3. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/backup_manager.py +5 -4
  4. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_config_dashboards.py +237 -84
  5. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  6. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/LICENSE +0 -0
  7. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/MANIFEST.in +0 -0
  8. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/README.md +0 -0
  9. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/setup.cfg +0 -0
  10. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/__init__.py +0 -0
  11. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/__main__.py +0 -0
  12. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_pypi_marker +0 -0
  13. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/__init__.py +0 -0
  14. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/requirements.txt +0 -0
  15. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/LICENSE +0 -0
  16. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/MANIFEST.sha256 +0 -0
  17. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/VENDORED +0 -0
  18. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/__init__.py +0 -0
  19. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/__main__.py +0 -0
  20. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/asyncio/__init__.py +0 -0
  21. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/asyncio/client.py +0 -0
  22. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/asyncio/connection.py +0 -0
  23. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/asyncio/messages.py +0 -0
  24. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/asyncio/router.py +0 -0
  25. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/asyncio/server.py +0 -0
  26. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/auth.py +0 -0
  27. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/cli.py +0 -0
  28. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/client.py +0 -0
  29. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/connection.py +0 -0
  30. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/datastructures.py +0 -0
  31. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/exceptions.py +0 -0
  32. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/extensions/__init__.py +0 -0
  33. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/extensions/base.py +0 -0
  34. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/extensions/permessage_deflate.py +0 -0
  35. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/frames.py +0 -0
  36. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/headers.py +0 -0
  37. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/http11.py +0 -0
  38. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/imports.py +0 -0
  39. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/legacy/__init__.py +0 -0
  40. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/legacy/auth.py +0 -0
  41. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/legacy/client.py +0 -0
  42. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/legacy/exceptions.py +0 -0
  43. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/legacy/framing.py +0 -0
  44. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/legacy/handshake.py +0 -0
  45. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/legacy/http.py +0 -0
  46. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/legacy/protocol.py +0 -0
  47. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/legacy/server.py +0 -0
  48. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/protocol.py +0 -0
  49. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/proxy.py +0 -0
  50. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/py.typed +0 -0
  51. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/server.py +0 -0
  52. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/streams.py +0 -0
  53. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/sync/__init__.py +0 -0
  54. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/sync/client.py +0 -0
  55. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/sync/connection.py +0 -0
  56. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/sync/messages.py +0 -0
  57. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/sync/router.py +0 -0
  58. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/sync/server.py +0 -0
  59. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/sync/utils.py +0 -0
  60. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/trio/__init__.py +0 -0
  61. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/trio/client.py +0 -0
  62. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/trio/connection.py +0 -0
  63. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/trio/messages.py +0 -0
  64. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/trio/router.py +0 -0
  65. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/trio/server.py +0 -0
  66. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/trio/utils.py +0 -0
  67. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/typing.py +0 -0
  68. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/uri.py +0 -0
  69. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/utils.py +0 -0
  70. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_vendor/websockets/version.py +0 -0
  71. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/_version.py +0 -0
  72. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/auth/__init__.py +0 -0
  73. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/auth/consent_form.py +0 -0
  74. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/auth/provider.py +0 -0
  75. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/browser_landing.py +0 -0
  76. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/client/__init__.py +0 -0
  77. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/client/rest_client.py +0 -0
  78. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/client/supervisor_client.py +0 -0
  79. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/client/websocket_client.py +0 -0
  80. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/client/websocket_listener.py +0 -0
  81. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/config.py +0 -0
  82. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  83. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  84. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
  85. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
  86. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  87. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/dashboard_screenshot/theme_guard.py +0 -0
  88. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/errors.py +0 -0
  89. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/hacs_auto_refresh.py +0 -0
  90. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/llm_exposure.py +0 -0
  91. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/policy/__init__.py +0 -0
  92. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/policy/approval_queue.py +0 -0
  93. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/policy/editing.py +0 -0
  94. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/policy/evaluator.py +0 -0
  95. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/policy/handlers.py +0 -0
  96. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/policy/middleware.py +0 -0
  97. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/policy/model.py +0 -0
  98. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/policy/persistence.py +0 -0
  99. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/policy/value_sources.py +0 -0
  100. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/py.typed +0 -0
  101. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/read_only.py +0 -0
  102. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/redaction.py +0 -0
  103. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  104. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  105. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  106. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  107. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  108. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  109. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  110. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  111. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  112. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  113. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  114. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  115. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
  116. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  117. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
  118. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  119. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  120. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  121. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  122. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  123. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  124. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  125. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
  126. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  127. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  128. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/server.py +0 -0
  129. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/__init__.py +0 -0
  130. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
  131. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
  132. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
  133. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
  134. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
  135. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
  136. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/_i18n.py +0 -0
  137. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/_persistence.py +0 -0
  138. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
  139. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/_theme.py +0 -0
  140. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
  141. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/locales/README.md +0 -0
  142. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/locales/de.json +0 -0
  143. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/locales/en.json +0 -0
  144. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/locales/es.json +0 -0
  145. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/locales/fr.json +0 -0
  146. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/locales/it.json +0 -0
  147. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/locales/nl.json +0 -0
  148. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/locales/pl.json +0 -0
  149. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/locales/ru.json +0 -0
  150. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/locales/sv.json +0 -0
  151. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/locales/zh-Hans.json +0 -0
  152. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/settings.css +0 -0
  153. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/settings.html +0 -0
  154. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/settings_ui/settings.js +0 -0
  155. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/smoke_test.py +0 -0
  156. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  157. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/strict_bps.py +0 -0
  158. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/__init__.py +0 -0
  159. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/auto_backup.py +0 -0
  160. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/backup.py +0 -0
  161. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  162. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/best_practice_result.py +0 -0
  163. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/best_practice_variables.py +0 -0
  164. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/component_api.py +0 -0
  165. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/component_config_reads.py +0 -0
  166. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/component_devices.py +0 -0
  167. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/component_registries.py +0 -0
  168. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/component_registry_lookup.py +0 -0
  169. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/config_entry_flow.py +0 -0
  170. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/config_entry_flow_form.py +0 -0
  171. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/config_entry_flow_menu.py +0 -0
  172. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/config_entry_flow_walker.py +0 -0
  173. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/device_control.py +0 -0
  174. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/hacs_registration.py +0 -0
  175. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/helpers.py +0 -0
  176. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/radio/__init__.py +0 -0
  177. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/radio/base.py +0 -0
  178. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/radio/matter.py +0 -0
  179. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/radio/thread.py +0 -0
  180. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/radio/zigbee.py +0 -0
  181. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/radio/zwave.py +0 -0
  182. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/reference_validator.py +0 -0
  183. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/registry.py +0 -0
  184. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  185. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  186. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  187. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
  188. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
  189. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  190. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  191. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  192. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  193. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
  194. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_addons.py +0 -0
  195. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_areas.py +0 -0
  196. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  197. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  198. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_calendar.py +0 -0
  199. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_camera.py +0 -0
  200. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_categories.py +0 -0
  201. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_code.py +0 -0
  202. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  203. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  204. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  205. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  206. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  207. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_dev.py +0 -0
  208. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_energy.py +0 -0
  209. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_entities.py +0 -0
  210. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  211. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_groups.py +0 -0
  212. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_hacs.py +0 -0
  213. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_history.py +0 -0
  214. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_integrations.py +0 -0
  215. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_labels.py +0 -0
  216. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_radio.py +0 -0
  217. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_registry.py +0 -0
  218. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_resources.py +0 -0
  219. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_search.py +0 -0
  220. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_security_policy.py +0 -0
  221. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_service.py +0 -0
  222. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_services.py +0 -0
  223. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_system.py +0 -0
  224. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_themes.py +0 -0
  225. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_todo.py +0 -0
  226. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_traces.py +0 -0
  227. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_updates.py +0 -0
  228. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_utility.py +0 -0
  229. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  230. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  231. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
  232. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/tools_zones.py +0 -0
  233. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/util_helpers.py +0 -0
  234. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/tools/validation_middleware.py +0 -0
  235. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/transforms/__init__.py +0 -0
  236. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/transforms/categorized_search.py +0 -0
  237. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  238. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/transport_security.py +0 -0
  239. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/update_check.py +0 -0
  240. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/utils/__init__.py +0 -0
  241. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/utils/config_hash.py +0 -0
  242. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/utils/config_write_lock.py +0 -0
  243. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/utils/data_paths.py +0 -0
  244. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/utils/domain_handlers.py +0 -0
  245. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  246. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  247. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/utils/operation_manager.py +0 -0
  248. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/utils/python_sandbox.py +0 -0
  249. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/utils/skill_loader.py +0 -0
  250. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/utils/usage_logger.py +0 -0
  251. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/visibility/__init__.py +0 -0
  252. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/visibility/enforcement.py +0 -0
  253. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/visibility/model.py +0 -0
  254. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/visibility/persistence.py +0 -0
  255. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp/visibility/resolver.py +0 -0
  256. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  257. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  258. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  259. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  260. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  261. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/tests/__init__.py +0 -0
  262. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/tests/test_constants.py +0 -0
  263. {ha_mcp_dev-8.1.1.dev2289 → ha_mcp_dev-8.1.1.dev2292}/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.1.1.dev2289
3
+ Version: 8.1.1.dev2292
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.1.1.dev2289"
7
+ version = "8.1.1.dev2292"
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
@@ -1441,10 +1441,11 @@ async def _fetch_dashboard(client: Any, entity_id: str) -> Any:
1441
1441
  try:
1442
1442
  config, _config_hash = await _get_dashboard_config_internal(client, fetch_path)
1443
1443
  except ToolError as err:
1444
- # ToolError carries the structured failure payload; treat a
1445
- # missing/unknown dashboard as "nothing to back up" (also covers a
1446
- # brand-new dashboard on the create path). "Unknown config
1447
- # specified" is HA's message for an unresolved url_path.
1444
+ # ToolError carries the structured failure payload, including HA's
1445
+ # preserved ``config_not_found`` code; treat a missing/unknown
1446
+ # dashboard as "nothing to back up" (also covers a brand-new dashboard
1447
+ # on the create path). "Unknown config specified" is HA's message for
1448
+ # an unresolved url_path.
1448
1449
  msg = str(err).lower()
1449
1450
  if "not_found" in msg or "config_not_found" in msg or "unknown config" in msg:
1450
1451
  return None
@@ -8,7 +8,7 @@ import json
8
8
  import logging
9
9
  import re
10
10
  from dataclasses import dataclass, replace
11
- from typing import Annotated, Any, Literal, cast, overload
11
+ from typing import Annotated, Any, Literal, NoReturn, cast, overload
12
12
 
13
13
  from fastmcp.exceptions import ToolError
14
14
  from fastmcp.tools import tool
@@ -40,7 +40,7 @@ from ..dashboard_screenshot.paths import (
40
40
  match_dashboard_view,
41
41
  resolve_dashboard_view,
42
42
  )
43
- from ..errors import ErrorCode, create_error_response
43
+ from ..errors import ErrorCode, create_error_response, get_error_code, get_error_message
44
44
  from ..strict_bps import BestPracticeKeyParam
45
45
  from ..utils.config_hash import compute_config_hash
46
46
  from ..utils.python_sandbox import (
@@ -134,8 +134,9 @@ async def _get_dashboard_config_internal(
134
134
  ``tools_config_scenes.py``).
135
135
 
136
136
  Raises ``ToolError`` with ``ErrorCode.SERVICE_CALL_FAILED`` if the
137
- WebSocket call reports failure or the response is not a dict; callers
138
- can rely on the returned tuple being populated.
137
+ WebSocket call reports failure or the response is not a dict. HA's
138
+ upstream error code is preserved as ``ha_error_code`` when present, so
139
+ callers can distinguish confirmed absence from an unverifiable read.
139
140
  """
140
141
  get_data: dict[str, Any] = {"type": "lovelace/config", "force": True}
141
142
  if url_path:
@@ -144,14 +145,20 @@ async def _get_dashboard_config_internal(
144
145
  response = await client.send_websocket_message(get_data)
145
146
 
146
147
  if isinstance(response, dict) and not response.get("success", True):
147
- error_msg = response.get("error", {})
148
- if isinstance(error_msg, dict):
149
- error_msg = error_msg.get("message", str(error_msg))
148
+ error_msg = get_error_message(response)
149
+ if error_msg is None:
150
+ error_msg = str(response.get("error", {}))
151
+ ha_error_code = response.get("error_code") or get_error_code(response)
152
+ error_context: dict[str, Any] = {"url_path": url_path}
153
+ if ha_error_code is not None:
154
+ # Preserve HA's code so downstream safety decisions do not have to
155
+ # infer an absent config from a multiply-prefixed message alone.
156
+ error_context["ha_error_code"] = str(ha_error_code)
150
157
  raise_tool_error(
151
158
  create_error_response(
152
159
  ErrorCode.SERVICE_CALL_FAILED,
153
160
  f"Dashboard fetch failed: {error_msg}",
154
- context={"url_path": url_path},
161
+ context=error_context,
155
162
  )
156
163
  )
157
164
 
@@ -772,12 +779,39 @@ def _card_matches(
772
779
  # regresses silently to never firing — re-verify with major HA upgrades.
773
780
  _LAZY_RESOLVE_TRIGGER = "Unknown config specified"
774
781
 
782
+ # Exact ``lovelace/config`` messages meaning that a storage dashboard exists
783
+ # but has not been given a config yet. Mirrors smart_search/_deep.py and stays
784
+ # deliberately narrower than ``_LAZY_RESOLVE_TRIGGER``: an unknown path or any
785
+ # other pre-read failure must remain fail-closed before a full replacement.
786
+ _NO_STORED_CONFIG_MESSAGES = frozenset(
787
+ {"No config found.", "Command failed: No config found."}
788
+ )
789
+ _DASHBOARD_FETCH_FAILED_PREFIX = "Dashboard fetch failed: "
790
+ _HA_CONFIG_NOT_FOUND_CODE = "config_not_found"
791
+
775
792
 
776
793
  def _should_lazy_resolve(error_msg: str) -> bool:
777
794
  """Return True if a WS error message indicates the identifier needs resolving."""
778
795
  return _LAZY_RESOLVE_TRIGGER in error_msg
779
796
 
780
797
 
798
+ def _is_no_stored_dashboard_config_error(exc: ToolError) -> bool:
799
+ """Return whether HA confirms that an existing dashboard has no config."""
800
+ try:
801
+ error_data = json.loads(str(exc))
802
+ except (json.JSONDecodeError, TypeError):
803
+ return False
804
+ if (
805
+ not isinstance(error_data, dict)
806
+ or error_data.get("ha_error_code") != _HA_CONFIG_NOT_FOUND_CODE
807
+ ):
808
+ return False
809
+ message = extract_tool_error_message(exc).removeprefix(
810
+ _DASHBOARD_FETCH_FAILED_PREFIX
811
+ )
812
+ return message in _NO_STORED_CONFIG_MESSAGES
813
+
814
+
781
815
  # The ha_mcp_tools/dashboards WS command: list / get / search over the live
782
816
  # lovelace collection in one in-process frame. Named once so the routing helpers
783
817
  # and their tests agree on the wire string (Global-Constraint-2 idiom, mirroring
@@ -1262,6 +1296,22 @@ async def fetch_dashboards_list(
1262
1296
  return None
1263
1297
 
1264
1298
 
1299
+ def _raise_dashboard_registry_read_error(*, action: str, url_path: str) -> NoReturn:
1300
+ """Raise the shared user-facing error for an unreadable dashboard registry."""
1301
+ raise_tool_error(
1302
+ create_error_response(
1303
+ ErrorCode.SERVICE_CALL_FAILED,
1304
+ "Failed to read the Home Assistant dashboard registry",
1305
+ suggestions=[
1306
+ "Retry the operation",
1307
+ "Check the Home Assistant connection and logs",
1308
+ "Use ha_config_get_dashboard(list_only=True) to verify registry access",
1309
+ ],
1310
+ context={"action": action, "url_path": url_path},
1311
+ )
1312
+ )
1313
+
1314
+
1265
1315
  async def _resolve_dashboard(
1266
1316
  client: Any, identifier: str
1267
1317
  ) -> tuple[dict[str, str] | None, list[dict[str, Any]] | None]:
@@ -2972,7 +3022,12 @@ class DashboardConfigTools:
2972
3022
  async def _resolve_set_dashboard_url_path(
2973
3023
  self, url_path: str
2974
3024
  ) -> tuple[str, str | None, list[dict[str, Any]] | None]:
2975
- """Validate + canonicalize ``url_path`` for ``ha_config_set_dashboard``.
3025
+ """Validate the set target and canonicalize supported dashboard identifiers.
3026
+
3027
+ Hyphenless input is allowed only when it exactly matches an existing
3028
+ dashboard's ``url_path`` (plus the built-in ``lovelace`` alias case).
3029
+ New dashboard paths must contain a hyphen. An internal-ID-only match may
3030
+ still canonicalize to a different, hyphenated existing ``url_path``.
2976
3031
 
2977
3032
  Returns ``(url_path, pre_resolved_from, pre_fetched_dashboards)``:
2978
3033
  ``url_path`` is canonicalized (``"default"`` -> ``"lovelace"``, and an
@@ -3004,22 +3059,17 @@ class DashboardConfigTools:
3004
3059
  if url_path == "default":
3005
3060
  url_path = "lovelace"
3006
3061
 
3007
- # Pre-resolve internal dashboard ID to url_path form before the
3008
- # hyphen check below, so callers may pass either form. Only fires
3009
- # when the identifier looks like an internal id (no hyphen, not
3010
- # the built-in "lovelace") and matches a known dashboard.
3062
+ # Pre-resolve a hyphenless dashboard identifier before enforcing the
3063
+ # exact-url-path exception below. Internal-ID support is preserved when
3064
+ # it canonicalizes to a hyphenated existing path, but an ID-only match
3065
+ # cannot exempt a different hyphenless path from validation.
3011
3066
  #
3012
- # Caveat: if a caller passes a hyphenless identifier intending
3013
- # to *create* a new dashboard, but it happens to match an
3014
- # existing dashboard's id, the rewrite silently re-targets the
3015
- # operation onto that existing dashboard. Pre-PR they'd have
3016
- # hit the hyphen-validation error and known their input was
3017
- # invalid; now the create-vs-update distinction depends on
3018
- # whether the registry happens to contain a matching id.
3019
- # We log the rewrite and surface the original identifier as
3020
- # ``resolved_from`` on the success response so callers can
3021
- # detect this redirect.
3067
+ # This is still a create-or-update API: a caller intending to create a
3068
+ # dashboard can target an existing one, either by exact url_path or by a
3069
+ # supported internal-ID rewrite. ``action`` reports create vs update;
3070
+ # ``resolved_from`` additionally reports only the latter rewrite.
3022
3071
  pre_resolved_from: str | None = None
3072
+ exact_url_path_exists = False
3023
3073
  # When the pre-resolver fires and finds a match, ``_resolve_dashboard``
3024
3074
  # has already fetched ``lovelace/dashboards/list``. Capture that list
3025
3075
  # so the existence-check site below can reuse it instead of paying
@@ -3027,29 +3077,37 @@ class DashboardConfigTools:
3027
3077
  pre_fetched_dashboards: list[dict[str, Any]] | None = None
3028
3078
  if "-" not in url_path and url_path != "lovelace":
3029
3079
  resolved, dashboards = await _resolve_dashboard(self._client, url_path)
3080
+ if dashboards is None:
3081
+ _raise_dashboard_registry_read_error(action="set", url_path=url_path)
3030
3082
  if resolved is not None and resolved["url_path"]:
3031
- original_url_path = url_path
3032
- url_path = resolved["url_path"]
3033
- pre_resolved_from = original_url_path
3083
+ exact_url_path_exists = resolved["url_path"] == url_path
3034
3084
  pre_fetched_dashboards = dashboards
3035
- logger.info(
3036
- "ha_config_set_dashboard pre-resolver mapped %r -> %r",
3037
- original_url_path,
3038
- url_path,
3039
- )
3085
+ if not exact_url_path_exists and "-" in resolved["url_path"]:
3086
+ original_url_path = url_path
3087
+ url_path = resolved["url_path"]
3088
+ pre_resolved_from = original_url_path
3089
+ logger.info(
3090
+ "ha_config_set_dashboard pre-resolver mapped %r -> %r",
3091
+ original_url_path,
3092
+ url_path,
3093
+ )
3040
3094
 
3041
- # Validate url_path contains hyphen for new dashboards
3095
+ # Existing exact url_path values may predate HA's creation rule and must
3096
+ # remain valid update targets unchanged.
3042
3097
  # The built-in "lovelace" dashboard is exempt since it already exists
3043
- if "-" not in url_path and url_path != "lovelace":
3098
+ if not exact_url_path_exists and "-" not in url_path and url_path != "lovelace":
3099
+ suggestions = [
3100
+ "Use format like 'my-dashboard' or 'mobile-view'",
3101
+ "Use 'lovelace' or 'default' to edit the default dashboard",
3102
+ ]
3103
+ hyphenated_url_path = url_path.replace("_", "-")
3104
+ if hyphenated_url_path != url_path:
3105
+ suggestions.insert(0, f"Try '{hyphenated_url_path}' instead")
3044
3106
  raise_tool_error(
3045
3107
  create_error_response(
3046
3108
  ErrorCode.VALIDATION_INVALID_PARAMETER,
3047
3109
  "url_path must contain a hyphen (-)",
3048
- suggestions=[
3049
- f"Try '{url_path.replace('_', '-')}' instead",
3050
- "Use format like 'my-dashboard' or 'mobile-view'",
3051
- "Use 'lovelace' or 'default' to edit the default dashboard",
3052
- ],
3110
+ suggestions=suggestions,
3053
3111
  context={"action": "set", "url_path": url_path},
3054
3112
  )
3055
3113
  )
@@ -3102,6 +3160,7 @@ class DashboardConfigTools:
3102
3160
  url_path: str, python_transform: str, current_config: dict[str, Any]
3103
3161
  ) -> dict[str, Any]:
3104
3162
  """Run ``python_transform`` against ``current_config`` in the sandbox."""
3163
+ was_strategy_dashboard = "strategy" in current_config
3105
3164
  try:
3106
3165
  transformed_config = safe_execute(python_transform, current_config)
3107
3166
  except PythonSandboxError as e:
@@ -3127,6 +3186,12 @@ class DashboardConfigTools:
3127
3186
  context={"action": "python_transform", "url_path": url_path},
3128
3187
  )
3129
3188
  )
3189
+ DashboardConfigTools._validate_strategy_dashboard_replacement(
3190
+ url_path,
3191
+ was_strategy_dashboard=was_strategy_dashboard,
3192
+ replacement_config=transformed_config,
3193
+ action="python_transform",
3194
+ )
3130
3195
  return transformed_config
3131
3196
 
3132
3197
  async def _save_dashboard_python_transform(
@@ -3255,7 +3320,10 @@ class DashboardConfigTools:
3255
3320
  if pre_fetched_dashboards is not None:
3256
3321
  existing_dashboards = pre_fetched_dashboards
3257
3322
  else:
3258
- existing_dashboards = await fetch_dashboards_list(self._client) or []
3323
+ fetched_dashboards = await fetch_dashboards_list(self._client)
3324
+ if fetched_dashboards is None:
3325
+ _raise_dashboard_registry_read_error(action="set", url_path=url_path)
3326
+ existing_dashboards = fetched_dashboards
3259
3327
  dashboard_exists = any(
3260
3328
  d.get("url_path") == url_path for d in existing_dashboards
3261
3329
  )
@@ -3344,7 +3412,7 @@ class DashboardConfigTools:
3344
3412
  ) -> tuple[str | None, bool, str | None]:
3345
3413
  """Update metadata for an existing dashboard if any metadata params were provided.
3346
3414
 
3347
- Returns ``(dashboard_id, metadata_updated, hint)``.
3415
+ Returns ``(dashboard_id, metadata_updated, warning)``.
3348
3416
  """
3349
3417
  dashboard_id = None
3350
3418
  for dashboard in existing_dashboards:
@@ -3370,13 +3438,13 @@ class DashboardConfigTools:
3370
3438
  if metadata_update_fields and dashboard_id is None:
3371
3439
  # Dashboard ID not found in storage list (e.g. default lovelace on
3372
3440
  # fresh installs). Metadata update via lovelace/dashboards/update
3373
- # is not possible without a storage ID — config update still proceeds.
3374
- hint = (
3441
+ # is not possible without a storage ID. The caller either proceeds
3442
+ # with a requested config write or rejects the resulting no-op.
3443
+ warning = (
3375
3444
  "Metadata fields were provided but could not be applied: "
3376
- "dashboard has no storage ID (likely the built-in default dashboard). "
3377
- "Config changes were still saved."
3445
+ "dashboard has no storage ID (likely the built-in default dashboard)."
3378
3446
  )
3379
- return dashboard_id, False, hint
3447
+ return dashboard_id, False, warning
3380
3448
  return dashboard_id, False, None
3381
3449
 
3382
3450
  async def _ensure_dashboard_exists(
@@ -3391,7 +3459,7 @@ class DashboardConfigTools:
3391
3459
  ) -> tuple[bool, str | None, bool, str | None]:
3392
3460
  """Create the dashboard if missing, else update its metadata if requested.
3393
3461
 
3394
- Returns ``(dashboard_exists, dashboard_id, metadata_updated, hint)`` —
3462
+ Returns ``(dashboard_exists, dashboard_id, metadata_updated, warning)`` —
3395
3463
  ``dashboard_exists`` reflects state *before* this call, so the caller
3396
3464
  can distinguish create vs update for the response's
3397
3465
  ``action``/``dashboard_created`` fields.
@@ -3409,7 +3477,7 @@ class DashboardConfigTools:
3409
3477
  )
3410
3478
  return dashboard_exists, dashboard_id, False, None
3411
3479
 
3412
- dashboard_id, metadata_updated, hint = await self._update_dashboard_metadata(
3480
+ dashboard_id, metadata_updated, warning = await self._update_dashboard_metadata(
3413
3481
  url_path,
3414
3482
  existing_dashboards,
3415
3483
  title=title,
@@ -3417,43 +3485,56 @@ class DashboardConfigTools:
3417
3485
  require_admin=require_admin,
3418
3486
  show_in_sidebar=show_in_sidebar,
3419
3487
  )
3420
- return dashboard_exists, dashboard_id, metadata_updated, hint
3488
+ return dashboard_exists, dashboard_id, metadata_updated, warning
3421
3489
 
3422
3490
  async def _check_dashboard_replace_hash(
3423
- self, url_path: str, config_hash: str | None
3491
+ self,
3492
+ url_path: str,
3493
+ config_hash: str | None,
3494
+ replacement_config: dict[str, Any],
3424
3495
  ) -> str | None:
3425
- """Optionally validate config_hash and warn on large full-config replacement.
3426
-
3427
- Tolerates fetch failures — full replacement still proceeds even if the
3428
- pre-read can't load the current state (force-replace path).
3496
+ """Validate replacement safety/hash and warn on large full-config replacement.
3497
+
3498
+ Omitting ``config_hash`` remains the force-replace path, but the current
3499
+ config must still be readable so strategy-dashboard protection can be
3500
+ evaluated before any replacement write. The one exception is an existing
3501
+ storage dashboard with no config yet and no supplied ``config_hash``:
3502
+ there is no strategy state to protect, so its first full config may be
3503
+ written. A supplied hash still conflicts with the now-absent config.
3429
3504
  """
3430
3505
  try:
3431
3506
  existing_config, existing_hash = await _get_dashboard_config_internal(
3432
3507
  self._client, url_path
3433
3508
  )
3434
- except ToolError:
3435
- # Pre-read failure is non-fatal on the force-replace path: skip
3436
- # the optimistic-lock check and large-config warning and proceed
3437
- # with the replacement.
3438
- return None
3439
-
3440
- if not isinstance(existing_config, dict):
3441
- return None
3442
-
3443
- existing_config_size = len(json.dumps(existing_config))
3444
- if config_hash is not None and existing_hash != config_hash:
3509
+ except ToolError as exc:
3510
+ if _is_no_stored_dashboard_config_error(exc):
3511
+ if config_hash is None:
3512
+ return None
3513
+ self._raise_dashboard_hash_conflict(url_path)
3445
3514
  raise_tool_error(
3446
3515
  create_error_response(
3447
3516
  ErrorCode.SERVICE_CALL_FAILED,
3448
- "Dashboard modified since last read (conflict)",
3517
+ "Cannot verify the current dashboard config before full replacement",
3518
+ details=extract_tool_error_message(exc),
3449
3519
  suggestions=[
3450
- "Call ha_config_get_dashboard() again",
3451
- "Use the fresh config_hash, or omit config_hash to force replace",
3520
+ "Retry the operation",
3521
+ "Read the dashboard with ha_config_get_dashboard first",
3452
3522
  ],
3453
3523
  context={"action": "set", "url_path": url_path},
3454
3524
  )
3455
3525
  )
3456
3526
 
3527
+ existing_config_size = len(json.dumps(existing_config))
3528
+ if config_hash is not None and existing_hash != config_hash:
3529
+ self._raise_dashboard_hash_conflict(url_path)
3530
+
3531
+ self._validate_strategy_dashboard_replacement(
3532
+ url_path,
3533
+ was_strategy_dashboard="strategy" in existing_config,
3534
+ replacement_config=replacement_config,
3535
+ action="config",
3536
+ )
3537
+
3457
3538
  if existing_config_size >= 10000:
3458
3539
  return (
3459
3540
  f"Replaced large config ({existing_config_size:,} bytes). "
@@ -3461,6 +3542,44 @@ class DashboardConfigTools:
3461
3542
  )
3462
3543
  return None
3463
3544
 
3545
+ @staticmethod
3546
+ def _raise_dashboard_hash_conflict(url_path: str) -> NoReturn:
3547
+ """Raise the shared optimistic-lock conflict for a full replacement."""
3548
+ raise_tool_error(
3549
+ create_error_response(
3550
+ ErrorCode.SERVICE_CALL_FAILED,
3551
+ "Dashboard modified since last read (conflict)",
3552
+ suggestions=[
3553
+ "Call ha_config_get_dashboard() again",
3554
+ "Use the fresh config_hash, or omit config_hash to force replace",
3555
+ ],
3556
+ context={"action": "set", "url_path": url_path},
3557
+ )
3558
+ )
3559
+
3560
+ @staticmethod
3561
+ def _validate_strategy_dashboard_replacement(
3562
+ url_path: str,
3563
+ *,
3564
+ was_strategy_dashboard: bool,
3565
+ replacement_config: dict[str, Any],
3566
+ action: str,
3567
+ ) -> None:
3568
+ """Prevent this tool from taking control of a strategy dashboard."""
3569
+ if not was_strategy_dashboard or "strategy" in replacement_config:
3570
+ return
3571
+ raise_tool_error(
3572
+ create_error_response(
3573
+ ErrorCode.VALIDATION_FAILED,
3574
+ "Strategy dashboards cannot be converted to custom dashboards via this tool",
3575
+ suggestions=[
3576
+ "Use 'Take Control' in the Home Assistant interface to convert it",
3577
+ "Keep a strategy configuration when updating this dashboard",
3578
+ ],
3579
+ context={"action": action, "url_path": url_path},
3580
+ )
3581
+ )
3582
+
3464
3583
  async def _save_dashboard_config(
3465
3584
  self, url_path: str, config_dict: dict[str, Any]
3466
3585
  ) -> None:
@@ -3499,7 +3618,7 @@ class DashboardConfigTools:
3499
3618
  ) -> tuple[bool, str | None, dict[str, Any]]:
3500
3619
  """Parse + validate ``config`` and save it as a full replacement.
3501
3620
 
3502
- Returns ``(config_updated, hint, saved_config)``.
3621
+ Returns ``(config_updated, warning, saved_config)``.
3503
3622
  """
3504
3623
  parsed_config = parse_json_param(config, "config")
3505
3624
  if parsed_config is None or not isinstance(parsed_config, dict):
@@ -3515,12 +3634,14 @@ class DashboardConfigTools:
3515
3634
  )
3516
3635
  config_dict = cast(dict[str, Any], parsed_config)
3517
3636
 
3518
- hint: str | None = None
3637
+ warning: str | None = None
3519
3638
  if dashboard_exists:
3520
- hint = await self._check_dashboard_replace_hash(url_path, config_hash)
3639
+ warning = await self._check_dashboard_replace_hash(
3640
+ url_path, config_hash, config_dict
3641
+ )
3521
3642
 
3522
3643
  await self._save_dashboard_config(url_path, config_dict)
3523
- return True, hint, config_dict
3644
+ return True, warning, config_dict
3524
3645
 
3525
3646
  async def _run_dashboard_config_update(
3526
3647
  self,
@@ -3543,7 +3664,7 @@ class DashboardConfigTools:
3543
3664
  dashboard_exists,
3544
3665
  dashboard_id,
3545
3666
  metadata_updated,
3546
- hint,
3667
+ metadata_warning,
3547
3668
  ) = await self._ensure_dashboard_exists(
3548
3669
  url_path,
3549
3670
  title=title,
@@ -3555,16 +3676,49 @@ class DashboardConfigTools:
3555
3676
 
3556
3677
  config_updated = False
3557
3678
  render_config: dict[str, Any] | None = None
3679
+ warnings: list[str] = []
3558
3680
  if config is not None:
3559
3681
  (
3560
3682
  config_updated,
3561
- config_hint,
3683
+ config_warning,
3562
3684
  render_config,
3563
3685
  ) = await self._apply_dashboard_config(
3564
3686
  url_path, config, config_hash, dashboard_exists
3565
3687
  )
3566
- if config_hint:
3567
- hint = config_hint
3688
+ if config_warning:
3689
+ warnings.append(config_warning)
3690
+
3691
+ if dashboard_exists and not config_updated and not metadata_updated:
3692
+ if metadata_warning is not None:
3693
+ raise_tool_error(
3694
+ create_error_response(
3695
+ ErrorCode.SERVICE_CALL_FAILED,
3696
+ "No dashboard changes were applied",
3697
+ details=metadata_warning,
3698
+ suggestions=[
3699
+ "Use a storage-mode dashboard when changing metadata",
3700
+ "Provide config to replace the dashboard configuration",
3701
+ ],
3702
+ context={"action": "set", "url_path": url_path},
3703
+ )
3704
+ )
3705
+ raise_tool_error(
3706
+ create_error_response(
3707
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
3708
+ "No dashboard changes were requested",
3709
+ suggestions=[
3710
+ "Provide config or python_transform to change dashboard content",
3711
+ "Provide a metadata field such as title or icon",
3712
+ "Use ha_config_get_dashboard to read a dashboard without changing it",
3713
+ ],
3714
+ context={"action": "set", "url_path": url_path},
3715
+ )
3716
+ )
3717
+
3718
+ if metadata_warning:
3719
+ if config_updated:
3720
+ metadata_warning = f"{metadata_warning} Dashboard config was saved."
3721
+ warnings.insert(0, metadata_warning)
3568
3722
 
3569
3723
  result_dict: dict[str, Any] = {
3570
3724
  "success": True,
@@ -3577,13 +3731,12 @@ class DashboardConfigTools:
3577
3731
  "message": f"Dashboard {url_path} {'created' if not dashboard_exists else 'updated'} successfully",
3578
3732
  }
3579
3733
 
3580
- if hint:
3581
- result_dict["hint"] = hint
3734
+ if warnings:
3735
+ result_dict["warnings"] = warnings
3582
3736
  if pre_resolved_from is not None:
3583
- # Caller passed an internal id; pre-resolver mapped it to
3584
- # the canonical url_path. Surface the original so a caller
3585
- # who *intended* to create a new dashboard can detect that
3586
- # an existing dashboard was updated instead.
3737
+ # This marks identifier canonicalization only. Exact url_path
3738
+ # matches deliberately omit it; callers use ``action`` to tell
3739
+ # whether the create-or-update operation updated an existing target.
3587
3740
  result_dict["resolved_from"] = pre_resolved_from
3588
3741
 
3589
3742
  render_config = await _attach_dashboard_render_paths_after_write(
@@ -3651,11 +3804,11 @@ class DashboardConfigTools:
3651
3804
  context={"action": "delete"},
3652
3805
  )
3653
3806
  resolved, dashboards = await _resolve_dashboard(self._client, url_path)
3807
+ if dashboards is None:
3808
+ _raise_dashboard_registry_read_error(action="delete", url_path=url_path)
3654
3809
  if resolved is None:
3655
3810
  available_ids = [
3656
- d.get("url_path")
3657
- for d in (dashboards or [])[:10]
3658
- if d.get("url_path")
3811
+ d.get("url_path") for d in dashboards[:10] if d.get("url_path")
3659
3812
  ]
3660
3813
  raise_tool_error(
3661
3814
  create_error_response(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 8.1.1.dev2289
3
+ Version: 8.1.1.dev2292
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