ha-mcp-dev 8.2.0.dev2352__tar.gz → 8.2.0.dev2354__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 (274) hide show
  1. {ha_mcp_dev-8.2.0.dev2352/src/ha_mcp_dev.egg-info → ha_mcp_dev-8.2.0.dev2354}/PKG-INFO +1 -1
  2. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/pyproject.toml +1 -1
  3. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/client/rest_client.py +90 -21
  4. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/backup.py +5 -1
  5. ha_mcp_dev-8.2.0.dev2354/src/ha_mcp/tools/component_config_entries.py +224 -0
  6. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/component_config_reads.py +10 -2
  7. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/component_devices.py +16 -2
  8. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/component_registries.py +8 -1
  9. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/component_registry_lookup.py +16 -2
  10. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/config_entry_flow.py +50 -14
  11. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/config_entry_flow_walker.py +512 -58
  12. ha_mcp_dev-8.2.0.dev2354/src/ha_mcp/tools/config_entry_identity.py +510 -0
  13. ha_mcp_dev-8.2.0.dev2354/src/ha_mcp/tools/config_entry_reconfigure.py +1012 -0
  14. ha_mcp_dev-8.2.0.dev2354/src/ha_mcp/tools/config_entry_reload_watch.py +210 -0
  15. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/device_control.py +3 -1
  16. ha_mcp_dev-8.2.0.dev2354/src/ha_mcp/tools/integration_reconfigure.py +352 -0
  17. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_blueprints.py +3 -1
  18. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_config_dashboards.py +5 -1
  19. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_config_helpers.py +3 -1
  20. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_dev.py +5 -1
  21. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_entities.py +5 -1
  22. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_integrations.py +124 -4
  23. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_search.py +9 -3
  24. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_service.py +3 -1
  25. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_services.py +5 -1
  26. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_system.py +3 -1
  27. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_voice_assistant.py +3 -1
  28. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/util_helpers.py +5 -1
  29. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  30. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp_dev.egg-info/SOURCES.txt +5 -0
  31. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/LICENSE +0 -0
  32. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/MANIFEST.in +0 -0
  33. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/README.md +0 -0
  34. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/setup.cfg +0 -0
  35. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/__init__.py +0 -0
  36. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/__main__.py +0 -0
  37. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_pypi_marker +0 -0
  38. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_rich_compat.py +0 -0
  39. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/__init__.py +0 -0
  40. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/requirements.txt +0 -0
  41. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/LICENSE +0 -0
  42. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/MANIFEST.sha256 +0 -0
  43. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/VENDORED +0 -0
  44. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/__init__.py +0 -0
  45. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/__main__.py +0 -0
  46. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/asyncio/__init__.py +0 -0
  47. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/asyncio/client.py +0 -0
  48. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/asyncio/connection.py +0 -0
  49. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/asyncio/messages.py +0 -0
  50. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/asyncio/router.py +0 -0
  51. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/asyncio/server.py +0 -0
  52. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/auth.py +0 -0
  53. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/cli.py +0 -0
  54. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/client.py +0 -0
  55. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/connection.py +0 -0
  56. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/datastructures.py +0 -0
  57. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/exceptions.py +0 -0
  58. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/extensions/__init__.py +0 -0
  59. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/extensions/base.py +0 -0
  60. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/extensions/permessage_deflate.py +0 -0
  61. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/frames.py +0 -0
  62. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/headers.py +0 -0
  63. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/http11.py +0 -0
  64. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/imports.py +0 -0
  65. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/legacy/__init__.py +0 -0
  66. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/legacy/auth.py +0 -0
  67. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/legacy/client.py +0 -0
  68. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/legacy/exceptions.py +0 -0
  69. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/legacy/framing.py +0 -0
  70. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/legacy/handshake.py +0 -0
  71. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/legacy/http.py +0 -0
  72. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/legacy/protocol.py +0 -0
  73. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/legacy/server.py +0 -0
  74. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/protocol.py +0 -0
  75. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/proxy.py +0 -0
  76. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/py.typed +0 -0
  77. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/server.py +0 -0
  78. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/streams.py +0 -0
  79. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/sync/__init__.py +0 -0
  80. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/sync/client.py +0 -0
  81. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/sync/connection.py +0 -0
  82. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/sync/messages.py +0 -0
  83. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/sync/router.py +0 -0
  84. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/sync/server.py +0 -0
  85. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/sync/utils.py +0 -0
  86. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/trio/__init__.py +0 -0
  87. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/trio/client.py +0 -0
  88. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/trio/connection.py +0 -0
  89. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/trio/messages.py +0 -0
  90. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/trio/router.py +0 -0
  91. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/trio/server.py +0 -0
  92. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/trio/utils.py +0 -0
  93. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/typing.py +0 -0
  94. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/uri.py +0 -0
  95. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/utils.py +0 -0
  96. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_vendor/websockets/version.py +0 -0
  97. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/_version.py +0 -0
  98. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/auth/__init__.py +0 -0
  99. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/auth/consent_form.py +0 -0
  100. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/auth/oidc_compat.py +0 -0
  101. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/auth/provider.py +0 -0
  102. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/backup_manager.py +0 -0
  103. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/browser_landing.py +0 -0
  104. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/client/__init__.py +0 -0
  105. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/client/supervisor_client.py +0 -0
  106. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/client/websocket_client.py +0 -0
  107. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/client/websocket_listener.py +0 -0
  108. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/config.py +0 -0
  109. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  110. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  111. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
  112. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
  113. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  114. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/dashboard_screenshot/theme_guard.py +0 -0
  115. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/errors.py +0 -0
  116. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/hacs_auto_refresh.py +0 -0
  117. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/llm_exposure.py +0 -0
  118. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/policy/__init__.py +0 -0
  119. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/policy/approval_queue.py +0 -0
  120. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/policy/editing.py +0 -0
  121. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/policy/evaluator.py +0 -0
  122. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/policy/handlers.py +0 -0
  123. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/policy/middleware.py +0 -0
  124. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/policy/model.py +0 -0
  125. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/policy/persistence.py +0 -0
  126. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/policy/value_sources.py +0 -0
  127. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/py.typed +0 -0
  128. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/read_only.py +0 -0
  129. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/redaction.py +0 -0
  130. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  131. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  132. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  133. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  134. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  135. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  136. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  137. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  138. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  139. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  140. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  141. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  142. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
  143. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  144. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
  145. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  146. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  147. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  148. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  149. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  150. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  151. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  152. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
  153. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  154. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  155. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/server.py +0 -0
  156. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/__init__.py +0 -0
  157. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
  158. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
  159. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
  160. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
  161. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
  162. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
  163. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/_i18n.py +0 -0
  164. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/_persistence.py +0 -0
  165. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
  166. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/_theme.py +0 -0
  167. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
  168. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/locales/README.md +0 -0
  169. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/locales/cs.json +0 -0
  170. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/locales/de.json +0 -0
  171. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/locales/en.json +0 -0
  172. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/locales/eo.json +0 -0
  173. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/locales/es.json +0 -0
  174. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/locales/fr.json +0 -0
  175. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/locales/it.json +0 -0
  176. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/locales/nl.json +0 -0
  177. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/locales/pl.json +0 -0
  178. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/locales/ru.json +0 -0
  179. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/locales/sv.json +0 -0
  180. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/locales/zh-Hans.json +0 -0
  181. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/settings.css +0 -0
  182. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/settings.html +0 -0
  183. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/settings_ui/settings.js +0 -0
  184. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/smoke_test.py +0 -0
  185. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  186. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/strict_bps.py +0 -0
  187. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/__init__.py +0 -0
  188. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/auto_backup.py +0 -0
  189. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  190. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/best_practice_result.py +0 -0
  191. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/best_practice_variables.py +0 -0
  192. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/component_api.py +0 -0
  193. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/config_entry_flow_form.py +0 -0
  194. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/config_entry_flow_menu.py +0 -0
  195. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/error_log_parsing.py +0 -0
  196. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/hacs_registration.py +0 -0
  197. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/helpers.py +0 -0
  198. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/radio/__init__.py +0 -0
  199. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/radio/base.py +0 -0
  200. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/radio/matter.py +0 -0
  201. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/radio/thread.py +0 -0
  202. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/radio/zigbee.py +0 -0
  203. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/radio/zwave.py +0 -0
  204. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/reference_validator.py +0 -0
  205. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/registry.py +0 -0
  206. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  207. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  208. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  209. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
  210. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
  211. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  212. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  213. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  214. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  215. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
  216. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_addons.py +0 -0
  217. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_areas.py +0 -0
  218. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_assist_pipeline.py +0 -0
  219. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  220. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_calendar.py +0 -0
  221. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_camera.py +0 -0
  222. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_categories.py +0 -0
  223. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_code.py +0 -0
  224. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  225. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  226. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  227. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  228. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_energy.py +0 -0
  229. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  230. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_groups.py +0 -0
  231. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_hacs.py +0 -0
  232. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_history.py +0 -0
  233. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_labels.py +0 -0
  234. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_radio.py +0 -0
  235. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_registry.py +0 -0
  236. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_resources.py +0 -0
  237. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_security_policy.py +0 -0
  238. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_themes.py +0 -0
  239. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_todo.py +0 -0
  240. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_traces.py +0 -0
  241. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_updates.py +0 -0
  242. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_utility.py +0 -0
  243. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  244. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
  245. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/tools_zones.py +0 -0
  246. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/tools/validation_middleware.py +0 -0
  247. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/transforms/__init__.py +0 -0
  248. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/transforms/categorized_search.py +0 -0
  249. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  250. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/transport_security.py +0 -0
  251. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/update_check.py +0 -0
  252. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/utils/__init__.py +0 -0
  253. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/utils/config_hash.py +0 -0
  254. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/utils/config_write_lock.py +0 -0
  255. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/utils/data_paths.py +0 -0
  256. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/utils/domain_handlers.py +0 -0
  257. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  258. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  259. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/utils/operation_manager.py +0 -0
  260. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/utils/python_sandbox.py +0 -0
  261. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/utils/skill_loader.py +0 -0
  262. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/utils/usage_logger.py +0 -0
  263. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/visibility/__init__.py +0 -0
  264. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/visibility/enforcement.py +0 -0
  265. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/visibility/model.py +0 -0
  266. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/visibility/persistence.py +0 -0
  267. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp/visibility/resolver.py +0 -0
  268. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  269. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  270. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  271. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  272. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/tests/__init__.py +0 -0
  273. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/tests/test_constants.py +0 -0
  274. {ha_mcp_dev-8.2.0.dev2352 → ha_mcp_dev-8.2.0.dev2354}/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.2.0.dev2352
