ha-mcp-dev 8.0.0.dev2228__tar.gz → 8.0.0.dev2233__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 (199) hide show
  1. {ha_mcp_dev-8.0.0.dev2228/src/ha_mcp_dev.egg-info → ha_mcp_dev-8.0.0.dev2233}/PKG-INFO +1 -1
  2. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/pyproject.toml +1 -1
  3. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/__main__.py +12 -5
  4. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/client/websocket_client.py +30 -2
  5. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/settings_ui/locales/de.json +1 -1
  6. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/settings_ui/locales/es.json +1 -1
  7. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/settings_ui/locales/fr.json +1 -1
  8. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/settings_ui/locales/it.json +1 -1
  9. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/settings_ui/locales/ru.json +1 -1
  10. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/settings_ui/locales/zh-Hans.json +1 -1
  11. ha_mcp_dev-8.0.0.dev2233/src/ha_mcp/tools/config_entry_flow.py +376 -0
  12. ha_mcp_dev-8.0.0.dev2233/src/ha_mcp/tools/config_entry_flow_form.py +694 -0
  13. ha_mcp_dev-8.0.0.dev2233/src/ha_mcp/tools/config_entry_flow_menu.py +185 -0
  14. ha_mcp_dev-8.0.0.dev2233/src/ha_mcp/tools/config_entry_flow_walker.py +728 -0
  15. ha_mcp_dev-8.0.0.dev2233/src/ha_mcp/tools/hacs_registration.py +393 -0
  16. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/tools_config_helpers.py +24 -13
  17. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/tools_dev.py +17 -1
  18. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/tools_hacs.py +214 -392
  19. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/tools_integrations.py +5 -2
  20. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  21. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp_dev.egg-info/SOURCES.txt +4 -0
  22. ha_mcp_dev-8.0.0.dev2228/src/ha_mcp/tools/config_entry_flow.py +0 -1718
  23. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/LICENSE +0 -0
  24. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/MANIFEST.in +0 -0
  25. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/README.md +0 -0
  26. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/setup.cfg +0 -0
  27. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/__init__.py +0 -0
  28. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/_pypi_marker +0 -0
  29. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/_version.py +0 -0
  30. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/auth/__init__.py +0 -0
  31. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/auth/consent_form.py +0 -0
  32. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/auth/provider.py +0 -0
  33. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/backup_manager.py +0 -0
  34. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/browser_landing.py +0 -0
  35. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/client/__init__.py +0 -0
  36. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/client/rest_client.py +0 -0
  37. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/client/supervisor_client.py +0 -0
  38. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/client/websocket_listener.py +0 -0
  39. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/config.py +0 -0
  40. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  41. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  42. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
  43. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
  44. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  45. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/dashboard_screenshot/theme_guard.py +0 -0
  46. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/errors.py +0 -0
  47. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/llm_exposure.py +0 -0
  48. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/policy/__init__.py +0 -0
  49. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/policy/approval_queue.py +0 -0
  50. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/policy/evaluator.py +0 -0
  51. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/policy/handlers.py +0 -0
  52. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/policy/middleware.py +0 -0
  53. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/policy/model.py +0 -0
  54. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/policy/persistence.py +0 -0
  55. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/policy/value_sources.py +0 -0
  56. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/py.typed +0 -0
  57. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/read_only.py +0 -0
  58. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  59. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  60. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  61. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  62. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  63. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  64. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  65. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  66. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  67. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  68. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  69. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  70. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
  71. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  72. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
  73. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  74. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  75. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  76. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  77. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  78. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  79. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  80. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
  81. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  82. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  83. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/server.py +0 -0
  84. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/settings_ui/__init__.py +0 -0
  85. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
  86. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
  87. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
  88. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
  89. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
  90. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
  91. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/settings_ui/_i18n.py +0 -0
  92. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/settings_ui/_persistence.py +0 -0
  93. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
  94. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/settings_ui/_theme.py +0 -0
  95. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
  96. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/settings_ui/locales/README.md +0 -0
  97. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/settings_ui/locales/en.json +0 -0
  98. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/settings_ui/settings.css +0 -0
  99. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/settings_ui/settings.html +0 -0
  100. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/settings_ui/settings.js +0 -0
  101. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/smoke_test.py +0 -0
  102. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  103. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/strict_bps.py +0 -0
  104. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/__init__.py +0 -0
  105. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/auto_backup.py +0 -0
  106. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/backup.py +0 -0
  107. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  108. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/best_practice_result.py +0 -0
  109. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/best_practice_variables.py +0 -0
  110. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/component_api.py +0 -0
  111. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/component_config_reads.py +0 -0
  112. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/component_devices.py +0 -0
  113. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/component_registries.py +0 -0
  114. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/component_registry_lookup.py +0 -0
  115. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/device_control.py +0 -0
  116. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/helpers.py +0 -0
  117. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/radio/__init__.py +0 -0
  118. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/radio/base.py +0 -0
  119. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/radio/matter.py +0 -0
  120. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/radio/thread.py +0 -0
  121. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/radio/zigbee.py +0 -0
  122. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/radio/zwave.py +0 -0
  123. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/reference_validator.py +0 -0
  124. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/registry.py +0 -0
  125. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  126. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  127. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  128. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
  129. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
  130. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  131. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  132. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  133. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  134. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
  135. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/tools_addons.py +0 -0
  136. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/tools_areas.py +0 -0
  137. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  138. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  139. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/tools_calendar.py +0 -0
  140. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/tools_camera.py +0 -0
  141. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/tools_categories.py +0 -0
  142. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/tools_code.py +0 -0
  143. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  144. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  145. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  146. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  147. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  148. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/tools_energy.py +0 -0
  149. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/tools_entities.py +0 -0
  150. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  151. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/tools_groups.py +0 -0
  152. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/tools_history.py +0 -0
  153. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/tools_labels.py +0 -0
  154. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/tools_radio.py +0 -0
  155. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/tools_registry.py +0 -0
  156. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/tools_resources.py +0 -0
  157. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/tools_search.py +0 -0
  158. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/tools_service.py +0 -0
  159. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/tools_services.py +0 -0
  160. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/tools_system.py +0 -0
  161. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/tools_themes.py +0 -0
  162. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/tools_todo.py +0 -0
  163. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/tools_traces.py +0 -0
  164. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/tools_updates.py +0 -0
  165. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/tools_utility.py +0 -0
  166. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  167. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  168. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
  169. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/tools_zones.py +0 -0
  170. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/util_helpers.py +0 -0
  171. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/tools/validation_middleware.py +0 -0
  172. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/transforms/__init__.py +0 -0
  173. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/transforms/categorized_search.py +0 -0
  174. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  175. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/transport_security.py +0 -0
  176. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/update_check.py +0 -0
  177. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/utils/__init__.py +0 -0
  178. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/utils/config_hash.py +0 -0
  179. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/utils/config_write_lock.py +0 -0
  180. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/utils/data_paths.py +0 -0
  181. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/utils/domain_handlers.py +0 -0
  182. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  183. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  184. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/utils/operation_manager.py +0 -0
  185. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/utils/python_sandbox.py +0 -0
  186. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/utils/skill_loader.py +0 -0
  187. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/utils/usage_logger.py +0 -0
  188. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/visibility/__init__.py +0 -0
  189. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/visibility/enforcement.py +0 -0
  190. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/visibility/model.py +0 -0
  191. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/visibility/persistence.py +0 -0
  192. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp/visibility/resolver.py +0 -0
  193. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  194. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  195. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  196. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  197. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/tests/__init__.py +0 -0
  198. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/tests/test_constants.py +0 -0
  199. {ha_mcp_dev-8.0.0.dev2228 → ha_mcp_dev-8.0.0.dev2233}/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.0.0.dev2228
