ha-mcp-dev 7.5.0.dev559__tar.gz → 7.5.0.dev560__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 (113) hide show
  1. {ha_mcp_dev-7.5.0.dev559/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.5.0.dev560}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/client/rest_client.py +67 -0
  4. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/tools/tools_config_entry_flow.py +202 -2
  5. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/tools/tools_config_helpers.py +162 -13
  6. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/tools/tools_integrations.py +293 -25
  7. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  8. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/LICENSE +0 -0
  9. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/MANIFEST.in +0 -0
  10. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/README.md +0 -0
  11. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/setup.cfg +0 -0
  12. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/__init__.py +0 -0
  13. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/__main__.py +0 -0
  14. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/_pypi_marker +0 -0
  15. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/_version.py +0 -0
  16. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/auth/__init__.py +0 -0
  17. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/auth/consent_form.py +0 -0
  18. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/auth/provider.py +0 -0
  19. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/client/__init__.py +0 -0
  20. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/client/supervisor_client.py +0 -0
  21. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/client/websocket_client.py +0 -0
  22. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/client/websocket_listener.py +0 -0
  23. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/config.py +0 -0
  24. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/errors.py +0 -0
  25. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/py.typed +0 -0
  26. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  27. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  28. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  29. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  30. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  31. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  32. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  33. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  34. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  35. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  36. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  37. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  38. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  39. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  40. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  41. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  42. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  43. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  44. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  45. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  46. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  47. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  48. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/server.py +0 -0
  49. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/settings_ui.py +0 -0
  50. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/smoke_test.py +0 -0
  51. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/tools/__init__.py +0 -0
  52. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/tools/backup.py +0 -0
  53. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  54. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/tools/device_control.py +0 -0
  55. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/tools/enhanced.py +0 -0
  56. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/tools/helpers.py +0 -0
  57. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/tools/reference_validator.py +0 -0
  58. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/tools/registry.py +0 -0
  59. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/tools/smart_search.py +0 -0
  60. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/tools/tools_addons.py +0 -0
  61. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/tools/tools_areas.py +0 -0
  62. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  63. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  64. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/tools/tools_calendar.py +0 -0
  65. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/tools/tools_camera.py +0 -0
  66. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/tools/tools_categories.py +0 -0
  67. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/tools/tools_code.py +0 -0
  68. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  69. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  70. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  71. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  72. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/tools/tools_energy.py +0 -0
  73. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/tools/tools_entities.py +0 -0
  74. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  75. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/tools/tools_groups.py +0 -0
  76. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/tools/tools_hacs.py +0 -0
  77. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/tools/tools_history.py +0 -0
  78. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/tools/tools_labels.py +0 -0
  79. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  80. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/tools/tools_registry.py +0 -0
  81. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/tools/tools_resources.py +0 -0
  82. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/tools/tools_search.py +0 -0
  83. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/tools/tools_service.py +0 -0
  84. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/tools/tools_services.py +0 -0
  85. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/tools/tools_system.py +0 -0
  86. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/tools/tools_todo.py +0 -0
  87. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/tools/tools_traces.py +0 -0
  88. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/tools/tools_updates.py +0 -0
  89. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/tools/tools_utility.py +0 -0
  90. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  91. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  92. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/tools/tools_zones.py +0 -0
  93. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/tools/util_helpers.py +0 -0
  94. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/transforms/__init__.py +0 -0
  95. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/transforms/categorized_search.py +0 -0
  96. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  97. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/utils/__init__.py +0 -0
  98. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/utils/config_hash.py +0 -0
  99. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/utils/data_paths.py +0 -0
  100. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/utils/domain_handlers.py +0 -0
  101. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  102. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  103. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/utils/operation_manager.py +0 -0
  104. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/utils/python_sandbox.py +0 -0
  105. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp/utils/usage_logger.py +0 -0
  106. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  107. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  108. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  109. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  110. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  111. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/tests/__init__.py +0 -0
  112. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/tests/test_constants.py +0 -0
  113. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev560}/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.5.0.dev559
3
+ Version: 7.5.0.dev560
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.5.0.dev559"
7
+ version = "7.5.0.dev560"
8
8
  description = "Home Assistant MCP Server - Complete control of Home Assistant through MCP"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13,<3.14"
@@ -1127,6 +1127,73 @@ class HomeAssistantClient:
1127
1127
  "DELETE", f"/config/config_entries/options/flow/{flow_id}"
1128
1128
  )
1129
1129
 