3
+ Version: 8.2.0.dev2354
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.2.0.dev2352"
7
+ version = "8.2.0.dev2354"
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
@@ -36,9 +36,21 @@ def _is_ssl_error(exc: BaseException) -> bool:
36
36
  logger = logging.getLogger(__name__)
37
37
 
38
38
  # Transient gateway statuses from a reverse proxy / Supervisor ingress — HA Core
39
- # restarting or briefly overloaded behind it. The upstream couldn't be reached,
40
- # so the request did not execute and retrying is safe even for writes.
41
- _RETRYABLE_STATUS = frozenset({502, 503, 504})
39
+ # restarting or briefly overloaded behind it. Retried for SAFE METHODS ONLY.
40
+ #
41
+ # None of them proves Home Assistant did not execute the request: RFC 9110 has
42
+ # 502 as "received an invalid response from an inbound server", which a proxy
43
+ # only sends after reaching it, and 504 as the upstream answering too slowly.
44
+ # #1623 extended the retry to writes on the premise that "a gateway 5xx means
45
+ # the request never reached the backend"; that premise is false, and replaying
46
+ # a write can double-apply it — fire an event twice, run a script twice, or
47
+ # turn a completed DELETE into a misleading 404 on the replay.
48
+ #
49
+ # The flake class #1623 fixed was a 502 storm failing ~190 tests at once, which
50
+ # is overwhelmingly reads; those keep the retry. A write that fails loudly is
51
+ # recoverable by the caller, which a silent double-apply is not.
52
+ _RETRYABLE_GATEWAY_STATUS = frozenset({502, 503, 504})
53
+ _SAFE_METHODS = frozenset({"GET", "HEAD", "OPTIONS"})
42
54
  _MAX_REQUEST_ATTEMPTS = 3