3
+ Version: 8.0.0.dev2233
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.0.0.dev2228"
7
+ version = "8.0.0.dev2233"
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
@@ -690,11 +690,18 @@ async def _run_with_shutdown(server_coro: Coroutine[Any, Any, Any]) -> None:
690
690
  logger.error("Server task cancelled without a shutdown signal")
691
691
  raise
692
692
  finally:
693
- try:
694
- await asyncio.wait_for(
695
- _cleanup_resources(), timeout=SHUTDOWN_TIMEOUT_SECONDS
696
- )
697
- except TimeoutError:
693
+ # Cancel-and-abandon, not wait_for: wait_for awaits the cancelled
694
+ # coroutine before raising, and the cleanup stack swallows
695
+ # CancelledError at several layers (per-client in
696
+ # WebSocketManager.disconnect, in client.disconnect's own task-cancel
697
+ # guard), so a hung close handshake could block shutdown past the
698
+ # budget. A straggler is left to the runner's own task sweep.
699
+ cleanup_task = asyncio.ensure_future(_cleanup_resources())
700
+ _done, cleanup_pending = await asyncio.wait(
701
+ {cleanup_task}, timeout=SHUTDOWN_TIMEOUT_SECONDS
702
+ )
703
+ if cleanup_pending:
704
+ cleanup_task.cancel()
698
705
  logger.warning("Resource cleanup timed out")
