ha-mcp-dev 8.4.2.dev2533__tar.gz → 8.4.3.dev2539__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 (309) hide show
  1. {ha_mcp_dev-8.4.2.dev2533/src/ha_mcp_dev.egg-info → ha_mcp_dev-8.4.3.dev2539}/PKG-INFO +1 -1
  2. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/pyproject.toml +1 -1
  3. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/backup_manager.py +266 -29
  4. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/config.py +27 -1
  5. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/read_only.py +23 -0
  6. ha_mcp_dev-8.4.3.dev2539/src/ha_mcp/renamed_tools.py +103 -0
  7. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/server.py +22 -8
  8. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/settings_ui/_persistence.py +62 -10
  9. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/backup.py +1 -1
  10. ha_mcp_dev-8.4.3.dev2539/src/ha_mcp/tools/blueprint_sources.py +402 -0
  11. ha_mcp_dev-8.4.3.dev2539/src/ha_mcp/tools/blueprint_substitute.py +243 -0
  12. ha_mcp_dev-8.4.3.dev2539/src/ha_mcp/tools/blueprint_write.py +501 -0
  13. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/bulk_selector.py +20 -11
  14. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/component_api.py +7 -0
  15. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/component_devices.py +9 -2
  16. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/renamed_tool_middleware.py +9 -2
  17. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/smart_search/_entities.py +9 -12
  18. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/smart_search/_overview.py +11 -12
  19. ha_mcp_dev-8.4.3.dev2539/src/ha_mcp/tools/tools_blueprints.py +734 -0
  20. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/tools_config_automations.py +129 -17
  21. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/tools_config_scripts.py +103 -20
  22. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/tools_entities.py +5 -1
  23. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/tools_filesystem.py +4 -3
  24. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/tools_registry.py +36 -7
  25. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/tools_search.py +42 -14
  26. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/tools_voice_assistant.py +5 -1
  27. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/transforms/categorized_search.py +69 -21
  28. ha_mcp_dev-8.4.3.dev2539/src/ha_mcp/utils/device_registry_semantics.py +171 -0
  29. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/visibility/resolver.py +85 -26
  30. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  31. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp_dev.egg-info/SOURCES.txt +4 -0
  32. ha_mcp_dev-8.4.2.dev2533/src/ha_mcp/renamed_tools.py +0 -43
  33. ha_mcp_dev-8.4.2.dev2533/src/ha_mcp/tools/tools_blueprints.py +0 -583
  34. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/LICENSE +0 -0
  35. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/MANIFEST.in +0 -0
  36. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/README.md +0 -0
  37. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/setup.cfg +0 -0
  38. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/__init__.py +0 -0
  39. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/__main__.py +0 -0
  40. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_pypi_marker +0 -0
  41. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_rich_compat.py +0 -0
  42. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/__init__.py +0 -0
  43. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/requirements.txt +0 -0
  44. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/LICENSE +0 -0
  45. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/MANIFEST.sha256 +0 -0
  46. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/VENDORED +0 -0
  47. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/__init__.py +0 -0
  48. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/__main__.py +0 -0
  49. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/asyncio/__init__.py +0 -0
  50. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/asyncio/client.py +0 -0
  51. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/asyncio/connection.py +0 -0
  52. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/asyncio/messages.py +0 -0
  53. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/asyncio/router.py +0 -0
  54. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/asyncio/server.py +0 -0
  55. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/auth.py +0 -0
  56. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/cli.py +0 -0
  57. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/client.py +0 -0
  58. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/connection.py +0 -0
  59. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/datastructures.py +0 -0
  60. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/exceptions.py +0 -0
  61. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/extensions/__init__.py +0 -0
  62. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/extensions/base.py +0 -0
  63. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/extensions/permessage_deflate.py +0 -0
  64. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/frames.py +0 -0
  65. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/headers.py +0 -0
  66. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/http11.py +0 -0
  67. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/imports.py +0 -0
  68. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/legacy/__init__.py +0 -0
  69. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/legacy/auth.py +0 -0
  70. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/legacy/client.py +0 -0
  71. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/legacy/exceptions.py +0 -0
  72. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/legacy/framing.py +0 -0
  73. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/legacy/handshake.py +0 -0
  74. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/legacy/http.py +0 -0
  75. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/legacy/protocol.py +0 -0
  76. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/legacy/server.py +0 -0
  77. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/protocol.py +0 -0
  78. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/proxy.py +0 -0
  79. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/py.typed +0 -0
  80. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/server.py +0 -0
  81. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/streams.py +0 -0
  82. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/sync/__init__.py +0 -0
  83. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/sync/client.py +0 -0
  84. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/sync/connection.py +0 -0
  85. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/sync/messages.py +0 -0
  86. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/sync/router.py +0 -0
  87. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/sync/server.py +0 -0
  88. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/sync/utils.py +0 -0
  89. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/trio/__init__.py +0 -0
  90. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/trio/client.py +0 -0
  91. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/trio/connection.py +0 -0
  92. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/trio/messages.py +0 -0
  93. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/trio/router.py +0 -0
  94. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/trio/server.py +0 -0
  95. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/trio/utils.py +0 -0
  96. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/typing.py +0 -0
  97. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/uri.py +0 -0
  98. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/utils.py +0 -0
  99. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_vendor/websockets/version.py +0 -0
  100. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/_version.py +0 -0
  101. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/auth/__init__.py +0 -0
  102. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/auth/consent_form.py +0 -0
  103. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/auth/oidc_compat.py +0 -0
  104. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/auth/provider.py +0 -0
  105. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/browser_landing.py +0 -0
  106. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/client/__init__.py +0 -0
  107. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/client/rest_client.py +0 -0
  108. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/client/supervisor_client.py +0 -0
  109. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/client/websocket_client.py +0 -0
  110. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/client/websocket_listener.py +0 -0
  111. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  112. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  113. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
  114. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
  115. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  116. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/dashboard_screenshot/theme_guard.py +0 -0
  117. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/errors.py +0 -0
  118. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/hacs_auto_refresh.py +0 -0
  119. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/llm_exposure.py +0 -0
  120. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/log_filters.py +0 -0
  121. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/policy/__init__.py +0 -0
  122. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/policy/approval_queue.py +0 -0
  123. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/policy/editing.py +0 -0
  124. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/policy/evaluator.py +0 -0
  125. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/policy/handlers.py +0 -0
  126. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/policy/middleware.py +0 -0
  127. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/policy/model.py +0 -0
  128. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/policy/persistence.py +0 -0
  129. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/policy/value_sources.py +0 -0
  130. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/py.typed +0 -0
  131. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/redaction.py +0 -0
  132. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  133. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  134. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  135. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  136. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  137. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  138. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  139. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  140. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  141. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  142. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/resources/skills-vendor/evals/area-target-over-entity-list/case.yaml +0 -0
  143. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/resources/skills-vendor/evals/arrive-home-automation/case.yaml +0 -0
  144. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/resources/skills-vendor/evals/assist-shortcut-card/case.yaml +0 -0
  145. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/resources/skills-vendor/evals/avg-temperature/case.yaml +0 -0
  146. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/resources/skills-vendor/evals/backup-deletion-confirmation/case.yaml +0 -0
  147. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/resources/skills-vendor/evals/blueprint-authoring/case.yaml +0 -0
  148. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/resources/skills-vendor/evals/button-event-entity/case.yaml +0 -0
  149. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/resources/skills-vendor/evals/device-id-trigger-migration/case.yaml +0 -0
  150. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/resources/skills-vendor/evals/helper-flow-fields-not-platform-yaml/case.yaml +0 -0
  151. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/resources/skills-vendor/evals/media-player-song/case.yaml +0 -0
  152. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/resources/skills-vendor/evals/motion-light-automation-mode/case.yaml +0 -0
  153. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/resources/skills-vendor/evals/rename-entity-impact-analysis/case.yaml +0 -0
  154. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/resources/skills-vendor/evals/template-attributes-yaml-exception/case.yaml +0 -0
  155. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/resources/skills-vendor/evals/timer-finished-automation/case.yaml +0 -0
  156. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/resources/skills-vendor/evals/vacuum-last-clean/case.yaml +0 -0
  157. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/resources/skills-vendor/scripts/check_eval_cases.py +0 -0
  158. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  159. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
  160. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  161. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/backups.md +0 -0
  162. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
  163. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  164. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  165. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  166. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  167. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  168. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  169. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  170. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
  171. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  172. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  173. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/settings_ui/__init__.py +0 -0
  174. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
  175. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
  176. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
  177. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
  178. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
  179. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
  180. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/settings_ui/_i18n.py +0 -0
  181. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/settings_ui/_locale_policy.py +0 -0
  182. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
  183. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/settings_ui/_theme.py +0 -0
  184. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
  185. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/settings_ui/locales/README.md +0 -0
  186. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/settings_ui/locales/cs.json +0 -0
  187. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/settings_ui/locales/de.json +0 -0
  188. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/settings_ui/locales/en.json +0 -0
  189. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/settings_ui/locales/eo.json +0 -0
  190. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/settings_ui/locales/es.json +0 -0
  191. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/settings_ui/locales/fr.json +0 -0
  192. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/settings_ui/locales/it.json +0 -0
  193. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/settings_ui/locales/ko.json +0 -0
  194. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/settings_ui/locales/nl.json +0 -0
  195. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/settings_ui/locales/pl.json +0 -0
  196. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/settings_ui/locales/ru.json +0 -0
  197. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/settings_ui/locales/sv.json +0 -0
  198. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/settings_ui/locales/tlh.json +0 -0
  199. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/settings_ui/locales/zh-Hans.json +0 -0
  200. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/settings_ui/settings.css +0 -0
  201. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/settings_ui/settings.html +0 -0
  202. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/settings_ui/settings.js +0 -0
  203. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/smoke_test.py +0 -0
  204. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  205. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/strict_bps.py +0 -0
  206. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/__init__.py +0 -0
  207. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/auto_backup.py +0 -0
  208. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  209. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/best_practice_result.py +0 -0
  210. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/best_practice_variables.py +0 -0
  211. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/component_config_entries.py +0 -0
  212. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/component_config_reads.py +0 -0
  213. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/component_registries.py +0 -0
  214. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/component_registry_lookup.py +0 -0
  215. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/config_entry_flow.py +0 -0
  216. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/config_entry_flow_form.py +0 -0
  217. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/config_entry_flow_menu.py +0 -0
  218. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/config_entry_flow_walker.py +0 -0
  219. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/config_entry_identity.py +0 -0
  220. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/config_entry_reconfigure.py +0 -0
  221. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/config_entry_reload_watch.py +0 -0
  222. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/device_control.py +0 -0
  223. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/error_log_parsing.py +0 -0
  224. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/hacs_registration.py +0 -0
  225. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/helpers.py +0 -0
  226. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/integration_reconfigure.py +0 -0
  227. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/log_common.py +0 -0
  228. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/log_sources.py +0 -0
  229. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/log_sources_supervisor.py +0 -0
  230. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/radio/__init__.py +0 -0
  231. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/radio/base.py +0 -0
  232. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/radio/matter.py +0 -0
  233. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/radio/thread.py +0 -0
  234. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/radio/zigbee.py +0 -0
  235. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/radio/zwave.py +0 -0
  236. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/reference_validator.py +0 -0
  237. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/registry.py +0 -0
  238. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  239. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  240. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  241. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
  242. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  243. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/smart_search/_graph.py +0 -0
  244. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  245. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  246. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
  247. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/tools_addons.py +0 -0
  248. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/tools_areas.py +0 -0
  249. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/tools_assist_pipeline.py +0 -0
  250. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  251. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/tools_calendar.py +0 -0
  252. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/tools_camera.py +0 -0
  253. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/tools_categories.py +0 -0
  254. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/tools_code.py +0 -0
  255. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  256. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  257. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  258. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  259. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/tools_dev.py +0 -0
  260. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/tools_energy.py +0 -0
  261. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/tools_groups.py +0 -0
  262. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/tools_hacs.py +0 -0
  263. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/tools_history.py +0 -0
  264. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/tools_integrations.py +0 -0
  265. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/tools_labels.py +0 -0
  266. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/tools_logs.py +0 -0
  267. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/tools_radio.py +0 -0
  268. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/tools_resources.py +0 -0
  269. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/tools_security_policy.py +0 -0
  270. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/tools_service.py +0 -0
  271. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/tools_services.py +0 -0
  272. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/tools_system.py +0 -0
  273. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/tools_themes.py +0 -0
  274. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/tools_todo.py +0 -0
  275. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/tools_traces.py +0 -0
  276. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/tools_updates.py +0 -0
  277. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/tools_utility.py +0 -0
  278. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  279. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
  280. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/tools_zones.py +0 -0
  281. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/util_helpers.py +0 -0
  282. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/tools/validation_middleware.py +0 -0
  283. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/transforms/__init__.py +0 -0
  284. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  285. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/transport_security.py +0 -0
  286. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/update_check.py +0 -0
  287. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/utils/__init__.py +0 -0
  288. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/utils/config_hash.py +0 -0
  289. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/utils/config_write_lock.py +0 -0
  290. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/utils/data_paths.py +0 -0
  291. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/utils/domain_handlers.py +0 -0
  292. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/utils/entity_membership.py +0 -0
  293. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  294. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  295. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/utils/operation_manager.py +0 -0
  296. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/utils/python_sandbox.py +0 -0
  297. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/utils/skill_loader.py +0 -0
  298. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/utils/usage_logger.py +0 -0
  299. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/visibility/__init__.py +0 -0
  300. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/visibility/enforcement.py +0 -0
  301. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/visibility/model.py +0 -0
  302. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp/visibility/persistence.py +0 -0
  303. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  304. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  305. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  306. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  307. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/tests/__init__.py +0 -0
  308. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/tests/test_constants.py +0 -0
  309. {ha_mcp_dev-8.4.2.dev2533 → ha_mcp_dev-8.4.3.dev2539}/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.2.dev2533
