ha-mcp-dev 7.14.2.dev2183__tar.gz → 7.14.2.dev2188__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 (192) hide show
  1. {ha_mcp_dev-7.14.2.dev2183/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.14.2.dev2188}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/config_entry_flow.py +411 -52
  4. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_service.py +147 -16
  5. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/util_helpers.py +11 -2
  6. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  7. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/LICENSE +0 -0
  8. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/MANIFEST.in +0 -0
  9. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/README.md +0 -0
  10. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/setup.cfg +0 -0
  11. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/__init__.py +0 -0
  12. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/__main__.py +0 -0
  13. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/_pypi_marker +0 -0
  14. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/_version.py +0 -0
  15. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/auth/__init__.py +0 -0
  16. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/auth/consent_form.py +0 -0
  17. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/auth/provider.py +0 -0
  18. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/backup_manager.py +0 -0
  19. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/browser_landing.py +0 -0
  20. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/client/__init__.py +0 -0
  21. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/client/rest_client.py +0 -0
  22. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/client/supervisor_client.py +0 -0
  23. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/client/websocket_client.py +0 -0
  24. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/client/websocket_listener.py +0 -0
  25. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/config.py +0 -0
  26. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  27. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  28. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
  29. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
  30. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  31. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/dashboard_screenshot/theme_guard.py +0 -0
  32. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/errors.py +0 -0
  33. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/llm_exposure.py +0 -0
  34. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/policy/__init__.py +0 -0
  35. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/policy/approval_queue.py +0 -0
  36. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/policy/evaluator.py +0 -0
  37. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/policy/handlers.py +0 -0
  38. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/policy/middleware.py +0 -0
  39. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/policy/model.py +0 -0
  40. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/policy/persistence.py +0 -0
  41. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/policy/value_sources.py +0 -0
  42. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/py.typed +0 -0
  43. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/read_only.py +0 -0
  44. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  45. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  46. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  47. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  48. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  49. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  50. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  51. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  52. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  53. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  54. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  55. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  56. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
  57. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  58. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
  59. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  60. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  61. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  62. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  63. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  64. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  65. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  66. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
  67. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  68. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  69. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/server.py +0 -0
  70. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/settings_ui/__init__.py +0 -0
  71. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
  72. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
  73. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
  74. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
  75. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
  76. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
  77. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/settings_ui/_i18n.py +0 -0
  78. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/settings_ui/_persistence.py +0 -0
  79. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
  80. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/settings_ui/_theme.py +0 -0
  81. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
  82. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/settings_ui/locales/README.md +0 -0
  83. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/settings_ui/locales/de.json +0 -0
  84. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/settings_ui/locales/en.json +0 -0
  85. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/settings_ui/locales/es.json +0 -0
  86. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/settings_ui/locales/fr.json +0 -0
  87. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/settings_ui/locales/it.json +0 -0
  88. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/settings_ui/locales/ru.json +0 -0
  89. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/settings_ui/locales/zh-Hans.json +0 -0
  90. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/settings_ui/settings.css +0 -0
  91. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/settings_ui/settings.html +0 -0
  92. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/settings_ui/settings.js +0 -0
  93. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/smoke_test.py +0 -0
  94. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  95. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/strict_bps.py +0 -0
  96. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/__init__.py +0 -0
  97. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/auto_backup.py +0 -0
  98. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/backup.py +0 -0
  99. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  100. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/component_api.py +0 -0
  101. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/component_config_reads.py +0 -0
  102. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/component_devices.py +0 -0
  103. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/component_registries.py +0 -0
  104. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/component_registry_lookup.py +0 -0
  105. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/device_control.py +0 -0
  106. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/helpers.py +0 -0
  107. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/radio/__init__.py +0 -0
  108. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/radio/base.py +0 -0
  109. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/radio/matter.py +0 -0
  110. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/radio/thread.py +0 -0
  111. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/radio/zigbee.py +0 -0
  112. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/radio/zwave.py +0 -0
  113. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/reference_validator.py +0 -0
  114. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/registry.py +0 -0
  115. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  116. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  117. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  118. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
  119. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
  120. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  121. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  122. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  123. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  124. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
  125. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_addons.py +0 -0
  126. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_areas.py +0 -0
  127. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  128. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  129. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_calendar.py +0 -0
  130. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_camera.py +0 -0
  131. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_categories.py +0 -0
  132. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_code.py +0 -0
  133. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  134. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  135. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  136. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  137. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  138. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  139. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_dev.py +0 -0
  140. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_energy.py +0 -0
  141. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_entities.py +0 -0
  142. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  143. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_groups.py +0 -0
  144. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_hacs.py +0 -0
  145. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_history.py +0 -0
  146. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_integrations.py +0 -0
  147. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_labels.py +0 -0
  148. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_radio.py +0 -0
  149. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_registry.py +0 -0
  150. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_resources.py +0 -0
  151. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_search.py +0 -0
  152. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_services.py +0 -0
  153. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_system.py +0 -0
  154. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_themes.py +0 -0
  155. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_todo.py +0 -0
  156. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_traces.py +0 -0
  157. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_updates.py +0 -0
  158. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_utility.py +0 -0
  159. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  160. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  161. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
  162. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_zones.py +0 -0
  163. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/validation_middleware.py +0 -0
  164. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/transforms/__init__.py +0 -0
  165. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/transforms/categorized_search.py +0 -0
  166. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  167. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/transport_security.py +0 -0
  168. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/update_check.py +0 -0
  169. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/utils/__init__.py +0 -0
  170. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/utils/config_hash.py +0 -0
  171. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/utils/config_write_lock.py +0 -0
  172. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/utils/data_paths.py +0 -0
  173. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/utils/domain_handlers.py +0 -0
  174. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  175. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  176. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/utils/operation_manager.py +0 -0
  177. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/utils/python_sandbox.py +0 -0
  178. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/utils/skill_loader.py +0 -0
  179. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/utils/usage_logger.py +0 -0
  180. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/visibility/__init__.py +0 -0
  181. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/visibility/enforcement.py +0 -0
  182. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/visibility/model.py +0 -0
  183. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/visibility/persistence.py +0 -0
  184. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/visibility/resolver.py +0 -0
  185. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  186. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  187. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  188. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  189. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  190. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/tests/__init__.py +0 -0
  191. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/tests/test_constants.py +0 -0
  192. {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/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: 7.14.2.dev2183
3
+ Version: 7.14.2.dev2188
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 = "7.14.2.dev2183"
7
+ version = "7.14.2.dev2188"
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
@@ -8,12 +8,20 @@ Config Entry Flow API.
8
8
  The create/update entry point is the unified ha_config_set_helper tool in
9
9
  tools_config_helpers.py, which routes to create_flow_helper / update_flow_helper
10
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``.
11
17
  """
12
18
 
13
19
  import asyncio
14
20
  import copy
15
21
  import logging
16
22
  from collections.abc import Iterator
23
+ from dataclasses import dataclass
24
+ from dataclasses import field as dc_field
17
25
  from enum import StrEnum
18
26
  from typing import Any, Literal, NoReturn
19
27
 
@@ -80,6 +88,8 @@ _RECONFIGURE_SUCCESS_REASONS = frozenset(
80
88
  }
81
89
  )
82
90
  _MISSING_DEFAULT = object()
91
+ # "Submit nothing for this field" — see _redeclared_field_submission.
92
+ _NO_SUBMISSION: tuple[Any, bool] = (_MISSING_DEFAULT, False)
83
93
 
84
94
 
85
95
  class _FlowType(StrEnum):
@@ -159,6 +169,89 @@ def _section_path(path_prefix: str, name: Any) -> str:
159
169
  return f"{path_prefix}.{name}" if path_prefix else name
160
170
 
161
171
 
172
+ @dataclass
173
+ class _ReuseState:
174
+ """Caller-supplied form values a flow's steps have already consumed.
175
+
176
+ A config key applies to every form step that declares it, not only the
177
+ first step to pop it out of ``remaining_config`` — see
178
+ :func:`_redeclared_field_submission` for when a recorded value is
179
+ resubmitted. The record is split by how the caller wrote the value:
180
+
181
+ - ``scoped`` maps a dotted declaration path to a value that came out of an
182
+ explicitly supplied section dict. The caller named that section, so the
183
+ value belongs to that path and nowhere else.
184
+ - ``flat`` maps a leaf name to a value that came out of the flat caller
185
+ dict. The caller named no section, so the value is position-agnostic and
186
+ fills that leaf wherever a later step declares it.
187
+
188
+ ``filled`` holds the dotted paths the current step already filled from the
189
+ caller's own keys, so nothing is injected over a value the caller wrote for
190
+ this very step; it is cleared per step. ``fired`` bounds resubmission to
191
+ one write per (step, path) for the whole flow, ``notes`` collects the
192
+ warning each of those writes emits, and ``step_id`` names the step being
193
+ consumed.
194
+ """
195
+
196
+ scoped: dict[str, Any] = dc_field(default_factory=dict)
197
+ flat: dict[str, Any] = dc_field(default_factory=dict)
198
+ filled: set[str] = dc_field(default_factory=set)
199
+ fired: set[str] = dc_field(default_factory=set)
200
+ notes: list[str] = dc_field(default_factory=list)
201
+ step_id: str | None = None
202
+
203
+ def begin_step(self, step_id: Any) -> None:
204
+ """Start recording for a new form step."""
205
+ self.step_id = step_id if isinstance(step_id, str) else None
206
+ self.filled.clear()
207
+
208
+ def record(
209
+ self, path_prefix: str, name: str, value: Any, *, scoped_only: bool
210
+ ) -> None:
211
+ """Snapshot a value the caller supplied at this declaration site.
212
+
213
+ A flat value that lands on a path already recorded from an explicit
214
+ section dict also replaces that scoped entry: the flat key is the one
215
+ actually submitted for the path (flat overrides explicit), so the
216
+ record must carry the effective value or a later redeclaration of the
217
+ path would resurrect the overridden one.
218
+ """
219
+ dotted = _section_path(path_prefix, name)
220
+ self.filled.add(dotted)
221
+ if scoped_only:
222
+ self.scoped[dotted] = copy.deepcopy(value)
223
+ else:
224
+ self.flat[name] = copy.deepcopy(value)
225
+ if dotted in self.scoped:
226
+ self.scoped[dotted] = copy.deepcopy(value)
227
+
228
+ def recorded_value(self, path_prefix: str, name: str) -> Any:
229
+ """Return the value recorded for this declaration site, else _MISSING_DEFAULT."""
230
+ dotted = _section_path(path_prefix, name)
231
+ if dotted in self.scoped:
232
+ return copy.deepcopy(self.scoped[dotted])
233
+ if name in self.flat:
234
+ return copy.deepcopy(self.flat[name])
235
+ return _MISSING_DEFAULT
236
+
237
+ def claim_write(self, dotted: str) -> bool:
238
+ """Spend the single resubmission allowed for this (step, path), if unspent.
239
+
240
+ A flow that re-presents the same step gets one reused write and then
241
+ HA's own loud "required key not provided" naming the field, rather than
242
+ an unbounded run of silent rewrites.
243
+ """
244
+ key = f"{self.step_id}:{dotted}"
245
+ if key in self.fired:
246
+ return False
247
+ self.fired.add(key)
248
+ self.notes.append(
249
+ f"Resubmitted '{dotted}' at step '{self.step_id}': supplied once "
250
+ "but declared at more than one site in this flow"
251
+ )
252
+ return True
253
+
254
+
162
255
  def _record_ignored_section_keys(
163
256
  ignored_config_keys: set[str] | None,
164
257
  remaining_config: dict[str, Any],
@@ -237,6 +330,22 @@ def _ignored_keys_warnings(
237
330
  return warnings
238
331
 
239
332
 
333
+ def _success_warnings(
334
+ ignored_config_keys: set[str],
335
+ remaining_config: dict[str, Any],
336
+ reuse_state: _ReuseState,
337
+ ) -> list[str]:
338
+ """Build a success response's ``warnings`` list (empty when there is nothing to say).
339
+
340
+ Merges the keys no step declared with the resubmissions the walk performed,
341
+ keeping ``warnings`` a flat ``list[str]`` per the response contract in
342
+ ``tests/src/unit/test_helper_response_shape.py``.
343
+ """
344
+ return _ignored_keys_warnings(ignored_config_keys, remaining_config) + list(
345
+ reuse_state.notes
346
+ )
347
+
348
+
240
349
  def _consume_section_schema(
241
350
  field: dict[str, Any],
242
351
  explicit_section: dict[str, Any] | None,
@@ -244,8 +353,17 @@ def _consume_section_schema(
244
353
  ignored_config_keys: set[str] | None,
245
354
  consumed_config_keys: set[str] | None,
246
355
  path_prefix: str,
356
+ reuse_state: _ReuseState | None = None,
357
+ *,
358
+ allow_reuse: bool = True,
359
+ explicit_source: bool = False,
247
360
  ) -> dict[str, Any]:
248
- """Consume config values for a nested flow section."""
361
+ """Consume config values for a nested flow section.
362
+
363
+ Values inside an explicitly supplied section dict are consumed first, then
364
+ flat caller keys, which is what lets a flat child override the same key
365
+ written inside the section dict.
366
+ """
249
367
  nested_schema = field.get("schema")
250
368
  if not isinstance(nested_schema, list):
251
369
  return {}
@@ -263,6 +381,9 @@ def _consume_section_schema(
263
381
  ignored_config_keys,
264
382
  consumed_config_keys,
265
383
  section_path,
384
+ reuse_state,
385
+ allow_reuse=allow_reuse,
386
+ explicit_source=True,
266
387
  )
267
388
  )
268
389
  _record_ignored_section_keys(
@@ -278,6 +399,9 @@ def _consume_section_schema(
278
399
  ignored_config_keys,
279
400
  consumed_config_keys,
280
401
  section_path,
402
+ reuse_state,
403
+ allow_reuse=allow_reuse,
404
+ explicit_source=explicit_source,
281
405
  )
282
406
  )
283
407
  return nested_data
@@ -288,7 +412,13 @@ def _mark_consumed(
288
412
  path_prefix: str,
289
413
  name: str,
290
414
  ) -> None:
291
- """Record that a caller-supplied form key, not an injected default, was used."""
415
+ """Record that a caller-supplied value was used, fresh or resubmitted.
416
+
417
+ Values the step's own schema supplies — section defaults, suggestions,
418
+ constants — are never marked: they are HA's data, and marking them would
419
+ let a config of nothing but misspelled keys look partially applied to
420
+ :func:`_finish_flow_entry`.
421
+ """
292
422
  if consumed_config_keys is not None:
293
423
  consumed_config_keys.add(_section_path(path_prefix, name))
294
424
 
@@ -315,19 +445,202 @@ def _auto_confirm_form_payload(current_step: dict[str, Any]) -> dict[str, Any] |
315
445
  return {name: True}
316
446
 
317
447
 
448
+ def _step_owned_submission_value(field: dict[str, Any]) -> Any:
449
+ """Return the value a step's own schema supplies for ``field``, else _MISSING_DEFAULT.
450
+
451
+ Deliberately distinct from :func:`_field_default_value`, which answers
452
+ "what would the UI show in this box" and is what seeds a form. This answers
453
+ "what does the step itself say to submit", which is a different question:
454
+
455
+ - ``voluptuous_serialize`` emits ``"default"`` only for an actual
456
+ voluptuous default. HA's edit-style pre-fill
457
+ (``add_suggested_values_to_schema``) copies the marker and overwrites
458
+ only its description, so a marker that already carried a default
459
+ serializes with both keys; the suggestion is the stored current value
460
+ and outranks the static default.
461
+ - A constant field serializes as ``{"type": "constant", "value": X}`` and
462
+ ``X`` is the only value it accepts.
463
+
464
+ The bare top-level ``suggested_value`` shape is not something
465
+ ``voluptuous_serialize`` produces; it is read defensively alongside the
466
+ nested one.
467
+ """
468
+ description = field.get("description")
469
+ if isinstance(description, dict) and description.get("suggested_value") is not None:
470
+ return copy.deepcopy(description["suggested_value"])
471
+ if field.get("suggested_value") is not None:
472
+ return copy.deepcopy(field["suggested_value"])
473
+ if field.get("type") == "constant" and field.get("value") is not None:
474
+ return copy.deepcopy(field["value"])
475
+ return _MISSING_DEFAULT
476
+
477
+
478
+ def _redeclared_field_submission(
479
+ field: dict[str, Any],
480
+ name: str,
481
+ path_prefix: str,
482
+ reuse_state: _ReuseState | None,
483
+ allow_reuse: bool,
484
+ ) -> tuple[Any, bool]:
485
+ """Decide what to submit for a declared field the caller named no key for here.
486
+
487
+ Returns ``(value, from_caller)``, or ``_NO_SUBMISSION`` to omit the key
488
+ entirely. A site the caller's own key already filled earlier in this same
489
+ step is left alone — a section dict and a flat key can name the same leaf,
490
+ and the caller's value for this step outranks anything injected. Otherwise,
491
+ in order:
492
+
493
+ 1. The field is not required: omit it. Nothing is ever injected into an
494
+ optional field, or into a section that is neither required nor named by
495
+ the caller (``allow_reuse``) — materializing either would invent data.
496
+ 2. The step's own schema supplies a value — a suggestion or a constant's
497
+ only legal value, per :func:`_step_owned_submission_value`: submit that.
498
+ It is schema data rather than a caller key, so it is neither marked
499
+ consumed nor warned about. A suggestion outranks a coexisting
500
+ ``"default"``: both keys can serialize together, and the suggestion is
501
+ the stored current value while the default is the static schema one —
502
+ omitting would let voluptuous substitute the static value over it.
503
+ 3. The field carries a ``"default"`` key (and no value of its own): omit it
504
+ and let voluptuous fill the default in. Key presence is the test, so
505
+ ``default: None`` is a default too.
506
+ 4. Otherwise the field is required, has no default and has no value of its
507
+ own, which makes omitting it a guaranteed "required key not provided":
508
+ resubmit the value the caller supplied for an earlier step, warn, and
509
+ spend the one write allowed per (step, path).
510
+
511
+ Mutates ``reuse_state`` on the fourth branch. Menu selection keys never
512
+ reach here. Motivating regression (issue #2057): an options flow —
513
+ LocalTuya's — that declares the same field on an early step and again on a
514
+ later one.
515
+ """
516
+ if reuse_state is None or not allow_reuse:
517
+ return _NO_SUBMISSION
518
+ dotted = _section_path(path_prefix, name)
519
+ if dotted in reuse_state.filled:
520
+ return _NO_SUBMISSION
521
+ if not field.get("required"):
522
+ return _NO_SUBMISSION
523
+ step_owned = _step_owned_submission_value(field)
524
+ if step_owned is not _MISSING_DEFAULT:
525
+ return step_owned, False
526
+ if "default" in field:
527
+ return _NO_SUBMISSION
528
+ recorded = reuse_state.recorded_value(path_prefix, name)
529
+ if recorded is _MISSING_DEFAULT:
530
+ return _NO_SUBMISSION
531
+ if not reuse_state.claim_write(dotted):
532
+ return _NO_SUBMISSION
533
+ return recorded, True
534
+
535
+
536
+ def _consume_leaf_field(
537
+ field: dict[str, Any],
538
+ name: str,
539
+ form_data: dict[str, Any],
540
+ remaining_config: dict[str, Any],
541
+ consumed_config_keys: set[str] | None,
542
+ reuse_state: _ReuseState | None,
543
+ path_prefix: str,
544
+ *,
545
+ allow_reuse: bool = True,
546
+ explicit_source: bool = False,
547
+ ) -> None:
548
+ """Fill ``name`` in ``form_data`` from the caller's config or from the step itself.
549
+
550
+ A key the caller supplied here is popped, submitted, and recorded in
551
+ ``reuse_state`` — scoped to this dotted path when it came out of an
552
+ explicitly supplied section dict, keyed by leaf name when it came out of
553
+ the flat caller dict. With no key to pop,
554
+ :func:`_redeclared_field_submission` chooses between omitting the field,
555
+ submitting the step's own value, and resubmitting the recorded one.
556
+ """
557
+ if name in remaining_config:
558
+ value = remaining_config.pop(name)
559
+ form_data[name] = value
560
+ _mark_consumed(consumed_config_keys, path_prefix, name)
561
+ if reuse_state is not None:
562
+ reuse_state.record(path_prefix, name, value, scoped_only=explicit_source)
563
+ return
564
+
565
+ value, from_caller = _redeclared_field_submission(
566
+ field, name, path_prefix, reuse_state, allow_reuse
567
+ )
568
+ if value is _MISSING_DEFAULT:
569
+ return
570
+ form_data[name] = value
571
+ if from_caller:
572
+ _mark_consumed(consumed_config_keys, path_prefix, name)
573
+
574
+
575
+ def _consume_declared_section(
576
+ field: dict[str, Any],
577
+ form_data: dict[str, Any],
578
+ remaining_config: dict[str, Any],
579
+ ignored_config_keys: set[str] | None,
580
+ consumed_config_keys: set[str] | None,
581
+ path_prefix: str,
582
+ reuse_state: _ReuseState | None,
583
+ *,
584
+ allow_reuse: bool,
585
+ explicit_source: bool,
586
+ ) -> None:
587
+ """Merge one section field's data into ``form_data``.
588
+
589
+ A caller who supplies the section as a non-dict value gets it submitted
590
+ verbatim — HA, not this walker, decides what that means. Reuse is allowed
591
+ inside the section only when HA marks it required or the caller named it,
592
+ so an untouched optional section is never brought into existence.
593
+ """
594
+ name = field.get("name")
595
+ section_name = name if isinstance(name, str) else None
596
+ explicit_section: dict[str, Any] | None = None
597
+ if section_name is not None and section_name in remaining_config:
598
+ explicit_value = remaining_config.pop(section_name)
599
+ if not isinstance(explicit_value, dict):
600
+ form_data[section_name] = explicit_value
601
+ _mark_consumed(consumed_config_keys, path_prefix, section_name)
602
+ return
603
+ explicit_section = explicit_value
604
+
605
+ nested_data = _consume_section_schema(
606
+ field,
607
+ explicit_section,
608
+ remaining_config,
609
+ ignored_config_keys,
610
+ consumed_config_keys,
611
+ path_prefix,
612
+ reuse_state,
613
+ allow_reuse=allow_reuse
614
+ and (bool(field.get("required")) or explicit_section is not None),
615
+ explicit_source=explicit_source,
616
+ )
617
+ if not nested_data:
618
+ return
619
+ if section_name is not None:
620
+ form_data[section_name] = nested_data
621
+ else:
622
+ form_data.update(nested_data)
623
+
624
+
318
625
  def _consume_form_schema(
319
626
  data_schema: list[Any],
320
627
  remaining_config: dict[str, Any],
321
628
  ignored_config_keys: set[str] | None = None,
322
629
  consumed_config_keys: set[str] | None = None,
323
630
  path_prefix: str = "",
631
+ reuse_state: _ReuseState | None = None,
632
+ *,
633
+ allow_reuse: bool = True,
634
+ explicit_source: bool = False,
324
635
  ) -> dict[str, Any]:
325
636
  """Consume matching config values and shape nested flow sections.
326
637
 
327
638
  Mutates ``remaining_config`` by removing every consumed key. Flat child
328
639
  values override the same value inside an explicitly supplied section dict.
329
640
  Unknown keys inside explicit section dicts are added to
330
- ``ignored_config_keys`` with their dotted section path.
641
+ ``ignored_config_keys`` with their dotted section path. A declared field the
642
+ caller named no key for is filled per
643
+ :func:`_redeclared_field_submission`.
331
644
  """
332
645
  form_data: dict[str, Any] = {}
333
646
 
@@ -336,40 +649,32 @@ def _consume_form_schema(
336
649
  continue
337
650
 
338
651
  name = field.get("name")
339
- nested_schema = field.get("schema")
340
- if isinstance(nested_schema, list):
341
- section_name = name if isinstance(name, str) else None
342
- explicit_section = None
343
- if section_name is not None and section_name in remaining_config:
344
- explicit_value = remaining_config.pop(section_name)
345
- if not isinstance(explicit_value, dict):
346
- form_data[section_name] = explicit_value
347
- _mark_consumed(consumed_config_keys, path_prefix, section_name)
348
- continue
349
- explicit_section = explicit_value
350
-
351
- nested_data = _consume_section_schema(
652
+ if isinstance(field.get("schema"), list):
653
+ _consume_declared_section(
352
654
  field,
353
- explicit_section,
655
+ form_data,
354
656
  remaining_config,
355
657
  ignored_config_keys,
356
658
  consumed_config_keys,
357
659
  path_prefix,
660
+ reuse_state,
661
+ allow_reuse=allow_reuse,
662
+ explicit_source=explicit_source,
358
663
  )
359
- if nested_data:
360
- if section_name is not None:
361
- form_data[section_name] = nested_data
362
- else:
363
- form_data.update(nested_data)
364
664
  continue
365
665
 
366
- if (
367
- isinstance(name, str)
368
- and name not in _MENU_SELECTION_KEYS
369
- and name in remaining_config
370
- ):
371
- form_data[name] = remaining_config.pop(name)
372
- _mark_consumed(consumed_config_keys, path_prefix, name)
666
+ if isinstance(name, str) and name not in _MENU_SELECTION_KEYS:
667
+ _consume_leaf_field(
668
+ field,
669
+ name,
670
+ form_data,
671
+ remaining_config,
672
+ consumed_config_keys,
673
+ reuse_state,
674
+ path_prefix,
675
+ allow_reuse=allow_reuse,
676
+ explicit_source=explicit_source,
677
+ )
373
678
 
374
679
  return form_data
375
680
 
@@ -393,12 +698,38 @@ def _extract_schema_field_names(data_schema: Any) -> set[str] | None:
393
698
  return names
394
699
 
395
700
 
701
+ def _consume_all_remaining_keys(
702
+ remaining_config: dict[str, Any],
703
+ consumed_config_keys: set[str] | None,
704
+ reuse_state: _ReuseState | None,
705
+ ) -> dict[str, Any]:
706
+ """Submit every non-menu key, for a step whose schema HA did not send.
707
+
708
+ Without field names there is nothing to filter on, so the whole config is
709
+ dumped into this one submit and cleared out of ``remaining_config``. Each
710
+ consumed key is still recorded by leaf name, so a later step that *does*
711
+ arrive with a schema declaring one of them can be filled from the record
712
+ rather than submitted without it.
713
+ """
714
+ form_data: dict[str, Any] = {}
715
+ for key in list(remaining_config.keys()):
716
+ if key in _MENU_SELECTION_KEYS:
717
+ continue
718
+ value = remaining_config.pop(key)
719
+ form_data[key] = value
720
+ _mark_consumed(consumed_config_keys, "", key)
721
+ if reuse_state is not None:
722
+ reuse_state.record("", key, value, scoped_only=False)
723
+ return form_data
724
+
725
+
396
726
  def _handle_form_step(
397
727
  flow_id: str,
398
728
  current_step: dict[str, Any],
399
729
  remaining_config: dict[str, Any],
400
730
  ignored_config_keys: set[str] | None = None,
401
731
  consumed_config_keys: set[str] | None = None,
732
+ reuse_state: _ReuseState | None = None,
402
733
  ) -> dict[str, Any]:
403
734
  """Validate a form step and return form data to submit.
404
735
 
@@ -408,6 +739,15 @@ def _handle_form_step(
408
739
  submitted. Fields declared inside a section are grouped under the section
409
740
  key; callers may provide them flat or inside an explicit section dict.
410
741
 
742
+ Caller-supplied values are recorded in ``reuse_state``. A field this step
743
+ declares but the caller named no key for here is filled per
744
+ :func:`_redeclared_field_submission`: omitted when the schema carries a
745
+ ``"default"`` or the field is optional, submitted from the step's own
746
+ suggestion or constant, and otherwise — required, no default, no value of
747
+ its own — resubmitted once from an earlier step's caller value with a
748
+ warning. Nothing is injected into an optional field, or into a section
749
+ neither marked required nor named by the caller.
750
+
411
751
  When ``data_schema`` is absent (HA didn't tell us field names), falls
412
752
  back to legacy behaviour: submit all non-menu keys and clear them. This
413
753
  keeps single-step flows working when HA omits the schema.
@@ -429,24 +769,23 @@ def _handle_form_step(
429
769
  )
430
770
  )
431
771
 
432
- data_schema = current_step.get("data_schema")
772
+ if reuse_state is not None:
773
+ reuse_state.begin_step(current_step.get("step_id"))
433
774
 
434
- form_data: dict[str, Any] = {}
775
+ data_schema = current_step.get("data_schema")
435
776
  if not isinstance(data_schema, list):
436
- # Legacy fallback: no schema info — dump every non-menu key and
437
- # consume them all so a follow-up step (rare without schema) won't
438
- # re-submit the same data.
439
- for key in list(remaining_config.keys()):
440
- if key in _MENU_SELECTION_KEYS:
441
- continue
442
- form_data[key] = remaining_config.pop(key)
443
- _mark_consumed(consumed_config_keys, "", key)
444
- else:
445
- form_data = _consume_form_schema(
446
- data_schema, remaining_config, ignored_config_keys, consumed_config_keys
777
+ return _consume_all_remaining_keys(
778
+ remaining_config, consumed_config_keys, reuse_state
447
779
  )
448
780
 
449
- return form_data
781
+ return _consume_form_schema(
782
+ data_schema,
783
+ remaining_config,
784
+ ignored_config_keys,
785
+ consumed_config_keys,
786
+ "",
787
+ reuse_state,
788
+ )
450
789
 
451
790
 
452
791
  def _parse_flow_api_error(
@@ -738,6 +1077,7 @@ def _finish_flow_entry(
738
1077
  any_form_key_consumed: bool,
739
1078
  ignored_config_keys: set[str],
740
1079
  remaining_config: dict[str, Any],
1080
+ reuse_state: _ReuseState,
741
1081
  ) -> dict[str, Any]:
742
1082
  """Build the CREATE_ENTRY success response, or raise on total key miss.
743
1083
 
@@ -770,7 +1110,7 @@ def _finish_flow_entry(
770
1110
  )
771
1111
  )
772
1112
  response: dict[str, Any] = {"success": True, "entry": current_step}
773
- warnings = _ignored_keys_warnings(ignored_config_keys, remaining_config)
1113
+ warnings = _success_warnings(ignored_config_keys, remaining_config, reuse_state)
774
1114
  if warnings:
775
1115
  response["warnings"] = warnings
776
1116
  return response
@@ -818,8 +1158,13 @@ async def _handle_flow_steps(
818
1158
  flow_id: Flow ID from start_config_flow or start_options_flow
819
1159
  initial_step: The first step returned by the flow start call
820
1160
  config: Full caller-provided config dict. Menu selection keys are
821
- consumed by menu steps; remaining keys are submitted on the
822
- first form step.
1161
+ consumed by menu steps; remaining keys are submitted on the first
1162
+ form step whose schema declares them or, when HA omits the schema,
1163
+ on the first form step outright. A later step that redeclares an
1164
+ already-consumed field gets that value resubmitted once, with a
1165
+ warning, where the step marks it required and supplies neither a
1166
+ default nor a value of its own (see
1167
+ :func:`_redeclared_field_submission`).
823
1168
  submit_fn: Async function to submit a step. Defaults to
824
1169
  client.submit_config_flow_step (create). Pass
825
1170
  client.submit_options_flow_step for options (update) flows.
@@ -830,8 +1175,9 @@ async def _handle_flow_steps(
830
1175
  Returns:
831
1176
  ``{"success": True, "entry": result}`` on success, plus ``warnings``
832
1177
  when SOME caller-supplied config keys were not declared by any flow
833
- step. When the flow presented at least one form step and NONE of the
834
- supplied keys were consumed, raises ``VALIDATION_INVALID_PARAMETER``
1178
+ step, or when a key had to be resubmitted to a later step that
1179
+ redeclared it. When the flow presented at least one form step and NONE
1180
+ of the supplied keys were consumed, raises ``VALIDATION_INVALID_PARAMETER``
835
1181
  instead of reporting a misleading success — the flow completed on
836
1182
  empty forms (defaults), applying nothing the caller asked for (see
837
1183
  :func:`_finish_flow_entry`). Raises ToolError on any failure.
@@ -842,6 +1188,7 @@ async def _handle_flow_steps(
842
1188
  current_step = initial_step
843
1189
  last_menu_choice: str | None = None
844
1190
  ignored_config_keys: set[str] = set()
1191
+ reuse_state = _ReuseState()
845
1192
  supplied_keys = sorted(k for k in config if k not in _MENU_SELECTION_KEYS)
846
1193
  saw_form_step = False
847
1194
  any_form_key_consumed = False
@@ -859,6 +1206,7 @@ async def _handle_flow_steps(
859
1206
  any_form_key_consumed=any_form_key_consumed,
860
1207
  ignored_config_keys=ignored_config_keys,
861
1208
  remaining_config=remaining_config,
1209
+ reuse_state=reuse_state,
862
1210
  )
863
1211
 
864
1212
  if result_type == _FlowType.ABORT:
@@ -885,7 +1233,9 @@ async def _handle_flow_steps(
885
1233
  # _handle_form_step pops only the keys declared in the current
886
1234
  # step's data_schema, leaving any other keys in remaining_config
887
1235
  # for subsequent steps (HA can present multi-step forms, e.g.
888
- # statistics: user step then pick-characteristic step).
1236
+ # statistics: user step then pick-characteristic step), and records
1237
+ # what it consumed for any later step that redeclares the same
1238
+ # field.
889
1239
  saw_form_step = True
890
1240
  consumed_form_keys: set[str] = set()
891
1241
  form_data = _auto_confirm_form_payload(current_step)
@@ -896,6 +1246,7 @@ async def _handle_flow_steps(
896
1246
  remaining_config,
897
1247
  ignored_config_keys,
898
1248
  consumed_form_keys,
1249
+ reuse_state,
899
1250
  )
900
1251
  if consumed_form_keys:
901
1252
  any_form_key_consumed = True
@@ -957,12 +1308,15 @@ async def _handle_config_subentry_flow_steps(
957
1308
  """Walk a config subentry flow and accept HA's reconfigure-success abort.
958
1309
 
959
1310
  Successful results include ``warnings`` when caller-supplied config keys
960
- were not declared by any flow step.
1311
+ were not declared by any flow step, or when a key was resubmitted to a
1312
+ later step that redeclared it. Fields redeclared by a later step are filled
1313
+ on the same terms as :func:`_handle_flow_steps`.
961
1314
  """
962
1315
  remaining_config = dict(config)
963
1316
  current_step = initial_step
964
1317
  last_menu_choice: str | None = None
965
1318
  ignored_config_keys: set[str] = set()
1319
+ reuse_state = _ReuseState()
966
1320
  max_steps = 10
967
1321
 
968
1322
  for step_num in range(max_steps):
@@ -974,7 +1328,9 @@ async def _handle_config_subentry_flow_steps(
974
1328
  "operation": "created",
975
1329
  "flow_result": current_step,
976
1330
  }
977
- warnings = _ignored_keys_warnings(ignored_config_keys, remaining_config)
1331
+ warnings = _success_warnings(
1332
+ ignored_config_keys, remaining_config, reuse_state
1333
+ )
978
1334
  if warnings:
979
1335
  response["warnings"] = warnings
980
1336
  return response
@@ -987,7 +1343,9 @@ async def _handle_config_subentry_flow_steps(
987
1343
  "operation": "reconfigured",
988
1344
  "flow_result": current_step,
989
1345
  }
990
- warnings = _ignored_keys_warnings(ignored_config_keys, remaining_config)
1346
+ warnings = _success_warnings(
1347
+ ignored_config_keys, remaining_config, reuse_state
1348
+ )
991
1349
  if warnings:
992
1350
  response["warnings"] = warnings
993
1351
  return response
@@ -1025,6 +1383,7 @@ async def _handle_config_subentry_flow_steps(
1025
1383
  current_step,
1026
1384
  remaining_config,
1027
1385
  ignored_config_keys,
1386
+ reuse_state=reuse_state,
1028
1387
  )
1029
1388
  logger.debug(
1030
1389
  "Config subentry flow step %s: form submit (step_id=%s, keys=%s)",