43
55
  # Journald window requested for the Core error log on Supervisor-backed
44
56
  # installs. Both such branches of get_error_log() build their request from this
@@ -304,7 +316,8 @@ class HomeAssistantClient:
304
316
  Handles auth, HTTP 4xx/5xx, and transport errors in one place.
305
317
  Callers parse the body themselves (JSON via `_request`, text via
306
318
  `get_addon_logs`, etc.). Transient gateway errors (502/503/504) are
307
- retried with bounded exponential backoff before surfacing.
319
+ retried with bounded exponential backoff for safe methods only; a write
320
+ is never replayed.
308
321
 
309
322
  Raises:
310
323
  HomeAssistantAuthError: 401 response.
@@ -323,10 +336,11 @@ class HomeAssistantClient:
323
336
  if response.status_code >= 400:
324
337
  message, error_data = self._error_message_from_response(response)
325
338
 
326
- if (
327
- response.status_code in _RETRYABLE_STATUS
328
- and attempt < _MAX_REQUEST_ATTEMPTS
329
- ):
339
+ retryable = (
340
+ response.status_code in _RETRYABLE_GATEWAY_STATUS
341
+ and method.upper() in _SAFE_METHODS
342
+ )
343
+ if retryable and attempt < _MAX_REQUEST_ATTEMPTS:
330
344
  logger.warning(
331
345
  f"Transient {response.status_code} from Home Assistant "
332
346
  f"(attempt {attempt}/{_MAX_REQUEST_ATTEMPTS}), retrying "
@@ -1211,6 +1225,28 @@ class HomeAssistantClient:
1211
1225
  logger.debug(f"Starting config flow for handler: {handler}")
1212
1226
  return await self._request("POST", "/config/config_entries/flow", json=payload)
1213
1227
 
1228
+ async def start_reconfigure_flow(
1229
+ self, handler: str, entry_id: str
1230
+ ) -> dict[str, Any]:
1231
+ """Start Home Assistant's official reconfigure flow for an entry.
1232
+
1233
+ Home Assistant selects ``SOURCE_RECONFIGURE`` when ``entry_id`` is
1234
+ included in the config-flow start payload. The integration's
1235
+ ``async_step_reconfigure`` then owns validation and updates the
1236
+ existing entry in place; this method deliberately does not edit
1237
+ storage or delete/recreate entries.
1238
+ """
1239
+ logger.debug(
1240
+ "Starting reconfigure flow for handler %s and entry %s",
1241
+ handler,
1242
+ entry_id,
1243
+ )
1244
+ return await self._request(
1245
+ "POST",
1246
+ "/config/config_entries/flow",
1247
+ json={"handler": handler, "entry_id": entry_id},
1248
+ )
1249
+
1214
1250
  async def submit_config_flow_step(
1215
1251
  self, flow_id: str, user_input: dict[str, Any]
1216
1252
  ) -> dict[str, Any]:
@@ -1228,6 +1264,10 @@ class HomeAssistantClient:
1228
1264
  HomeAssistantAPIError: If flow submission fails
1229
1265
  """
1230
1266
  logger.debug(f"Submitting flow step for flow_id: {flow_id}")
1267
+ # POST, so no gateway retry — see _RETRYABLE_GATEWAY_STATUS. This is
1268
+ # the call that motivated the rule: HA consumes the flow_id on success,
1269
+ # so a replay returns 404 "Invalid flow specified", a definitive-looking
1270
+ # 4xx that hides a first attempt which may already have committed.
1231
1271
  return await self._request(
1232
1272
  "POST", f"/config/config_entries/flow/{flow_id}", json=user_input
1233
1273
  )
@@ -1382,6 +1422,43 @@ class HomeAssistantClient:
1382
1422
  }
1383
1423
  )
1384
1424
 
1425
+ async def list_config_entries(self) -> list[dict[str, Any]]:
1426
+ """List all config entries from Home Assistant."""
1427
+ logger.debug("Listing Home Assistant config entries")
1428
+ entries: Any = await self._request("GET", "/config/config_entries/entry")
1429
+ if not isinstance(entries, list):
1430
+ raise HomeAssistantAPIError(
1431
+ "Unexpected response format from config entries API",
1432
+ status_code=500,
1433
+ )
1434
+ return [dict(entry) for entry in entries if isinstance(entry, dict)]
1435
+
1436
+ async def _list_registry(self, ws_type: str, label: str) -> list[dict[str, Any]]:
1437
+ """Read a registry list, raising rather than returning a partial one.
1438
+
1439
+ Callers use "empty" to mean the entry genuinely has nothing registered,
1440
+ so a malformed response must never degrade to [].
1441
+ """
1442
+ response = await self.send_websocket_message({"type": ws_type})
1443
+ result: Any = response.get("result") if isinstance(response, dict) else None
1444
+ if not isinstance(result, list) or not all(
1445
+ isinstance(item, dict) for item in result
1446
+ ):
1447
+ detail = response.get("error") if isinstance(response, dict) else response
1448
+ raise HomeAssistantAPIError(
1449
+ f"Unexpected response from {label} registry API: {detail!r}",
1450
+ status_code=500,
1451
+ )
1452
+ return [dict(item) for item in result]
1453
+
1454
+ async def list_entity_registry(self) -> list[dict[str, Any]]:
1455
+ """List Home Assistant's entity registry through the official WebSocket API."""
1456
+ return await self._list_registry("config/entity_registry/list", "entity")
1457
+
1458
+ async def list_device_registry(self) -> list[dict[str, Any]]:
1459
+ """List Home Assistant's device registry through the official WebSocket API."""
1460
+ return await self._list_registry("config/device_registry/list", "device")
1461
+
1385
1462
  async def get_config_entry(self, entry_id: str) -> dict[str, Any]:
1386
1463
  """
1387
1464
  Get config entry details.
@@ -1393,25 +1470,17 @@ class HomeAssistantClient:
1393
1470
  entry_id: Config entry ID
1394
1471
 
1395
1472
  Returns:
1396
- Full config entry data
1473
+ Home Assistant's config-entry fragment (``ConfigEntry.as_json_fragment``):
1474
+ identity, state and capability flags. It carries NO ``data`` key, so the
1475
+ entry's connection settings and credentials are not in it.
1397
1476
 
1398
1477
  Raises:
1399
1478
  HomeAssistantAPIError: If entry not found or API error
1400
1479
  """
1401
1480
  logger.debug(f"Getting config entry: {entry_id}")
1402
- # List all entries and filter by entry_id.
1403
- # Typed as Any because _request returns dict[str, Any] generically,
1404
- # but this endpoint actually returns a list.
1405
- entries: Any = await self._request("GET", "/config/config_entries/entry")
1406
-
1407
- if not isinstance(entries, list):
1408
- raise HomeAssistantAPIError(
1409
- "Unexpected response format from config entries API",
1410
- status_code=500,
1411
- )
1412
-
1481
+ entries = await self.list_config_entries()
1413
1482
  found: dict[str, Any] | None = next(
1414
- (dict(e) for e in entries if e.get("entry_id") == entry_id), None
1483
+ (entry for entry in entries if entry.get("entry_id") == entry_id), None
1415
1484
  )
1416
1485
  if found is None:
1417
1486
  raise HomeAssistantAPIError(
@@ -255,7 +255,11 @@ async def _backup_prep_via_component(
255
255
  if not component_supports(caps, "backup_prep"):
256
256
  return None
257
257
  try:
258
- ws = await get_websocket_client(url=client.base_url, token=client.token)
258
+ ws = await get_websocket_client(
259
+ url=client.base_url,
260
+ token=client.token,
261
+ verify_ssl=getattr(client, "verify_ssl", None),
262
+ )
259
263
  raw = await ws.send_command(WS_BACKUP_PREP)
260
264
  except (HomeAssistantCommandError, HomeAssistantCommandTimeout) as exc:
261
265
  if is_unknown_command(exc):
@@ -0,0 +1,224 @@
1
+ """Config-entry reads over the ``ha_mcp_tools`` component gate.
2
+
3
+ Exists for one field: ``unique_id``. Home Assistant deliberately withholds it
4
+ from every config-entry endpoint — the REST list, ``config_entries/get`` and
5
+ ``config_entries/get_single`` all serialize ``ConfigEntry.as_json_fragment``,
6
+ which has no ``unique_id`` key — so a server that wants it as an identity
7
+ anchor has no source but the component, which holds the live ``ConfigEntry``.
8
+
9
+ The component's ``config_entries`` command returns that field as a deliberate
10
+ superset of the fragment. This module owns the caps-gated fetch, following the
11
+ routing discipline the sibling component modules established: probe caps, send
12
+ one frame, invalidate on ``unknown_command``, fall back on any component error.
13
+
14
+ **Three states, and callers must keep them apart.** ``unique_id`` is not a
15
+ two-valued field here:
16
+
17
+ - **known, with a value** — the component answered and the entry has one.
18
+ - **known to be absent** — the component answered and the entry has none
19
+ (MQTT-style entries genuinely have no unique_id). The key is present, the
20
+ value is ``None``.
21
+ - **unknown** — no component, an older component predating the field, or a
22
+ transport failure. Nothing can be concluded.
23
+
24
+ :func:`fetch_config_entry_unique_id` returns exactly that distinction so a
25
+ caller never mistakes "we could not read it" for "the entry has none". That is
26
+ what lets the field be additive within ``schema_version`` 1 with no version
27
+ gate: an older component omits the KEY, which is distinguishable from sending
28
+ it as ``None`` — the same discipline as ``device_get``'s opt-in entities join.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import logging
34
+ from typing import Any, NamedTuple
35
+
36
+ from ..client.rest_client import (
37
+ HomeAssistantCommandError,
38
+ HomeAssistantCommandTimeout,
39
+ )
40
+ from ..client.websocket_client import get_websocket_client
41
+ from .component_api import (
42
+ component_supports,
43
+ get_component_caps,
44
+ invalidate_caps,
45
+ is_unknown_command,
46
+ )
47
+
48
+ logger = logging.getLogger(__name__)
49
+
50
+ WS_CONFIG_ENTRIES = "ha_mcp_tools/config_entries"
51
+
52
+
53
+ class EntryUniqueId(NamedTuple):
54
+ """A config entry's ``unique_id``, and whether it could be read at all."""
55
+
56
+ #: False when no component answered, or the component predates the field.
57
+ known: bool
58
+ #: The value when ``known``; always ``None`` when not.
59
+ value: str | None
60
+
61
+
62
+ UNKNOWN_UNIQUE_ID = EntryUniqueId(known=False, value=None)
63
+
64
+
65
+ async def _config_entry_rows(client: Any, **query: Any) -> list[dict[str, Any]] | None:
66
+ """One ``config_entries`` read; ``None`` when the answer cannot be trusted."""
67
+ caps = await get_component_caps(client)
68
+ if not component_supports(caps, "config_entries"):
69
+ return None
70
+ try:
71
+ ws = await get_websocket_client(
72
+ url=client.base_url,
73
+ token=client.token,
74
+ # verify_ssl keys the client pool, so omitting it hands back a
75
+ # DIFFERENT pooled client than the capability probe used and a
76
+ # self-signed setup fails the read after passing detection.
77
+ verify_ssl=getattr(client, "verify_ssl", None),
78
+ )
79
+ raw = await ws.send_command(WS_CONFIG_ENTRIES, **query)
80
+ except (HomeAssistantCommandError, HomeAssistantCommandTimeout) as exc:
81
+ if is_unknown_command(exc):
82
+ invalidate_caps(client)
83
+ else:
84
+ logger.warning("%s failed: %r", WS_CONFIG_ENTRIES, exc)
85
+ return None
86
+ except Exception as exc:
87
+ logger.warning("%s connection error: %r", WS_CONFIG_ENTRIES, exc)
88
+ return None
89
+ result = raw.get("result") if isinstance(raw, dict) else None
90
+ entries = result.get("entries") if isinstance(result, dict) else None
91
+ if not isinstance(entries, list):
92
+ return None
93
+ if any(not isinstance(row, dict) for row in entries):
94
+ logger.debug("%s returned a malformed row", WS_CONFIG_ENTRIES)
95
+ return None
96
+ return list(entries)
97
+
98
+
99
+ async def fetch_domain_unique_ids(client: Any, domain: str) -> dict[str, str] | None:
100
+ """Map ``entry_id -> unique_id`` for one domain, or ``None`` if unreadable.
101
+
102
+ One read, not one per candidate. The duplicate scan needs the unique_id of
103
+ every same-domain entry, and Home Assistant's own config-entry rows carry
104
+ none — so without this the scan can only ever compare entry_ids and must
105
+ not claim it checked unique_ids.
106
+
107
+ A genuinely ``None`` ``unique_id`` is the ONLY value omitted from the map,
108
+ so a present key always means a real value. Anything malformed returns
109
+ ``None`` for the whole map instead: the duplicate scan reads a returned map
110
+ as a COMPLETED check, so an incomplete one could drop the very duplicate
111
+ row it exists to catch.
112
+ """
113
+ rows = await _config_entry_rows(client, domain=domain)
114
+ if rows is None:
115
+ return None
116
+ mapping: dict[str, str] = {}
117
+ for row in rows:
118
+ if "unique_id" not in row:
119
+ # An older component: the field is missing wholesale.
120
+ logger.debug(
121
+ "%s rows for domain %s predate the unique_id field",
122
+ WS_CONFIG_ENTRIES,
123
+ domain,
124
+ )
125
+ return None
126
+ entry_id = row.get("entry_id")
127
+ unique_id = row["unique_id"]
128
+ if not isinstance(entry_id, str) or (
129
+ unique_id is not None and not isinstance(unique_id, str)
130
+ ):
131
+ # A partial map would be read as a COMPLETED scan by the duplicate
132
+ # check, so a malformed duplicate row could be dropped and let
133
+ # verification pass. Incomplete must be indistinguishable from
134
+ # unreadable.
135
+ logger.debug(
136
+ "%s returned a malformed row for domain %s; map unusable",
137
+ WS_CONFIG_ENTRIES,
138
+ domain,
139
+ )
140
+ return None
141
+ # unique_id None is a valid answer (the entry has none) and is the one
142
+ # value legitimately omitted from the map.
143
+ if unique_id is not None:
144
+ mapping[entry_id] = unique_id
145
+ return mapping
146
+
147
+
148
+ async def fetch_config_entry_unique_id(client: Any, entry_id: str) -> EntryUniqueId:
149
+ """Read one config entry's ``unique_id`` through the component.
150
+
151
+ Returns :data:`UNKNOWN_UNIQUE_ID` whenever the answer cannot be trusted —
152
+ no ``config_entries`` capability, a downgraded component, a transport
153
+ failure, a malformed payload, or a row that predates the field. Never
154
+ raises: an unreadable ``unique_id`` degrades the anchor, and the caller
155
+ decides what that means, rather than failing the whole reconfigure here.
156
+ """
157
+ caps = await get_component_caps(client)
158
+ if not component_supports(caps, "config_entries"):
159
+ return UNKNOWN_UNIQUE_ID
160
+ try:
161
+ ws = await get_websocket_client(
162
+ url=client.base_url,
163
+ token=client.token,
164
+ # verify_ssl keys the client pool, so omitting it hands back a
165
+ # DIFFERENT pooled client than the capability probe used and a
166
+ # self-signed setup fails the read after passing detection.
167
+ verify_ssl=getattr(client, "verify_ssl", None),
168
+ )
169
+ raw = await ws.send_command(WS_CONFIG_ENTRIES, entry_id=entry_id)
170
+ except (HomeAssistantCommandError, HomeAssistantCommandTimeout) as exc:
171
+ if is_unknown_command(exc):
172
+ invalidate_caps(client)
173
+ else:
174
+ logger.warning(
175
+ "%s failed; unique_id unavailable: %r", WS_CONFIG_ENTRIES, exc
176
+ )
177
+ return UNKNOWN_UNIQUE_ID
178
+ except Exception as exc:
179
+ logger.warning(
180
+ "%s connection error; unique_id unavailable: %r", WS_CONFIG_ENTRIES, exc
181
+ )
182
+ return UNKNOWN_UNIQUE_ID
183
+
184
+ result = raw.get("result") if isinstance(raw, dict) else None
185
+ entries = result.get("entries") if isinstance(result, dict) else None
186
+ if not isinstance(entries, list) or not entries:
187
+ logger.debug(
188
+ "%s returned no row for %s; unique_id unavailable",
189
+ WS_CONFIG_ENTRIES,
190
+ entry_id,
191
+ )
192
+ return UNKNOWN_UNIQUE_ID
193
+ row = entries[0]
194
+ # Key presence is the capability probe: a component older than the field
195
+ # omits it entirely, which must not read as "this entry has no unique_id".
196
+ if not isinstance(row, dict) or "unique_id" not in row:
197
+ logger.debug(
198
+ "%s row for %s predates the unique_id field; unique_id unavailable",
199
+ WS_CONFIG_ENTRIES,
200
+ entry_id,
201
+ )
202
+ return UNKNOWN_UNIQUE_ID
203
+ if row.get("entry_id") != entry_id:
204
+ # Reading another entry's anchor would be worse than reading none.
205
+ logger.debug(
206
+ "%s answered for %r when %r was asked; unique_id unavailable",
207
+ WS_CONFIG_ENTRIES,
208
+ row.get("entry_id"),
209
+ entry_id,
210
+ )
211
+ return UNKNOWN_UNIQUE_ID
212
+ value = row["unique_id"]
213
+ if value is not None and not isinstance(value, str):
214
+ # Malformed is unknown, NOT known-absent: silently turning junk into
215
+ # "this entry has no unique_id" is the exact conflation this type
216
+ # exists to prevent.
217
+ logger.debug(
218
+ "%s returned a non-string unique_id (%r) for %s; unique_id unavailable",
219
+ WS_CONFIG_ENTRIES,
220
+ value,
221
+ entry_id,
222
+ )
223
+ return UNKNOWN_UNIQUE_ID
224
+ return EntryUniqueId(known=True, value=value)
@@ -117,7 +117,11 @@ async def fetch_entity_lookup_via_component(
117
117
  if domain is not None:
118
118
  kwargs["domain"] = domain
119
119
  try:
120
- ws = await get_websocket_client(url=client.base_url, token=client.token)
120
+ ws = await get_websocket_client(
121
+ url=client.base_url,
122
+ token=client.token,
123
+ verify_ssl=getattr(client, "verify_ssl", None),
124
+ )
121
125
  raw = await ws.send_command(WS_ENTITY_LOOKUP, **kwargs)
122
126
  except (HomeAssistantCommandError, HomeAssistantCommandTimeout) as exc:
123
127
  if is_unknown_command(exc):
@@ -177,7 +181,11 @@ async def fetch_reference_data_via_component(
177
181
  if not include_states:
178
182
  kwargs["include_states"] = False
179
183
  try:
180
- ws = await get_websocket_client(url=client.base_url, token=client.token)
184
+ ws = await get_websocket_client(
185
+ url=client.base_url,
186
+ token=client.token,
187
+ verify_ssl=getattr(client, "verify_ssl", None),
188
+ )
181
189
  raw = await ws.send_command(WS_REFERENCE_DATA, **kwargs)
182
190
  except (HomeAssistantCommandError, HomeAssistantCommandTimeout) as exc:
183
191
  if is_unknown_command(exc):
@@ -91,7 +91,14 @@ async def fetch_device_via_component(
91
91
  if include_entities:
92
92
  kwargs["include_entities"] = True
93
93
  try:
94
- ws = await get_websocket_client(url=client.base_url, token=client.token)
94
+ ws = await get_websocket_client(
95
+ url=client.base_url,
96
+ token=client.token,
97
+ # verify_ssl keys the client pool, so omitting it hands back a
98
+ # DIFFERENT pooled client than the capability probe used and a
99
+ # self-signed setup fails the read after passing detection.
100
+ verify_ssl=getattr(client, "verify_ssl", None),
101
+ )
95
102
  raw = await ws.send_command(WS_DEVICE_GET, **kwargs)
96
103
  except (HomeAssistantCommandError, HomeAssistantCommandTimeout) as exc:
97
104
  if is_unknown_command(exc):
@@ -163,7 +170,14 @@ async def fetch_device_list_via_component(client: Any) -> dict[str, Any] | None:
163
170
  if not component_supports(caps, "device_list"):
164
171
  return None
165
172
  try:
166
- ws = await get_websocket_client(url=client.base_url, token=client.token)
173
+ ws = await get_websocket_client(
174
+ url=client.base_url,
175
+ token=client.token,
176
+ # verify_ssl keys the client pool, so omitting it hands back a
177
+ # DIFFERENT pooled client than the capability probe used and a
178
+ # self-signed setup fails the read after passing detection.
179
+ verify_ssl=getattr(client, "verify_ssl", None),
180
+ )
167
181
  raw = await ws.send_command(WS_DEVICE_LIST)
168
182
  except (HomeAssistantCommandError, HomeAssistantCommandTimeout) as exc:
169
183
  if is_unknown_command(exc):
@@ -117,7 +117,14 @@ async def fetch_registries_via_component(
117
117
  if category_scopes:
118
118
  kwargs["category_scopes"] = list(category_scopes)
119
119
  try:
120
- ws = await get_websocket_client(url=client.base_url, token=client.token)
120
+ ws = await get_websocket_client(
121
+ url=client.base_url,
122
+ token=client.token,
123
+ # verify_ssl keys the client pool, so omitting it hands back a
124
+ # DIFFERENT pooled client than the capability probe used and a
125
+ # self-signed setup fails the read after passing detection.
126
+ verify_ssl=getattr(client, "verify_ssl", None),
127
+ )
121
128
  raw = await ws.send_command(WS_REGISTRIES, **kwargs)
122
129
  except (HomeAssistantCommandError, HomeAssistantCommandTimeout) as exc:
123
130
  if is_unknown_command(exc):
@@ -85,7 +85,14 @@ async def fetch_entities_for_config_entry_via_component(
85
85
  if not component_supports(caps, "registry_lookup"):
86
86
  return None
87
87
  try:
88
- ws = await get_websocket_client(url=client.base_url, token=client.token)
88
+ ws = await get_websocket_client(
89
+ url=client.base_url,
90
+ token=client.token,
91
+ # verify_ssl keys the client pool, so omitting it hands back a
92
+ # DIFFERENT pooled client than the capability probe used and a
93
+ # self-signed setup fails the read after passing detection.
94
+ verify_ssl=getattr(client, "verify_ssl", None),
95
+ )
89
96
  raw = await ws.send_command(WS_REGISTRY_LOOKUP, config_entry_id=config_entry_id)
90
97
  except (HomeAssistantCommandError, HomeAssistantCommandTimeout) as exc:
91
98
  if is_unknown_command(exc):
@@ -133,7 +140,14 @@ async def resolve_entities_via_component(
133
140
  if not component_supports(caps, "registry_lookup"):
134
141
  return None
135
142
  try:
136
- ws = await get_websocket_client(url=client.base_url, token=client.token)
143
+ ws = await get_websocket_client(
144
+ url=client.base_url,
145
+ token=client.token,
146
+ # verify_ssl keys the client pool, so omitting it hands back a
147
+ # DIFFERENT pooled client than the capability probe used and a
148
+ # self-signed setup fails the read after passing detection.
149
+ verify_ssl=getattr(client, "verify_ssl", None),
150
+ )
137
151
  raw = await ws.send_command(WS_REGISTRY_LOOKUP, entity_ids=entity_ids)
138
152
  except (HomeAssistantCommandError, HomeAssistantCommandTimeout) as exc:
139
153
  if is_unknown_command(exc):
@@ -11,9 +11,12 @@ for the 17 helper types listed in FLOW_HELPER_TYPES.
11
11
 
12
12
  The same flow walkers drive every other config-entry surface, not just
13
13
  helpers: ``ha_set_integration`` creates entries for arbitrary domains through
14
- ``create_config_entry`` and edits them through ``update_config_entry_options``,
15
- and ``ha_config_set_helper(helper_type="config_subentry")`` drives subentry
16
- flows through ``set_config_subentry``.
14
+ ``create_config_entry`` and edits them through ``update_config_entry_options``;
15
+ ``ha_config_set_helper(helper_type="config_subentry")`` drives subentry flows
16
+ through ``set_config_subentry``. Changing a live entry's connection settings is
17
+ a different problem — it commits in place and Home Assistant reloads afterwards
18
+ — and lives in ``config_entry_reconfigure``, which depends on this module for
19
+ the shared flow-abort and sentinel-rejection helpers.
17
20
 
18
21
  The step machinery those entry points drive lives in three sibling modules,
19
22
  imported in one direction only (menu <- form <- walker <- here):
@@ -25,13 +28,17 @@ imported in one direction only (menu <- form <- walker <- here):
25
28
  """
26
29
 
27
30
  import asyncio
31
+ import json
28
32
  import logging
29
33
  from typing import Any, Literal
30
34
 
35
+ from fastmcp.exceptions import ToolError
36
+
31
37
  from ..errors import ErrorCode, create_error_response
32
38
  from ..redaction import sentinel_option_keys
33
39
  from .config_entry_flow_form import _extract_schema_field_names
34
40
  from .config_entry_flow_walker import (
41
+ POST_COMMIT_STATUSES,
35
42
  _FlowType,
36
43
  _handle_config_subentry_flow_steps,
37
44
  _handle_flow_steps,
@@ -41,6 +48,26 @@ from .helpers import raise_tool_error
41
48
  logger = logging.getLogger(__name__)
42
49
 
43
50
 
51
+ async def _abort_flow_best_effort(client: Any, flow_id: str) -> None:
52
+ """Abort a still-pending flow without hiding the original failure."""
53
+ try:
54
+ await asyncio.wait_for(client.abort_config_flow(flow_id), timeout=5.0)
55
+ except Exception as abort_err:
56
+ logger.warning("Failed to abort flow %s after error: %s", flow_id, abort_err)
57
+
58
+
59
+ async def _abort_subentry_flow_best_effort(client: Any, flow_id: str) -> None:
60
+ """Abort a pending subentry flow without hiding the original failure."""
61
+ try:
62
+ await asyncio.wait_for(client.abort_config_subentry_flow(flow_id), timeout=5.0)
63
+ except Exception as abort_err:
64
+ logger.warning(
65
+ "Failed to abort config subentry flow %s after error: %s",
66
+ flow_id,
67
+ abort_err,
68
+ )
69
+
70
+
44
71
  def _reject_redaction_sentinels(config_dict: dict[str, Any]) -> None:
45
72
  """Reject config values that are redaction placeholders (#2157).
46
73
 
@@ -138,6 +165,11 @@ async def set_config_subentry(
138
165
  subentry, provided reconfigures that existing subentry.
139
166
  ``show_advanced_options`` is a no-op on HA 2026.6+ and kept only for older
140
167
  HA versions pending removal before HA 2027.6.
168
+
169
+ The reconfigure branch fails when the flow leaves any supplied config key
170
+ unconsumed, where it previously returned success plus a warning — see
171
+ :func:`_handle_config_subentry_flow_steps` for why. The create branch is
172
+ unchanged.
141
173
  """
142
174
  _reject_redaction_sentinels(config_dict)
143
175
  flow_result = await client.start_config_subentry_flow(
@@ -174,17 +206,21 @@ async def set_config_subentry(
174
206
  config_dict,
175
207
  is_reconfigure=subentry_id is not None,
176
208
  )
177
- except Exception:
178
- try:
179
- await asyncio.wait_for(
180
- client.abort_config_subentry_flow(flow_id), timeout=5.0
181
- )
182
- except Exception as abort_err:
183
- logger.warning(
184
- "Failed to abort config subentry flow %s after error: %s",
185
- flow_id,
186
- abort_err,
187
- )
209
+ except asyncio.CancelledError:
210
+ await _abort_subentry_flow_best_effort(client, flow_id)
211
+ raise
212
+ except Exception as flow_error:
213
+ payload: dict[str, Any] = {}
214
+ if isinstance(flow_error, ToolError):
215
+ try:
216
+ parsed_payload = json.loads(str(flow_error))
217
+ except (TypeError, ValueError):
218
+ parsed_payload = {}
219
+ if isinstance(parsed_payload, dict):
220
+ payload = parsed_payload
221
+ post_commit_status = payload.get("status") in POST_COMMIT_STATUSES
222
+ if payload.get("flow_budget_exhausted") or not post_commit_status:
223
+ await _abort_subentry_flow_best_effort(client, flow_id)
188
224
  raise
189
225
 
190
226
  response = {