3
+ Version: 8.4.3.dev2539
4
4
  Summary: Home Assistant MCP Server - Complete control of Home Assistant through MCP
5
5
  Author-email: Julien <github@qc-h.net>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ha-mcp-dev"
7
- version = "8.4.2.dev2533"
7
+ version = "8.4.3.dev2539"
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
@@ -40,6 +40,7 @@ from __future__ import annotations
40
40
 
41
41
  import asyncio
42
42
  import difflib
43
+ import hashlib
43
44
  import logging
44
45
  import os
45
46
  import re
@@ -103,15 +104,24 @@ class MandatoryBackupError(Exception):
103
104
  _TRACKER_SOFT_CAP = 10_000
104
105
  _TRACKER_PRUNE_BATCH = 1_000
105
106
 
106
- # Filename pattern: <domain>.<safe_entity_id>.<YYYYMMDD_HHMMSS>.yaml
107
+ # Filename pattern: <domain>.<safe_entity_id>.<YYYYMMDD_HHMMSS>[_NN].yaml
107
108
  # The middle ``.`` separators make the timestamp rsplit reliable even
108
- # when entity_id contains dots (after sanitization, dots are kept).
109
+ # when entity_id contains dots (after sanitization, dots are kept). ``_NN``
110
+ # is the same-second sequence (see ``_unclaimed_target``): the timestamp is
111
+ # a second and the throttle defaults to 0, so a second capture of one entity
112
+ # inside that second takes the next free suffix rather than replacing the
113
+ # first. ``_`` sorts after ``.``, so a suffixed name stays newest-last.
109
114
  _FILENAME_RE = re.compile(
110
115
  r"^(?P<domain>[A-Za-z0-9_]+)\."
111
116
  r"(?P<entity_id>[A-Za-z0-9._-]+)\."
112
- r"(?P<ts>\d{8}_\d{6})\.yaml$"
117
+ r"(?P<ts>\d{8}_\d{6})(?:_(?P<seq>\d{2}))?\.yaml$"
113
118
  )