1130
+ async def start_config_subentry_flow(
1131
+ self,
1132
+ entry_id: str,
1133
+ subentry_type: str,
1134
+ *,
1135
+ subentry_id: str | None = None,
1136
+ show_advanced_options: bool | None = None,
1137
+ ) -> dict[str, Any]:
1138
+ """Start a config subentry create or reconfigure flow."""
1139
+ # HA requires the handler as [parent_entry_id, subentry_type].
1140
+ payload: dict[str, Any] = {"handler": [entry_id, subentry_type]}
1141
+ if subentry_id is not None:
1142
+ payload["subentry_id"] = subentry_id
1143
+ if show_advanced_options is not None:
1144
+ payload["show_advanced_options"] = show_advanced_options
1145
+
1146
+ logger.debug(
1147
+ "Starting config subentry flow for entry %s and type %s",
1148
+ entry_id,
1149
+ subentry_type,
1150
+ )
1151
+ return await self._request(
1152
+ "POST",
1153
+ "/config/config_entries/subentries/flow",
1154
+ json=payload,
1155
+ )
1156
+
1157
+ async def submit_config_subentry_flow_step(
1158
+ self, flow_id: str, user_input: dict[str, Any]
1159
+ ) -> dict[str, Any]:
1160
+ """Submit data for a config subentry flow step."""
1161
+ logger.debug("Submitting config subentry flow step for flow_id: %s", flow_id)
1162
+ return await self._request(
1163
+ "POST",
1164
+ f"/config/config_entries/subentries/flow/{flow_id}",
1165
+ json=user_input,
1166
+ )
1167
+
1168
+ async def abort_config_subentry_flow(self, flow_id: str) -> dict[str, Any]:
1169
+ """Abort an in-progress config subentry flow."""
1170
+ logger.debug("Aborting config subentry flow: %s", flow_id)
1171
+ return await self._request(
1172
+ "DELETE", f"/config/config_entries/subentries/flow/{flow_id}"
1173
+ )
1174
+
1175
+ async def list_config_subentries(self, entry_id: str) -> dict[str, Any]:
1176
+ """List subentries for a config entry."""
1177
+ logger.debug("Listing config subentries for entry: %s", entry_id)
1178
+ return await self.send_websocket_message(
1179
+ {"type": "config_entries/subentries/list", "entry_id": entry_id}
1180
+ )
1181
+
1182
+ async def delete_config_subentry(
1183
+ self,
1184
+ entry_id: str,
1185
+ subentry_id: str,
1186
+ ) -> dict[str, Any]:
1187
+ """Delete a config subentry."""
1188
+ logger.debug("Deleting config subentry %s for entry %s", subentry_id, entry_id)
1189
+ return await self.send_websocket_message(
1190
+ {
1191
+ "type": "config_entries/subentries/delete",
1192
+ "entry_id": entry_id,
1193
+ "subentry_id": subentry_id,
1194
+ }
1195
+ )
1196
+
1130
1197
  async def get_config_entry(self, entry_id: str) -> dict[str, Any]:
1131
1198
  """
1132
1199
  Get config entry details.
@@ -62,6 +62,10 @@ FLOW_HELPER_TYPES: frozenset[str] = frozenset({
62
62
 
63
63
  # Keys used to specify a menu selection — stripped before submitting form data.
64
64
  _MENU_SELECTION_KEYS = frozenset({"group_type", "next_step_id", "menu_option"})
65
+ _RECONFIGURE_SUCCESS_REASONS = frozenset({
66
+ "reauth_successful",
67
+ "reconfigure_successful",
68
+ })
65
69
 
66
70
 
67
71
  class _FlowType(StrEnum):
@@ -364,9 +368,15 @@ async def _raise_flow_api_error(
364
368
  suggestions: list[str] = []
365
369
  message: str
366
370
 
371
+ current_schema = None
372
+ if current_step is not None:
373
+ step_schema = current_step.get("data_schema")
374
+ if isinstance(step_schema, list):
375
+ current_schema = step_schema
376
+
367
377
  # Single introspection round-trip — used by both branches below.
368
378
  info = await fetch_helper_flow_info(client, helper_type, menu_choice)
369
- schema = info.get("schema")
379
+ schema = info.get("schema") or current_schema
370
380
 
371
381
  if field_errors:
372
382
  # Structured field errors — tell the caller which fields failed.
@@ -526,6 +536,197 @@ async def _handle_flow_steps(
526
536
  ))
527
537
 
528
538
 
539
+ async def _handle_config_subentry_flow_steps(
540
+ client: Any,
541
+ flow_id: str,
542
+ initial_step: dict[str, Any],
543
+ config: dict[str, Any],
544
+ *,
545
+ is_reconfigure: bool,
546
+ ) -> dict[str, Any]:
547
+ """Walk a config subentry flow and accept HA's reconfigure-success abort."""
548
+ remaining_config = dict(config)
549
+ current_step = initial_step
550
+ last_menu_choice: str | None = None
551
+ max_steps = 10
552
+
553
+ for step_num in range(max_steps):
554
+ result_type = current_step.get("type")
555
+
556
+ if result_type == _FlowType.CREATE_ENTRY:
557
+ return {
558
+ "success": True,
559
+ "operation": "created",
560
+ "flow_result": current_step,
561
+ }
562
+
563
+ if result_type == _FlowType.ABORT:
564
+ reason = current_step.get("reason")
565
+ if is_reconfigure and reason in _RECONFIGURE_SUCCESS_REASONS:
566
+ return {
567
+ "success": True,
568
+ "operation": "reconfigured",
569
+ "flow_result": current_step,
570
+ }
571
+ raise_tool_error(create_error_response(
572
+ ErrorCode.SERVICE_CALL_FAILED,
573
+ f"Config subentry flow aborted: {reason}",
574
+ context={"flow_id": flow_id, "details": current_step},
575
+ ))
576
+
577
+ if result_type == _FlowType.MENU:
578
+ menu_choice = _handle_menu_step(flow_id, current_step, remaining_config)
579
+ last_menu_choice = menu_choice
580
+ logger.debug(
581
+ "Config subentry flow step %s: menu %s (step_id=%s)",
582
+ step_num,
583
+ menu_choice,
584
+ current_step.get("step_id"),
585
+ )
586
+ menu_payload = {"next_step_id": menu_choice}
587
+ try:
588
+ current_step = await asyncio.wait_for(
589
+ client.submit_config_subentry_flow_step(flow_id, menu_payload),
590
+ timeout=20.0,
591
+ )
592
+ except HomeAssistantAPIError as api_err:
593
+ if api_err.status_code in (400, 422):
594
+ await _raise_flow_api_error(
595
+ api_err,
596
+ client=client,
597
+ flow_id=flow_id,
598
+ helper_type=None,
599
+ menu_choice=last_menu_choice,
600
+ current_step=current_step,
601
+ submitted=menu_payload,
602
+ )
603
+ raise
604
+ continue
605
+
606
+ if result_type == _FlowType.FORM:
607
+ form_data = _handle_form_step(flow_id, current_step, remaining_config)
608
+ logger.debug(
609
+ "Config subentry flow step %s: form submit "
610
+ "(step_id=%s, keys=%s)",
611
+ step_num,
612
+ current_step.get("step_id"),
613
+ sorted(form_data.keys()),
614
+ )
615
+ try:
616
+ current_step = await asyncio.wait_for(
617
+ client.submit_config_subentry_flow_step(flow_id, form_data),
618
+ timeout=20.0,
619
+ )
620
+ except HomeAssistantAPIError as api_err:
621
+ if api_err.status_code in (400, 422):
622
+ await _raise_flow_api_error(
623
+ api_err,
624
+ client=client,
625
+ flow_id=flow_id,
626
+ helper_type=None,
627
+ menu_choice=last_menu_choice,
628
+ current_step=current_step,
629
+ submitted=form_data,
630
+ )
631
+ raise
632
+ continue
633
+
634
+ if result_type in {"progress", "progress_done"}:
635
+ raise_tool_error(create_error_response(
636
+ ErrorCode.SERVICE_CALL_FAILED,
637
+ "Config subentry flow requires an asynchronous progress step",
638
+ suggestions=[
639
+ "Complete the provider setup in Home Assistant so the "
640
+ "external resource is available.",
641
+ "Retry the same ha_config_set_helper call after the "
642
+ "resource is ready.",
643
+ ],
644
+ context={"flow_id": flow_id, "details": current_step},
645
+ ))
646
+
647
+ raise_tool_error(create_error_response(
648
+ ErrorCode.INTERNAL_UNEXPECTED,
649
+ f"Unexpected config subentry flow result type: {result_type}",
650
+ context={"flow_id": flow_id, "details": current_step},
651
+ ))
652
+
653
+ raise_tool_error(create_error_response(
654
+ ErrorCode.TIMEOUT_OPERATION,
655
+ f"Config subentry flow exceeded {max_steps} steps",
656
+ context={"flow_id": flow_id, "max_steps": max_steps},
657
+ ))
658
+
659
+
660
+ async def set_config_subentry(
661
+ client: Any,
662
+ entry_id: str,
663
+ subentry_type: str,
664
+ config_dict: dict[str, Any],
665
+ *,
666
+ subentry_id: str | None = None,
667
+ show_advanced_options: bool | None = None,
668
+ ) -> dict[str, Any]:
669
+ """Create or reconfigure a config subentry via its flow.
670
+
671
+ Presence of ``subentry_id`` is the discriminator: omitted creates a new
672
+ subentry, provided reconfigures that existing subentry.
673
+ """
674
+ flow_result = await client.start_config_subentry_flow(
675
+ entry_id,
676
+ subentry_type,
677
+ subentry_id=subentry_id,
678
+ show_advanced_options=show_advanced_options,
679
+ )
680
+ flow_id = flow_result.get("flow_id")
681
+
682
+ if not flow_id:
683
+ raise_tool_error(create_error_response(
684
+ ErrorCode.SERVICE_CALL_FAILED,
685
+ "Failed to start config subentry flow",
686
+ suggestions=[
687
+ "Use ha_get_integration(include_subentries=True) to confirm "
688
+ "the parent entry and available subentry metadata.",
689
+ ],
690
+ context={
691
+ "entry_id": entry_id,
692
+ "subentry_type": subentry_type,
693
+ "subentry_id": subentry_id,
694
+ "details": flow_result,
695
+ },
696
+ ))
697
+
698
+ try:
699
+ result = await _handle_config_subentry_flow_steps(
700
+ client,
701
+ flow_id,
702
+ flow_result,
703
+ config_dict,
704
+ is_reconfigure=subentry_id is not None,
705
+ )
706
+ except Exception:
707
+ try:
708
+ await asyncio.wait_for(
709
+ client.abort_config_subentry_flow(flow_id), timeout=5.0
710
+ )
711
+ except Exception as abort_err:
712
+ logger.warning(
713
+ "Failed to abort config subentry flow %s after error: %s",
714
+ flow_id,
715
+ abort_err,
716
+ )
717
+ raise
718
+
719
+ return {
720
+ "success": True,
721
+ "entry_id": entry_id,
722
+ "subentry_type": subentry_type,
723
+ "subentry_id": subentry_id,
724
+ "operation": result["operation"],
725
+ "flow_result": result["flow_result"],
726
+ "message": f"Config subentry {result['operation']} successfully",
727
+ }
728
+
729
+
529
730
  async def get_user_step_field_names(
530
731
  client: Any, helper_type: str
531
732
  ) -> set[str] | None:
@@ -666,4 +867,3 @@ async def create_flow_helper(
666
867
  "domain": helper_type,
667
868
  "message": f"{helper_type} helper created successfully",
668
869
  }
669
-
@@ -27,6 +27,7 @@ from .tools_config_entry_flow import (
27
27
  create_flow_helper,
28
28
  fetch_helper_flow_info,
29
29
  get_user_step_field_names,
30
+ set_config_subentry,
30
31
  update_flow_helper,
31
32
  )
32
33
  from .util_helpers import (
@@ -1978,6 +1979,7 @@ def register_config_helper_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
1978
1979
  helper_type: Annotated[
1979
1980
  Literal[
1980
1981
  "counter",
1982
+ "config_subentry",
1981
1983
  "derivative",
1982
1984
  "filter",
1983
1985
  "generic_hygrostat",
@@ -2011,12 +2013,13 @@ def register_config_helper_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
2011
2013
  str | None,
2012
2014
  Field(
2013
2015
  description=(
2014
- "REQUIRED when creating (no helper_id provided). Display name "
2015
- "for the helper. Optional on update — pass helper_id instead. "
2016
- "For flow-based helper types on update (template, group, "
2017
- "utility_meter, ...), this is typically ignored — options flows "
2018
- "don't expose renaming. Rename a flow helper by deleting and "
2019
- "recreating instead."
2016
+ "Display name for simple/flow helper creation. Required when "
2017
+ "creating a helper without helper_id. Optional on helper update. "
2018
+ "Ignored for helper_type='config_subentry', which uses "
2019
+ "entry_id/subentry_type/subentry_id instead. For flow-based "
2020
+ "helper updates (template, group, utility_meter, ...), this is "
2021
+ "typically ignored because options flows don't expose renaming. "
2022
+ "Rename a flow helper by deleting and recreating instead."
2020
2023
  ),
2021
2024
  default=None,
2022
2025
  ),
@@ -2028,6 +2031,46 @@ def register_config_helper_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
2028
2031
  default=None,
2029
2032
  ),
2030
2033
  ] = None,
2034
+ entry_id: Annotated[
2035
+ str | None,
2036
+ Field(
2037
+ description=(
2038
+ "Parent config entry ID when helper_type='config_subentry'. "
2039
+ "Use ha_get_integration() to find entry IDs."
2040
+ ),
2041
+ default=None,
2042
+ ),
2043
+ ] = None,
2044
+ subentry_type: Annotated[
2045
+ str | None,
2046
+ Field(
2047
+ description=(
2048
+ "Integration-defined subentry type when "
2049
+ "helper_type='config_subentry'."
2050
+ ),
2051
+ default=None,
2052
+ ),
2053
+ ] = None,
2054
+ subentry_id: Annotated[
2055
+ str | None,
2056
+ Field(
2057
+ description=(
2058
+ "Existing config subentry ID to reconfigure when "
2059
+ "helper_type='config_subentry'. Omit to create."
2060
+ ),
2061
+ default=None,
2062
+ ),
2063
+ ] = None,
2064
+ show_advanced_options: Annotated[
2065
+ bool | str,
2066
+ Field(
2067
+ description=(
2068
+ "When helper_type='config_subentry', ask Home Assistant "
2069
+ "to expose advanced flow options."
2070
+ ),
2071
+ default=False,
2072
+ ),
2073
+ ] = False,
2031
2074
  icon: Annotated[
2032
2075
  str | None,
2033
2076
  Field(
@@ -2249,7 +2292,8 @@ def register_config_helper_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
2249
2292
  str | dict | None,
2250
2293
  Field(
2251
2294
  description=(
2252
- "Config dict for flow-based helper types "
2295
+ "Config dict for flow-based helper types and "
2296
+ "helper_type='config_subentry' "
2253
2297
  "(template, group, utility_meter, derivative, min_max, threshold, "
2254
2298
  "integration, statistics, trend, random, filter, tod, "
2255
2299
  "generic_thermostat, switch_as_x, generic_hygrostat). "
@@ -2281,9 +2325,12 @@ def register_config_helper_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
2281
2325
  ] = None,
2282
2326
  ) -> dict[str, Any]:
2283
2327
  """
2284
- Create or update Home Assistant helper entities (27 types, unified interface).
2328
+ Create or update Home Assistant helper entities and config subentries
2329
+ (28 types, unified interface).
2285
2330
 
2286
- Create requires `name`; update requires `helper_id`.
2331
+ SIMPLE/FLOW helper create requires `name`; SIMPLE/FLOW helper update
2332
+ requires `helper_id`. Config subentry create requires `entry_id` and
2333
+ `subentry_type`; config subentry update also requires `subentry_id`.
2287
2334
 
2288
2335
  SIMPLE types (structured params, WebSocket API): input_boolean, input_button,
2289
2336
  input_select, input_number, input_text, input_datetime, counter, timer, schedule,
@@ -2295,15 +2342,20 @@ def register_config_helper_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
2295
2342
  Note: `tod` is the purpose-built "is-current-time-in-range" indicator
2296
2343
  (supports cross-midnight ranges, unlike `schedule`).
2297
2344
 
2345
+ CONFIG_SUBENTRY type (Config Subentry Flow API): config_subentry.
2346
+ Pass `entry_id`, `subentry_type`, and `config`. Pass `subentry_id` to
2347
+ reconfigure an existing subentry; omit it to create a new subentry.
2348
+
2298
2349
  For flow-type updates, pass the existing entry_id as `helper_id`. Options flows
2299
2350
  reject the `name` key on update — to rename a flow helper, delete and recreate.
2300
2351
 
2301
2352
  Behavior notes:
2302
2353
  - UPDATE preserves type-specific fields not re-passed (rename never wipes
2303
2354
  initial/icon/etc. for any simple helper).
2304
- - Pass `action="create"` or `action="update"` to disambiguate intent
2305
- without it the tool falls back to the implicit `helper_id`-presence
2306
- discriminator.
2355
+ - Pass `action="create"` or `action="update"` to disambiguate intent.
2356
+ For SIMPLE/FLOW helpers, omitted action falls back to the implicit
2357
+ `helper_id`-presence discriminator. For config subentries, omitted
2358
+ action falls back to the `subentry_id`-presence discriminator.
2307
2359
  - For flow-based helpers, config keys not declared by any step's
2308
2360
  data_schema are silently ignored by HA; submit once and the
2309
2361
  validation error returns the `data_schema` for that helper so
@@ -2327,13 +2379,110 @@ def register_config_helper_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
2327
2379
  - tod (time-of-day indicator, cross-midnight OK):
2328
2380
  ha_config_set_helper(helper_type="tod", name="Quiet Hours",
2329
2381
  config={"after_time": "22:00:00", "before_time": "07:00:00"})
2382
+ - config subentry (create under an existing integration):
2383
+ ha_config_set_helper(helper_type="config_subentry",
2384
+ entry_id="01HXYZ...", subentry_type="conversation",
2385
+ config={"name": "Local agent", "model": "gemma3:27b"})
2330
2386
 
2331
2387
  For helper-design guidance (when to pick which helper type, YAML
2332
2388
  examples, per-type field tables), use ha_get_skill_guide — the
2333
- skill's `helper-selection.md` reference covers all 27 helper types
2389
+ skill's `helper-selection.md` reference covers helper types
2334
2390
  with worked examples and a decision matrix.
2335
2391
  """
2336
2392
  try:
2393
+ if helper_type == "config_subentry":
2394
+ if action is not None:
2395
+ if action == "create" and subentry_id is not None:
2396
+ raise_tool_error(
2397
+ create_error_response(
2398
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
2399
+ "action='create' was passed with subentry_id. "
2400
+ "Omit subentry_id to create a new subentry.",
2401
+ context={
2402
+ "helper_type": helper_type,
2403
+ "action": action,
2404
+ "subentry_id": subentry_id,
2405
+ },
2406
+ )
2407
+ )
2408
+ if action == "update" and subentry_id is None:
2409
+ raise_tool_error(
2410
+ create_error_response(
2411
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
2412
+ "action='update' requires subentry_id.",
2413
+ context={
2414
+ "helper_type": helper_type,
2415
+ "action": action,
2416
+ },
2417
+ )
2418
+ )
2419
+ else:
2420
+ action = "update" if subentry_id else "create"
2421
+
2422
+ entry_id = validate_identifier_not_empty(
2423
+ entry_id,
2424
+ "entry_id",
2425
+ suggestions=[
2426
+ "Use ha_get_integration() to find the parent config entry ID",
2427
+ ],
2428
+ context={"helper_type": helper_type, "action": action},
2429
+ )
2430
+ subentry_type = validate_identifier_not_empty(
2431
+ subentry_type,
2432
+ "subentry_type",
2433
+ suggestions=[
2434
+ "Use ha_get_integration(entry_id=..., "
2435
+ "include_subentries=True, include_subentry_schema=True) "
2436
+ "to inspect available subentry metadata.",
2437
+ ],
2438
+ context={"helper_type": helper_type, "action": action},
2439
+ )
2440
+ if subentry_id is not None:
2441
+ subentry_id = validate_identifier_not_empty(
2442
+ subentry_id,
2443
+ "subentry_id",
2444
+ context={"helper_type": helper_type, "action": action},
2445
+ )
2446
+ if not isinstance(config, dict):
2447
+ try:
2448
+ config_dict = (
2449
+ parse_json_param(config, "config") if config else {}
2450
+ )
2451
+ except ValueError as err:
2452
+ raise_tool_error(
2453
+ create_error_response(
2454
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
2455
+ str(err),
2456
+ context={
2457
+ "helper_type": helper_type,
2458
+ "action": action,
2459
+ "parameter": "config",
2460
+ },
2461
+ )
2462
+ )
2463
+ else:
2464
+ config_dict = config
2465
+ if not isinstance(config_dict, dict):
2466
+ raise_tool_error(
2467
+ create_error_response(
2468
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
2469
+ "config must be an object for config_subentry",
2470
+ context={"helper_type": helper_type, "action": action},
2471
+ )
2472
+ )
2473
+ return await set_config_subentry(
2474
+ client,
2475
+ entry_id,
2476
+ subentry_type,
2477
+ config_dict,
2478
+ subentry_id=subentry_id,
2479
+ show_advanced_options=coerce_bool_param(
2480
+ show_advanced_options,
2481
+ "show_advanced_options",
2482
+ default=False,
2483
+ ),
2484
+ )
2485
+
2337
2486
  # Determine if this is a create or update — set early so the
2338
2487
  # outer exception handler's context dict can reference it even
2339
2488
  # if an exception bubbles out of the flow-helper branch below.