699
706
 
700
707
  try:
@@ -1251,10 +1251,31 @@ class WebSocketManager:
1251
1251
  self._last_used[key] = time.monotonic()
1252
1252
  return existing
1253
1253
 
1254
- # Remove stale client if present
1254
+ # Remove stale client if present. Disconnect it too: a client
1255
+ # whose connection dropped can still own a parked reader task and
1256
+ # a half-open socket, and simply dropping the reference abandons
1257
+ # both to garbage collection — the GC's asyncgen finalizer then
1258
+ # acloses the reader's ``Connection.__aiter__`` mid-``__anext__``
1259
+ # and logs ``aclose(): asynchronous generator is already
1260
+ # running`` (issue #2127). Same-loop by construction: a loop
1261
+ # change already detached the pool above, so ``existing`` was
1262
+ # built on ``current_loop`` and can be awaited here.
1255
1263
  if existing:
1256
1264
  self._clients.pop(key, None)
1257
1265
  self._last_used.pop(key, None)
1266
+ try:
1267
+ await existing.disconnect()
1268
+ except asyncio.CancelledError:
1269
+ # The caller itself was cancelled mid-cleanup: propagate.
1270
+ # Swallowing here would let a cancelled operation keep
1271
+ # doing network I/O and leave a fresh connection retained
1272
+ # in the pool.
1273
+ raise
1274
+ except (OSError, RuntimeError):
1275
+ logger.warning(
1276
+ "Error disconnecting stale WebSocket client",
1277
+ exc_info=True,
1278
+ )
1258
1279
 
1259
1280
  factory = self._client_factory or HomeAssistantWebSocketClient