114
119
 
120
+ # Captures of one entity inside one second before the manager gives up on a
121
+ # free name. Two is the realistic case (a save then a delete of the same
122
+ # blueprint); the cap only bounds the ``exists()`` probe.
123
+ _MAX_SAME_SECOND = 100
124
+
115
125
  # Domains whose snapshot ``config`` is raw text (file/YAML content) rather
116
126
  # than a structured dict. They carry a ``kind: "text"`` marker in the
117
127
  # snapshot payload and diff via a unified text diff instead of JSON-Patch
@@ -121,7 +131,19 @@ _FILENAME_RE = re.compile(
121
131
  # (read_file), but restored via edit_yaml_config(action="replace_file") because
122
132
  # write_file rejects config files. It backs the pre-restore safety snapshot and
123
133
  # the legacy-store restore (#1579).
124
- _TEXT_DOMAINS = frozenset({"file", "yaml", "yaml_file"})
134
+ #
135
+ # ``blueprint_automation`` / ``blueprint_script`` snapshot the blueprint's raw
136
+ # YAML text (#2329): core stores blueprints as files, and ``blueprint/save``
137
+ # takes the same YAML string back, so the round trip is text on both ends.
138
+ _TEXT_DOMAINS = frozenset(
139
+ {
140
+ "file",
141
+ "yaml",
142
+ "yaml_file",
143
+ "blueprint_automation",
144
+ "blueprint_script",
145
+ }
146
+ )
125
147
 
126
148
  # Pre-#1579 backups (``.ha_mcp_tools_backups/*.bak``) are surfaced through the
127
149
  # same scope="edits" actions under a synthetic name ``legacy:<filename>``. The
@@ -163,17 +185,74 @@ class DomainHandler:
163
185
  # ----------------------------- backup manager -------------------------------
164
186
 
165
187
 
188
+ def _entity_id_aliases(entity_id: str) -> list[str]:
189
+ """Every filename stem a snapshot of ``entity_id`` may carry.
190
+
191
+ Sanitised ids gained a digest suffix, so snapshots written before that
192
+ sit under the bare sanitised name. Rotation and entity filtering match
193
+ both, otherwise those older files would never be counted again: they
194
+ would never be pruned, and would drop out of an entity-filtered listing.
195
+ """
196
+ current = _safe_entity_id(entity_id)
197
+ legacy = _SAFE_ID_RE.sub("_", entity_id).lstrip(".") or "_"
198
+ return [current] if legacy == current else [current, legacy]
199
+
200
+
166
201
  def _safe_entity_id(entity_id: str) -> str:
167
202
  """Sanitize an entity id for use in a filename.
168
203
 
169
204
  Replaces any character outside ``[A-Za-z0-9._-]`` with ``_``. Path
170
205
  separators get caught by this (the regex excludes both ``/`` and ``\\``).
171
206
  Strips leading dots to prevent dotfile collisions.
207
+
208
+ Sanitizing is lossy, so ids that needed it get a short digest of the
209
+ ORIGINAL appended. Blueprint domains (#2329) key on paths, where
210
+ ``user/motion.yaml`` and ``user_motion.yaml`` both clean to
211
+ ``user_motion.yaml`` -- without the digest they share one snapshot
212
+ namespace, so captures in the same second overwrite each other and
213
+ rotation counts both histories as one, able to delete the only restore
214
+ point for a blueprint that was never written. Entity ids are already
215
+ within the safe set, so their filenames are unchanged and existing
216
+ snapshots stay discoverable.
172
217
  """
173
218
  if not entity_id:
174
219
  return "_"
175
220
  cleaned = _SAFE_ID_RE.sub("_", entity_id).lstrip(".")
176
- return cleaned or "_"
221
+ if not cleaned:
222
+ return "_"
223
+ if cleaned == entity_id:
224
+ return cleaned
225
+ digest = hashlib.sha256(entity_id.encode("utf-8")).hexdigest()[:8]
226
+ return f"{cleaned}-{digest}"
227
+
228
+
229
+ def _snapshot_order(path: Path) -> tuple[str, str, str]:
230
+ """Sort key placing snapshots in capture order, oldest first.
231
+
232
+ Filenames cannot be compared directly: an id the sanitiser rewrote has
233
+ two stems, and the digest stem sorts before the bare one on every name
234
+ (``-`` < ``.``) regardless of timestamp, so by name every new capture
235
+ would age below every pre-digest file. The parsed timestamp and the
236
+ same-second sequence order them; the name only breaks exact ties.
237
+ """
238
+ m = _FILENAME_RE.match(path.name)
239
+ if m is None:
240
+ return ("", "", path.name)
241
+ return (m.group("ts"), m.group("seq") or "", path.name)
242
+
243
+
244
+ def _id_matches(payload_id: str | None, wanted: str) -> bool:
245
+ """Whether a snapshot whose payload names ``payload_id`` is ``wanted``.
246
+
247
+ Two spellings select an entity: the id itself, and the digest stem
248
+ ``_safe_entity_id`` derives from it — the stem a listing used to show and
249
+ a filename still carries. Both name exactly one entity. The bare
250
+ pre-digest stem is deliberately not accepted: it is the current stem of a
251
+ differently-named entity, which is the ambiguity this check exists for.
252
+ """
253
+ if payload_id is None:
254
+ return False
255
+ return payload_id == wanted or _safe_entity_id(payload_id) == wanted
177
256
 
178
257
 
179
258
  def _now_ts() -> str:
@@ -517,9 +596,7 @@ class BackupManager:
517
596
  self, domain: str, entity_id: str, config: Any, tool_name: str | None
518
597
  ) -> Path:
519
598
  safe = _safe_entity_id(entity_id)
520
- ts = _now_ts()
521
- filename = f"{domain}.{safe}.{ts}.yaml"
522
- target = self._dir / filename
599
+ target = self._unclaimed_target(domain, safe, _now_ts())
523
600
  payload: dict[str, Any] = {
524
601
  "schema_version": SCHEMA_VERSION,
525
602
  "domain": domain,
@@ -538,10 +615,76 @@ class BackupManager:
538
615
  logger.info("Auto-backup: wrote %s", target.name)
539
616
  return target
540
617
 
618
+ def _unclaimed_target(self, domain: str, safe: str, ts: str) -> Path:
619
+ """First filename for ``ts`` that no snapshot of this entity holds yet.
620
+
621
+ The bare name is taken when it is free; each later capture inside the
622
+ same second appends ``_01``, ``_02``, ... so the atomic replace in
623
+ ``_write_snapshot`` can never discard an earlier pre-write state.
624
+ ``maybe_snapshot`` serialises captures per key, so the ``exists()``
625
+ probe is not racing another capture of the same entity.
626
+ """
627
+ target = self._dir / f"{domain}.{safe}.{ts}.yaml"
628
+ for seq in range(1, _MAX_SAME_SECOND):
629
+ if not target.exists():
630
+ return target
631
+ target = self._dir / f"{domain}.{safe}.{ts}_{seq:02d}.yaml"
632
+ if not target.exists():
633
+ return target
634
+ raise OSError(
635
+ f"{_MAX_SAME_SECOND} snapshots of {domain}:{safe} inside one second"
636
+ )
637
+
638
+ def _payload_entity_id(self, path: Path) -> str | None:
639
+ """The id the snapshot at ``path`` was taken for, from its payload.
640
+
641
+ Filename stems are ambiguous in both directions: the legacy stem for
642
+ ``user/motion.yaml`` is ``user_motion.yaml``, which is also the
643
+ CURRENT stem of a differently-named blueprint. The payload keeps the
644
+ original id, so rotation and the entity filter behind ``list_snapshots``
645
+ / ``delete_bulk`` ask it rather than trusting the name. A file that
646
+ cannot be read answers ``None`` and is treated as not ours: skipping it
647
+ wastes a rotation slot or hides it from one entity's listing, deleting
648
+ it could destroy another entity's only restore point.
649
+
650
+ Only the header is read. ``_write_snapshot`` emits ``entity_id`` before
651
+ ``config`` (which is the whole automation, dashboard or file), so the
652
+ read stops at the ``config:`` line and never parses the body: every
653
+ listing row opens its file, and an unfiltered listing of hundreds of
654
+ whole-file snapshots has to stay a header read per row.
655
+ """
656
+ try:
657
+ with path.open(encoding="utf-8") as handle:
658
+ header: list[str] = []
659
+ for line in handle:
660
+ if line.startswith("config:"):
661
+ break
662
+ header.append(line)
663
+ loaded = yaml.safe_load("".join(header))
664
+ except (OSError, yaml.YAMLError) as err:
665
+ logger.warning(
666
+ "Auto-backup: cannot identify %s, leaving it in place: %s",
667
+ path.name,
668
+ err,
669
+ )
670
+ return None
671
+ if not isinstance(loaded, dict):
672
+ return None
673
+ found = loaded.get("entity_id")
674
+ return found if isinstance(found, str) else None
675
+
676
+ def _snapshot_is_for(self, path: Path, entity_id: str) -> bool:
677
+ """Whether ``path`` holds a snapshot of ``entity_id`` (see ``_id_matches``)."""
678
+ return _id_matches(self._payload_entity_id(path), entity_id)
679
+
541
680
  def _rotate(self, domain: str, entity_id: str) -> None:
542
- safe = _safe_entity_id(entity_id)
543
- pattern = f"{domain}.{safe}.*.yaml"
544
- files = sorted(self._dir.glob(pattern))
681
+ candidates: set[Path] = set()
682
+ for safe in _entity_id_aliases(entity_id):
683
+ candidates.update(self._dir.glob(f"{domain}.{safe}.*.yaml"))
684
+ files = sorted(
685
+ (p for p in candidates if self._snapshot_is_for(p, entity_id)),
686
+ key=_snapshot_order,
687
+ )
545
688
  excess = len(files) - self.retain_per_entity
546
689
  for old in files[: max(0, excess)]:
547
690
  try:
@@ -566,30 +709,64 @@ class BackupManager:
566
709
  # would otherwise never match a filter passed in original form.
567
710
  # Sanitize the filter once up front so the per-file comparison is
568
711
  # symmetric: filter and ``meta["entity_id"]`` both come from the
569
- # same sanitization function.
570
- safe_filter = _safe_entity_id(entity_id) if entity_id else None
712
+ # same sanitization function. The stem only narrows the candidates:
713
+ # sanitising is lossy in both directions (``user/motion.yaml``'s
714
+ # pre-digest stem IS ``user_motion.yaml``'s current stem), so each
715
+ # stem match is confirmed against the id the payload stores before it
716
+ # counts as this entity's — ``delete_bulk`` unlinks whatever is
717
+ # returned here, and the caller named one entity.
718
+ safe_filter = set(_entity_id_aliases(entity_id)) if entity_id else None
571
719
  out: list[dict[str, Any]] = []
572
- # Reverse-sorted glob — newest filenames sort last lexicographically,
573
- # so reverse=True yields newest-first.
574
- for path in sorted(self._dir.glob("*.yaml"), reverse=True):
575
- meta = self._parse_filename(path.name)
576
- if meta is None:
577
- continue
578
- if domain and meta["domain"] != domain:
579
- continue
580
- if safe_filter and meta["entity_id"] != safe_filter:
581
- continue
582
- try:
583
- stat = path.stat()
584
- except OSError:
720
+ # Newest first by capture time — see ``_snapshot_order`` for why the
721
+ # filename itself is not the key.
722
+ for path in sorted(self._dir.glob("*.yaml"), key=_snapshot_order, reverse=True):
723
+ row = self._listing_row(
724
+ path, domain=domain, safe_filter=safe_filter, entity_id=entity_id
725
+ )
726
+ if row is None:
585
727
  continue
586
- meta["size"] = stat.st_size
587
- meta["mtime"] = stat.st_mtime
588
- out.append(meta)
728
+ out.append(row)
589
729
  if limit and len(out) >= limit:
590
730
  break
591
731
  return out
592
732
 
733
+ def _listing_row(
734
+ self,
735
+ path: Path,
736
+ *,
737
+ domain: str | None,
738
+ safe_filter: set[str] | None,
739
+ entity_id: str | None,
740
+ ) -> dict[str, Any] | None:
741
+ """One ``list_snapshots`` entry for ``path``, or ``None`` if filtered out."""
742
+ meta = self._parse_filename(path.name)
743
+ if meta is None:
744
+ return None
745
+ if domain and meta["domain"] != domain:
746
+ return None
747
+ if safe_filter and meta["entity_id"] not in safe_filter:
748
+ return None
749
+ # Every row reports the id its payload names, not its stem: a filter
750
+ # needs the stored id to settle the stem ambiguity anyway, and a stem
751
+ # shown to the caller — digest or pre-digest — is a value the filter
752
+ # would refuse or misroute, where the stored id is what the settings
753
+ # UI and ha_manage_backup can send straight back and get that entity's
754
+ # whole history. The read is header-only (``_payload_entity_id``); a
755
+ # file that cannot identify itself keeps its stem in an unfiltered
756
+ # listing and is left out of a filtered one.
757
+ payload_id = self._payload_entity_id(path)
758
+ if entity_id and not _id_matches(payload_id, entity_id):
759
+ return None
760
+ if payload_id is not None:
761
+ meta["entity_id"] = payload_id
762
+ try:
763
+ stat = path.stat()
764
+ except OSError:
765
+ return None
766
+ meta["size"] = stat.st_size
767
+ meta["mtime"] = stat.st_mtime
768
+ return meta
769
+
593
770
  def _parse_filename(self, name: str) -> dict[str, Any] | None:
594
771
  m = _FILENAME_RE.match(name)
595
772
  if m is None:
@@ -2272,6 +2449,62 @@ async def _read_legacy_backup(client: Any, filename: str) -> dict[str, Any]:
2272
2449
  return unwrap_service_response(result)
2273
2450
 
2274
2451
 
2452
+ # Blueprints (#2329) — ``ha_manage_blueprints`` can unlink a blueprint file that
2453
+ # core will not rebuild (``action="delete"``) or overwrite one
2454
+ # (``action="save"``), so the pre-write snapshot is the only way back.
2455
+ # ``entity_id`` is the blueprint path inside ``blueprints/<domain>/`` (e.g.
2456
+ # ``user/motion.yaml``) and the snapshot ``config`` is the raw YAML text,
2457
+ # restored through ``blueprint/save``.
2458
+
2459
+
2460
+ async def _fetch_blueprint(client: Any, path: str, domain: str) -> Any:
2461
+ """Fetch a blueprint's YAML for snapshotting, best copy first.
2462
+
2463
+ Delegates to the shared tier ladder in ``tools.blueprint_sources`` — the
2464
+ same one ``ha_manage_blueprints(action="get")`` walks — but only its
2465
+ installed-file tiers: the embedded read, the component's ``blueprint_get``
2466
+ text, and the tools entry's ``read_file``. The ``source_url`` re-fetch
2467
+ ``get`` may fall back to is deliberately NOT a snapshot source: it is what
2468
+ the author publishes now, so a restore from it could silently write
2469
+ different YAML than the delete or overwrite destroyed.
2470
+
2471
+ ``None`` means there is nothing to snapshot (no installed-file tier could
2472
+ serve it); the decorator logs that skip and the write proceeds
2473
+ un-backed-up, which is the best-effort contract.
2474
+ """
2475
+ from .tools.blueprint_sources import resolve_blueprint_source
2476
+
2477
+ found = await resolve_blueprint_source(client, domain, path, source_url=None)
2478
+ return found.text
2479
+
2480
+
2481
+ async def _restore_blueprint(client: Any, path: str, config: Any, domain: str) -> Any:
2482
+ """Re-install a captured blueprint via ``blueprint/save``."""
2483
+ response = await client.send_websocket_message(
2484
+ {
2485
+ "type": "blueprint/save",
2486
+ "domain": domain,
2487
+ "path": path,
2488
+ "yaml": str(config),
2489
+ "allow_override": True,
2490
+ }
2491
+ )
2492
+ if not isinstance(response, dict) or not response.get("success"):
2493
+ error = response.get("error") if isinstance(response, dict) else response
2494
+ raise HomeAssistantError(f"blueprint/save restore failed for {path!r}: {error}")
2495
+ return response.get("result") or {}
2496
+
2497
+
2498
+ def _make_blueprint_handler(domain: str) -> DomainHandler:
2499
+ async def fetch(client: Any, entity_id: str) -> Any:
2500
+ return await _fetch_blueprint(client, entity_id, domain)
2501
+
2502
+ async def restore(client: Any, entity_id: str, config: Any) -> Any:
2503
+ return await _restore_blueprint(client, entity_id, config, domain)
2504
+
2505
+ return DomainHandler(domain=f"blueprint_{domain}", fetch=fetch, restore=restore)
2506
+
2507
+
2275
2508
  def _make_helper_handler(helper_type: str) -> DomainHandler:
2276
2509
  async def fetch(client: Any, entity_id: str) -> Any:
2277
2510
  return await _fetch_helper(client, entity_id, helper_type)
@@ -2325,5 +2558,9 @@ def register_default_handlers(mgr: BackupManager, _client: Any) -> None:
2325
2558
  # edit_yaml_config(replace_file) since write_file rejects config files.
2326
2559
  # Backs the legacy-restore write path and its pre-restore safety snapshot.
2327
2560
  mgr.register(DomainHandler("yaml_file", _fetch_file, _restore_yaml_file))
2561
+ # Blueprint files (#2329): the pre-write snapshot for
2562
+ # ha_manage_blueprints(action="delete" / "save").
2563
+ for blueprint_domain in ("automation", "script"):
2564
+ mgr.register(_make_blueprint_handler(blueprint_domain))
2328
2565
  for helper_type in _KNOWN_HELPER_TYPES:
2329
2566
  mgr.register(_make_helper_handler(helper_type))
@@ -1548,7 +1548,10 @@ _EMBEDDED_CONNECTION: dict[str, str | bool] = {}
1548
1548
 
1549
1549
 
1550
1550
  def set_embedded_connection(
1551
- url: str, token: str, verify_ssl: bool | None = None
1551
+ url: str,
1552
+ token: str,
1553
+ verify_ssl: bool | None = None,
1554
+ config_dir: str | None = None,
1552
1555
  ) -> None:
1553
1556
  """Register the in-process HA connection for embedded mode.
1554
1557
 
@@ -1572,6 +1575,14 @@ def set_embedded_connection(
1572
1575
  for 127.0.0.1, so verification on the loopback connection can only fail.
1573
1576
  ``None`` (the default, and what pre-#1890 components pass implicitly)
1574
1577
  leaves ``Settings.verify_ssl`` alone.
1578
+
1579
+ ``config_dir`` is Home Assistant's own configuration directory (#2329).
1580
+ Embedded mode is its only writer and it is read back through
1581
+ :func:`get_embedded_config_dir` alone — deliberately NOT a ``Settings``
1582
+ field, so no env var or override file can point it anywhere. It lets an
1583
+ in-process server read a blueprint's on-disk YAML directly instead of
1584
+ routing the read through the component, and grants nothing an external
1585
+ server could not already reach.
1575
1586
  """
1576
1587
  _EMBEDDED_CONNECTION["url"] = url
1577
1588
  _EMBEDDED_CONNECTION["token"] = token
@@ -1579,10 +1590,25 @@ def set_embedded_connection(
1579
1590
  _EMBEDDED_CONNECTION.pop("verify_ssl", None)
1580
1591
  else:
1581
1592
  _EMBEDDED_CONNECTION["verify_ssl"] = verify_ssl
1593
+ if config_dir is None:
1594
+ _EMBEDDED_CONNECTION.pop("config_dir", None)
1595
+ else:
1596
+ _EMBEDDED_CONNECTION["config_dir"] = config_dir
1582
1597
  if _settings is not None:
1583
1598
  _apply_embedded_connection(_settings)
1584
1599
 
1585
1600
 
1601
+ def get_embedded_config_dir() -> str | None:
1602
+ """Home Assistant's configuration directory, when running embedded.
1603
+
1604
+ ``None`` outside embedded mode, and on an embedded install whose component
1605
+ predates the ``config_dir`` keyword. Consumers must treat it as an optional
1606
+ capability and fall back to their component / service path.
1607
+ """
1608
+ config_dir = _EMBEDDED_CONNECTION.get("config_dir")
1609
+ return config_dir if isinstance(config_dir, str) and config_dir else None
1610
+
1611
+
1586
1612
  def _reset_embedded_connection() -> None:
1587
1613
  """Drop the registered embedded connection (test seam).
1588
1614
 
@@ -153,6 +153,21 @@ def _updates_write(args: dict[str, Any]) -> str | None:
153
153
  return f"action={action!r}"
154
154
 
155
155
 
156
+ def _blueprint_write(args: dict[str, Any]) -> str | None:
157
+ # Reads (allowed): listing installed blueprints and reading one's
158
+ # metadata/inputs/body. ``substitute`` joins them because it is a pure
159
+ # render — core builds the standalone config in memory and sends it back,
160
+ # touching neither the blueprint store nor any automation/script; writing
161
+ # the rendered config anywhere still needs ha_config_set_automation /
162
+ # ha_config_set_script, which read-only mode blocks. ``import`` (writes a
163
+ # blueprint file), ``save`` (overwrites one with caller-supplied YAML) and
164
+ # ``delete`` stay blocked, and a missing/unknown action fails closed.
165
+ action = args.get("action")
166
+ if action in ("list", "get", "substitute"):
167
+ return None
168
+ return f"action={action!r}"
169
+
170
+
156
171
  def _theme_write(args: dict[str, Any]) -> str | None:
157
172
  # Reads (allowed): listing installed themes and inspecting the screenshot
158
173
  # engine account's saved per-user theme. The latter has no pure-read
@@ -259,6 +274,14 @@ READ_ONLY_EXEMPT_TOOLS: dict[str, ReadOnlyExemption] = {
259
274
  _security_policy_write,
260
275
  "reading the policy document (action='get')",
261
276
  ),
277
+ # Blueprint listing and reading exist only here — ha_get_blueprint was
278
+ # merged into this tool (issue #2329), so hiding it would remove the read
279
+ # surface entirely.
280
+ "ha_manage_blueprints": ReadOnlyExemption(
281
+ _blueprint_write,
282
+ "listing and reading blueprints (action='list' or 'get') and "
283
+ "rendering a standalone config (action='substitute')",
284
+ ),
262
285
  }
263
286
 
264
287
 
@@ -0,0 +1,103 @@
1
+ """Tools whose names moved, and the stored state that has to follow them.
2
+
3
+ Home Assistant 2026.2 renamed add-ons to apps, and this server's two add-on
4
+ tools moved with it. Issue #2329 then folded the two blueprint tools into one
5
+ ``ha_manage_blueprints``. Neither is a breaking change for callers — clients
6
+ resolve by name at runtime, and ``RenamedToolAliasMiddleware`` keeps the old
7
+ name callable for one that has not re-listed yet.
8
+
9
+ What does not repair itself is state a user already stored under the old name:
10
+ a tool they disabled or pinned in the settings UI, a ``DISABLED_TOOLS`` entry,
11
+ a policy rule naming the tool. Those keys are read through the mapping here,
12
+ so the setting survives the rename instead of falling back to the default —
13
+ which for a disabled write tool means silently re-enabling it.
14
+
15
+ A consolidation adds two wrinkles a plain rename does not have. Two retired
16
+ names can now carry different stored states for one current tool, so the
17
+ readers pick the more restrictive one (``rename_retired_keys``'s ``prefer``).
18
+ And a call on a retired name carries the old signature, which the merged tool
19
+ cannot dispatch without the ``action`` it never had —
20
+ ``adapt_retired_arguments`` fills that in.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from collections.abc import Callable, Mapping
26
+ from typing import Any
27
+
28
+ # Retired tool name -> the name that serves it today.
29
+ RENAMED_TOOLS: dict[str, str] = {
30
+ "ha_get_addon": "ha_get_app",
31
+ "ha_manage_addon": "ha_manage_app",
32
+ # #2329: consolidated, not renamed — both retired tools answer to one
33
+ # action-dispatched tool. A call on either old name needs an ``action``
34
+ # (see ``adapt_retired_arguments``), and stored state for the two can
35
+ # disagree (see ``rename_retired_keys``).
36
+ "ha_get_blueprint": "ha_manage_blueprints",
37
+ "ha_import_blueprint": "ha_manage_blueprints",
38
+ }
39
+
40
+
41
+ def current_tool_name(name: str) -> str:
42
+ """The name a tool answers to today, given any name it has had."""
43
+ return RENAMED_TOOLS.get(name, name)
44
+
45
+
46
+ def rename_retired_keys[T](
47
+ states: Mapping[str, T], *, prefer: Callable[[T, T], T] | None = None
48
+ ) -> dict[str, T]:
49
+ """Re-key a per-tool mapping onto the current tool names.
50
+
51
+ A value already stored under the current name wins: it was set against
52
+ the tool as it is now, while the retired key is inherited.
53
+
54
+ When two retired names were folded into one current tool and both carry
55
+ a value, ``prefer(a, b)`` picks the one to keep; readers pass the more
56
+ restrictive choice for their value type (a disabled tool must not come
57
+ back enabled because its sibling was merely pinned). Without ``prefer``
58
+ the later key in iteration order wins, which is only acceptable for
59
+ mappings that cannot collide.
60
+ """
61
+ renamed: dict[str, T] = {}
62
+ for name, value in states.items():
63
+ if name not in RENAMED_TOOLS:
64
+ continue
65
+ current = current_tool_name(name)
66
+ if current in renamed and prefer is not None:
67
+ renamed[current] = prefer(renamed[current], value)
68
+ else:
69
+ renamed[current] = value
70
+ kept = {name: value for name, value in states.items() if name not in RENAMED_TOOLS}
71
+ return {**renamed, **kept}
72
+
73
+
74
+ def _adapt_get_blueprint(arguments: dict[str, Any]) -> dict[str, Any]:
75
+ # ``ha_get_blueprint`` listed without a path and read one with it.
76
+ return {"action": "get" if arguments.get("path") else "list", **arguments}
77
+
78
+
79
+ def _adapt_import_blueprint(arguments: dict[str, Any]) -> dict[str, Any]:
80
+ return {"action": "import", **arguments}
81
+
82
+
83
+ # Retired name -> how its call arguments map onto the current tool's. Only a
84
+ # consolidation needs one; a plain rename forwards the arguments unchanged.
85
+ _RETIRED_ARGUMENT_ADAPTERS: dict[str, Callable[[dict[str, Any]], dict[str, Any]]] = {
86
+ "ha_get_blueprint": _adapt_get_blueprint,
87
+ "ha_import_blueprint": _adapt_import_blueprint,
88
+ }
89
+
90
+
91
+ def adapt_retired_arguments(
92
+ name: str, arguments: Mapping[str, Any] | None
93
+ ) -> dict[str, Any] | None:
94
+ """Rewrite a retired tool's call arguments for the tool that serves it now.
95
+
96
+ ``name`` is the name the caller used. Names without an adapter — current
97
+ names and plain renames — return the arguments untouched (``None`` stays
98
+ ``None``). An explicit ``action`` in the arguments is respected.
99
+ """
100
+ adapter = _RETIRED_ARGUMENT_ADAPTERS.get(name)
101
+ if adapter is None:
102
+ return dict(arguments) if arguments is not None else None
103
+ return adapter(dict(arguments or {}))
@@ -637,10 +637,12 @@ class HomeAssistantSmartMCPServer:
637
637
  ),
638
638
  "ha_config_set_automation": (
639
639
  "create update modify edit automation triggers conditions actions "
640
- "new automation write save"
640
+ "new automation write save take control blueprint detach "
641
+ "unlink standalone convert"
641
642
  ),
642
643
  "ha_config_set_script": (
643
- "create update modify edit script sequence actions new script write save"
644
+ "create update modify edit script sequence actions new script write "
645
+ "save take control blueprint detach unlink standalone convert"
644
646
  ),
645
647
  "ha_config_set_yaml": (
646
648
  "edit yaml configuration.yaml packages template sensor "
@@ -668,6 +670,14 @@ class HomeAssistantSmartMCPServer:
668
670
  "grid solar battery gas water consumption "
669
671
  "number_energy_price entity_energy_price stat_energy_from"
670
672
  ),
673
+ # Old tool names from before the #2329 consolidation, plus the verbs
674
+ # the merged tool gained. An agent that still knows ha_get_blueprint /
675
+ # ha_import_blueprint routes to the replacement instead of failing
676
+ # tool lookup.
677
+ "ha_manage_blueprints": (
678
+ "blueprint blueprints import delete remove unused substitute "
679
+ "take-control list ha_get_blueprint ha_import_blueprint"
680
+ ),
671
681
  # Old tool names from before #1134 consolidation. BM25 retrieval
672
682
  # on agents that still know the previous catalog ("call
673
683
  # ha_list_resources", "use ha_get_skill_home_assistant_best_practices")
@@ -702,11 +712,13 @@ class HomeAssistantSmartMCPServer:
702
712
  ),
703
713
  "ha_config_set_automation": (
704
714
  "Create or update a Home Assistant automation.\n\n"
705
- "Supports two modes: full `config` replacement, or surgical "
715
+ "Supports three modes: full `config` replacement, surgical "
706
716
  "`python_transform` on an existing automation (requires "
707
717
  "`identifier` and `config_hash` from "
708
- "ha_config_get_automation). Omit `identifier` to create a "
709
- "new automation.\n\n"
718
+ "ha_config_get_automation), or `take_control_of_blueprint` "
719
+ "to convert a blueprint-backed automation into an editable "
720
+ "standalone one (the UI's Take control action). Omit "
721
+ "`identifier` to create a new automation.\n\n"
710
722
  "For schema details, examples, and native-vs-template "
711
723
  "guidance, see ha_get_skill_guide or your locally "
712
724
  "installed skills."
@@ -721,10 +733,12 @@ class HomeAssistantSmartMCPServer:
721
733
  ),
722
734
  "ha_config_set_script": (
723
735
  "Create or update a Home Assistant script.\n\n"
724
- "Supports two modes: full `config` replacement, or surgical "
736
+ "Supports three modes: full `config` replacement, surgical "
725
737
  "`python_transform` on an existing script (requires "
726
- "`config_hash` from ha_config_get_script). `script_id` names "
727
- "the script in both modes.\n\n"
738
+ "`config_hash` from ha_config_get_script), or "
739
+ "`take_control_of_blueprint` to convert a blueprint-backed "
740
+ "script into an editable standalone one. `script_id` names "
741
+ "the script in every mode.\n\n"
728
742
  "For schema details and examples, see "
729
743
  "ha_get_skill_guide or your locally installed skills."
730
744
  ),