1260
1281
  client = (
@@ -1291,7 +1312,14 @@ class WebSocketManager:
1291
1312
  if stale:
1292
1313
  try:
1293
1314
  await stale.disconnect()
1294
- except (OSError, RuntimeError, asyncio.CancelledError):
1315
+ except asyncio.CancelledError:
1316
+ # The caller itself was cancelled mid-eviction: propagate.
1317
+ # Eviction runs after the fresh client was pooled, so a
1318
+ # swallow here would hand a connected client back to a
1319
+ # cancelled caller (mirror of the stale-replacement clause
1320
+ # in get_client).
1321
+ raise
1322
+ except (OSError, RuntimeError):
1295
1323
  logger.warning(
1296
1324
  "Error disconnecting evicted WebSocket client",
1297
1325
  exc_info=True,
@@ -735,7 +735,7 @@
735
735
  },
736
736
  "ha_manage_hacs": {
737
737
  "title": "HACS verwalten",
738
- "description": "Installiert, aktualisiert und fügt benutzerdefinierte HACS-Repositories hinzu."
738
+ "description": "Verwalte HACS (Home Assistant Community Store) — installiere/aktualisiere, entferne oder füge benutzerdefinierte Repositories hinzu."
739
739
  },
740
740
  "ha_manage_pipeline": {
741
741
  "title": "Assist-Pipeline verwalten",
@@ -735,7 +735,7 @@
735
735
  },
736
736
  "ha_manage_hacs": {
737
737
  "title": "Gestionar HACS",
738
- "description": "Gestiona HACS: instala o actualiza elementos y añade repositorios personalizados."
738
+ "description": "Gestionar HACS (Home Assistant Community Store) — instalar/actualizar, eliminar o añadir repositorios personalizados."
739
739
  },
740
740
  "ha_manage_pipeline": {
741
741
  "title": "Gestionar canalización de Assist",
@@ -735,7 +735,7 @@
735
735
  },
736
736
  "ha_manage_hacs": {
737
737
  "title": "Gérer HACS",
738
- "description": "Installe, met à jour et ajoute des dépôts HACS personnalisés."
738
+ "description": "Gérer HACS (Home Assistant Community Store) — installer/mettre à jour, supprimer ou ajouter des dépôts personnalisés."
739
739
  },
740
740
  "ha_manage_pipeline": {
741
741
  "title": "Gérer le pipeline Assist",
@@ -735,7 +735,7 @@
735
735
  },
736
736
  "ha_manage_hacs": {
737
737
  "title": "Gestisci HACS",
738
- "description": "Gestisce HACS (Home Assistant Community Store): installa o aggiorna elementi e aggiunge repository personalizzati."
738
+ "description": "Gestisci HACS (Home Assistant Community Store) — installare/aggiornare, rimuovere o aggiungere repository personalizzati."
739
739
  },
740
740
  "ha_manage_pipeline": {
741
741
  "title": "Gestisci le pipeline di Assist",
@@ -735,7 +735,7 @@
735
735
  },
736
736
  "ha_manage_hacs": {
737
737
  "title": "Управлять HACS",
738
- "description": "Устанавливать, обновлять и добавлять пользовательские репозитории HACS."
738
+ "description": "Управление HACS (Магазин сообщества Home Assistant) — устанавливать/обновлять, удалять или добавлять пользовательские репозитории."
739
739
  },
740
740
  "ha_manage_pipeline": {
741
741
  "title": "Управлять конвейером Assist",
@@ -735,7 +735,7 @@
735
735
  },
736
736
  "ha_manage_hacs": {
737
737
  "title": "管理 HACS",
738
- "description": "管理 HACS(Home Assistant 社区商店)——安装/更新或添加自定义仓库。"
738
+ "description": "管理 HACS (Home Assistant 社区商店) — 安装/更新、移除或添加自定义存储库。"
739
739
  },
740
740
  "ha_manage_pipeline": {
741
741
  "title": "管理 Assist 管线",
@@ -0,0 +1,376 @@
1
+ """
2
+ Config Entry Flow API machinery for Home Assistant MCP server.
3
+
4
+ This module provides the shared machinery for creating and updating
5
+ config-entry-based helpers (template, group, utility_meter, etc.) via the
6
+ Config Entry Flow API.
7
+
8
+ The create/update entry point is the unified ha_config_set_helper tool in
9
+ tools_config_helpers.py, which routes to create_flow_helper / update_flow_helper
10
+ for the 15 helper types listed in FLOW_HELPER_TYPES.
11
+
12
+ The same flow walkers drive every other config-entry surface, not just
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``.
17
+
18
+ The step machinery those entry points drive lives in three sibling modules,
19
+ imported in one direction only (menu <- form <- walker <- here):
20
+
21
+ - ``config_entry_flow_menu``: menu selection keys and menu-step handling
22
+ - ``config_entry_flow_form``: form-step schema consumption and reuse tracking
23
+ - ``config_entry_flow_walker``: step submission, HA error translation, flow
24
+ introspection, and the two flow walkers
25
+ """
26
+
27
+ import asyncio
28
+ import logging
29
+ from typing import Any, Literal
30
+
31
+ from ..errors import ErrorCode, create_error_response
32
+ from .config_entry_flow_form import _extract_schema_field_names
33
+ from .config_entry_flow_walker import (
34
+ _FlowType,
35
+ _handle_config_subentry_flow_steps,
36
+ _handle_flow_steps,
37
+ )
38
+ from .helpers import raise_tool_error
39
+
40
+ logger = logging.getLogger(__name__)
41
+
42
+ # 15 helpers that use Config Entry Flow API (Issue #324).
43
+ SUPPORTED_HELPERS = Literal[
44
+ "template",
45
+ "group",
46
+ "utility_meter",
47
+ "derivative",
48
+ "min_max",
49
+ "threshold",
50
+ "integration",
51
+ "statistics",
52
+ "trend",
53
+ "random",
54
+ "filter",
55
+ "tod",
56
+ "generic_thermostat",
57
+ "switch_as_x",
58
+ "generic_hygrostat",
59
+ ]
60
+
61
+ # Value-set form of SUPPORTED_HELPERS for runtime routing checks.
62
+ # Exported for import by tools_config_helpers.ha_config_set_helper.
63
+ FLOW_HELPER_TYPES: frozenset[str] = frozenset(
64
+ {
65
+ "template",
66
+ "group",
67
+ "utility_meter",
68
+ "derivative",
69
+ "min_max",
70
+ "threshold",
71
+ "integration",
72
+ "statistics",
73
+ "trend",
74
+ "random",
75
+ "filter",
76
+ "tod",
77
+ "generic_thermostat",
78
+ "switch_as_x",
79
+ "generic_hygrostat",
80
+ }
81
+ )
82
+
83
+
84
+ # ---------------------------------------------------------------------------
85
+ # Module-level flow machinery
86
+ #
87
+ # These functions are shared by the unified ha_config_set_helper tool in
88
+ # tools_config_helpers.py. They take a client instance as an explicit
89
+ # parameter so the same logic can be used from any caller.
90
+ # ---------------------------------------------------------------------------
91
+
92
+
93
+ async def set_config_subentry(
94
+ client: Any,
95
+ entry_id: str,
96
+ subentry_type: str,
97
+ config_dict: dict[str, Any],
98
+ *,
99
+ subentry_id: str | None = None,
100
+ show_advanced_options: bool | None = None,
101
+ ) -> dict[str, Any]:
102
+ """Create or reconfigure a config subentry via its flow.
103
+
104
+ Presence of ``subentry_id`` is the discriminator: omitted creates a new
105
+ subentry, provided reconfigures that existing subentry.
106
+ ``show_advanced_options`` is a no-op on HA 2026.6+ and kept only for older
107
+ HA versions pending removal before HA 2027.6.
108
+ """
109
+ flow_result = await client.start_config_subentry_flow(
110
+ entry_id,
111
+ subentry_type,
112
+ subentry_id=subentry_id,
113
+ show_advanced_options=show_advanced_options,
114
+ )
115
+ flow_id = flow_result.get("flow_id")
116
+
117
+ if not flow_id:
118
+ raise_tool_error(
119
+ create_error_response(
120
+ ErrorCode.SERVICE_CALL_FAILED,
121
+ "Failed to start config subentry flow",
122
+ suggestions=[
123
+ "Use ha_get_integration(include_subentries=True) to confirm "
124
+ "the parent entry and available subentry metadata.",
125
+ ],
126
+ context={
127
+ "entry_id": entry_id,
128
+ "subentry_type": subentry_type,
129
+ "subentry_id": subentry_id,
130
+ "details": flow_result,
131
+ },
132
+ )
133
+ )
134
+
135
+ try:
136
+ result = await _handle_config_subentry_flow_steps(
137
+ client,
138
+ flow_id,
139
+ flow_result,
140
+ config_dict,
141
+ is_reconfigure=subentry_id is not None,
142
+ )
143
+ except Exception:
144
+ try:
145
+ await asyncio.wait_for(
146
+ client.abort_config_subentry_flow(flow_id), timeout=5.0
147
+ )
148
+ except Exception as abort_err:
149
+ logger.warning(
150
+ "Failed to abort config subentry flow %s after error: %s",
151
+ flow_id,
152
+ abort_err,
153
+ )
154
+ raise
155
+
156
+ response = {
157
+ "success": True,
158
+ "entry_id": entry_id,
159
+ "subentry_type": subentry_type,
160
+ "subentry_id": subentry_id,
161
+ "operation": result["operation"],
162
+ "flow_result": result["flow_result"],
163
+ "message": f"Config subentry {result['operation']} successfully",
164
+ }
165
+ if result.get("warnings"):
166
+ response["warnings"] = result["warnings"]
167
+ return response
168
+
169
+
170
+ async def get_user_step_field_names(client: Any, helper_type: str) -> set[str] | None:
171
+ """Return field names in the user-step form schema for ``helper_type``.
172
+
173
+ Starts a config flow, peeks at the initial step's ``data_schema``,
174
+ and immediately aborts the flow. Used to decide whether to fold the
175
+ top-level ``name`` parameter into the form payload — some helpers
176
+ (e.g. ``switch_as_x``) take their entity name from the source switch
177
+ and reject ``name`` as an extra key.
178
+
179
+ Returns:
180
+ A set of field names if the initial step is a form. ``None`` if
181
+ the flow type is not introspectable from the top step (menu or
182
+ unexpected) — callers should fall back to the legacy behaviour
183
+ in that case to avoid regressing menu helpers (template, group).
184
+ Also returns ``None`` if the introspection itself fails; the
185
+ subsequent real flow will surface the error in context.
186
+ """
187
+ flow_id = None
188
+ try:
189
+ flow_result = await client.start_config_flow(helper_type)
190
+ flow_id = flow_result.get("flow_id")
191
+ if flow_result.get("type") != _FlowType.FORM:
192
+ return None
193
+ return _extract_schema_field_names(flow_result.get("data_schema"))
194
+ except Exception as e:
195
+ logger.debug(f"Schema introspection failed for {helper_type}: {e}")
196
+ return None
197
+ finally:
198
+ if flow_id:
199
+ try:
200
+ await asyncio.wait_for(client.abort_config_flow(flow_id), timeout=5.0)
201
+ except Exception as abort_err:
202
+ logger.warning(
203
+ f"Failed to abort introspection flow {flow_id}: {abort_err}"
204
+ )
205
+
206
+
207
+ async def update_config_entry_options(
208
+ client: Any,
209
+ entry_id: str,
210
+ config_dict: dict[str, Any],
211
+ *,
212
+ expected_domain: str | None = None,
213
+ noun: str = "integration",
214
+ ) -> dict[str, Any]:
215
+ """Update an existing config entry via its options flow.
216
+
217
+ When ``expected_domain`` is provided, verifies the entry's domain matches
218
+ it first (the helper path passes the helper_type; the generic
219
+ ``ha_set_integration`` path passes ``None`` to accept any domain). Starts
220
+ an options flow, walks the flow steps, and returns the result. Aborts the
221
+ flow on error. ``noun`` only affects response wording.
222
+ """
223
+ config_entry = await client.get_config_entry(entry_id)
224
+ actual_domain = config_entry.get("domain")
225
+ if expected_domain is not None and actual_domain != expected_domain:
226
+ raise_tool_error(
227
+ create_error_response(
228
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
229
+ f"entry_id '{entry_id}' belongs to domain '{actual_domain}', not '{expected_domain}'",
230
+ suggestions=[
231
+ f"Use ha_get_integration(domain='{expected_domain}') to find valid entry IDs",
232
+ ],
233
+ context={
234
+ "entry_id": entry_id,
235
+ "expected": expected_domain,
236
+ "actual": actual_domain,
237
+ },
238
+ )
239
+ )
240
+
241
+ flow_result = await client.start_options_flow(entry_id)
242
+ flow_id = flow_result.get("flow_id")
243
+
244
+ if not flow_id:
245
+ raise_tool_error(
246
+ create_error_response(
247
+ ErrorCode.SERVICE_CALL_FAILED,
248
+ "Failed to start options flow",
249
+ suggestions=[
250
+ "Check that the entry supports options (supports_options=true)"
251
+ ],
252
+ context={"entry_id": entry_id, "details": flow_result},
253
+ )
254
+ )
255
+
256
+ try:
257
+ result = await _handle_flow_steps(
258
+ client,
259
+ flow_id,
260
+ flow_result,
261
+ config_dict,
262
+ submit_fn=client.submit_options_flow_step,
263
+ helper_type=expected_domain,
264
+ )
265
+ except Exception:
266
+ try:
267
+ await asyncio.wait_for(client.abort_options_flow(flow_id), timeout=5.0)
268
+ except Exception as abort_err:
269
+ logger.warning(
270
+ f"Failed to abort options flow {flow_id} after error: {abort_err}"
271
+ )
272
+ raise
273
+
274
+ entry = result["entry"].get("result", {})
275
+ response = {
276
+ "success": True,
277
+ "entry_id": entry_id,
278
+ "title": entry.get("title"),
279
+ "domain": actual_domain,
280
+ "message": f"{actual_domain} {noun} updated successfully",
281
+ "updated": True,
282
+ }
283
+ if result.get("warnings"):
284
+ response["warnings"] = result["warnings"]
285
+ return response
286
+
287
+
288
+ async def update_flow_helper(
289
+ client: Any,
290
+ helper_type: str,
291
+ config_dict: dict[str, Any],
292
+ entry_id: str,
293
+ ) -> dict[str, Any]:
294
+ """Update an existing flow-based helper via its options flow.
295
+
296
+ Verifies the entry domain matches helper_type, starts an options flow,
297
+ walks the flow steps, and returns the result. Aborts the flow on error.
298
+ """
299
+ return await update_config_entry_options(
300
+ client,
301
+ entry_id,
302
+ config_dict,
303
+ expected_domain=helper_type,
304
+ noun="helper",
305
+ )
306
+
307
+
308
+ async def create_config_entry(
309
+ client: Any,
310
+ domain: str,
311
+ config_dict: dict[str, Any],
312
+ *,
313
+ noun: str = "integration",
314
+ ) -> dict[str, Any]:
315
+ """Create a config entry by driving ``domain``'s config flow.
316
+
317
+ Starts a config flow, walks the flow steps (menus and multi-step forms),
318
+ and returns the result. Aborts the flow on error. ``noun`` only affects
319
+ response wording.
320
+ """
321
+ flow_result = await client.start_config_flow(domain)
322
+ flow_id = flow_result.get("flow_id")
323
+
324
+ if not flow_id:
325
+ raise_tool_error(
326
+ create_error_response(
327
+ ErrorCode.SERVICE_CALL_FAILED,
328
+ "Failed to start config flow",
329
+ suggestions=[
330
+ f"Check that the {noun} domain exists and Home Assistant is reachable"
331
+ ],
332
+ context={"domain": domain, "details": flow_result},
333
+ )
334
+ )
335
+
336
+ try:
337
+ result = await _handle_flow_steps(
338
+ client,
339
+ flow_id,
340
+ flow_result,
341
+ config_dict,
342
+ helper_type=domain,
343
+ )
344
+ except Exception:
345
+ try:
346
+ await asyncio.wait_for(client.abort_config_flow(flow_id), timeout=5.0)
347
+ except Exception as abort_err:
348
+ logger.warning(
349
+ f"Failed to abort config flow {flow_id} after error: {abort_err}"
350
+ )
351
+ raise
352
+
353
+ entry = result["entry"].get("result", {})
354
+ response = {
355
+ "success": True,
356
+ "entry_id": entry.get("entry_id"),
357
+ "title": entry.get("title"),
358
+ "domain": domain,
359
+ "message": f"{domain} {noun} created successfully",
360
+ }
361
+ if result.get("warnings"):
362
+ response["warnings"] = result["warnings"]
363
+ return response
364
+
365
+
366
+ async def create_flow_helper(
367
+ client: Any,
368
+ helper_type: str,
369
+ config_dict: dict[str, Any],
370
+ ) -> dict[str, Any]:
371
+ """Create a new flow-based helper via the config flow.
372
+
373
+ Starts a config flow, walks the flow steps, and returns the result.
374
+ Aborts the flow on error.
375
+ """
376
+ return await create_config_entry(client, helper_type, config_dict, noun="helper")