ha-mcp-dev 7.5.0.dev559__tar.gz → 7.5.0.dev561__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.dev561}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/client/rest_client.py +88 -9
  4. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/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.dev561}/src/ha_mcp/tools/tools_config_helpers.py +162 -13
  6. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_integrations.py +293 -25
  7. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  8. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/LICENSE +0 -0
  9. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/MANIFEST.in +0 -0
  10. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/README.md +0 -0
  11. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/setup.cfg +0 -0
  12. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/__init__.py +0 -0
  13. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/__main__.py +0 -0
  14. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/_pypi_marker +0 -0
  15. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/_version.py +0 -0
  16. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/auth/__init__.py +0 -0
  17. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/auth/consent_form.py +0 -0
  18. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/auth/provider.py +0 -0
  19. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/client/__init__.py +0 -0
  20. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/client/supervisor_client.py +0 -0
  21. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/client/websocket_client.py +0 -0
  22. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/client/websocket_listener.py +0 -0
  23. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/config.py +0 -0
  24. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/errors.py +0 -0
  25. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/py.typed +0 -0
  26. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/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.dev561}/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.dev561}/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.dev561}/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.dev561}/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.dev561}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  32. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  33. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  34. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  35. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  36. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/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.dev561}/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.dev561}/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.dev561}/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.dev561}/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.dev561}/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.dev561}/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.dev561}/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.dev561}/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.dev561}/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.dev561}/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.dev561}/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.dev561}/src/ha_mcp/server.py +0 -0
  49. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/settings_ui.py +0 -0
  50. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/smoke_test.py +0 -0
  51. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/__init__.py +0 -0
  52. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/backup.py +0 -0
  53. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  54. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/device_control.py +0 -0
  55. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/enhanced.py +0 -0
  56. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/helpers.py +0 -0
  57. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/reference_validator.py +0 -0
  58. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/registry.py +0 -0
  59. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/smart_search.py +0 -0
  60. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_addons.py +0 -0
  61. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_areas.py +0 -0
  62. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  63. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  64. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_calendar.py +0 -0
  65. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_camera.py +0 -0
  66. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_categories.py +0 -0
  67. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_code.py +0 -0
  68. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  69. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  70. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  71. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  72. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_energy.py +0 -0
  73. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_entities.py +0 -0
  74. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  75. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_groups.py +0 -0
  76. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_hacs.py +0 -0
  77. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_history.py +0 -0
  78. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_labels.py +0 -0
  79. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  80. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_registry.py +0 -0
  81. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_resources.py +0 -0
  82. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_search.py +0 -0
  83. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_service.py +0 -0
  84. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_services.py +0 -0
  85. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_system.py +0 -0
  86. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_todo.py +0 -0
  87. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_traces.py +0 -0
  88. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_updates.py +0 -0
  89. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_utility.py +0 -0
  90. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  91. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  92. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_zones.py +0 -0
  93. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/util_helpers.py +0 -0
  94. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/transforms/__init__.py +0 -0
  95. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/transforms/categorized_search.py +0 -0
  96. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  97. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/utils/__init__.py +0 -0
  98. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/utils/config_hash.py +0 -0
  99. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/utils/data_paths.py +0 -0
  100. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/utils/domain_handlers.py +0 -0
  101. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  102. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  103. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/utils/operation_manager.py +0 -0
  104. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/utils/python_sandbox.py +0 -0
  105. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/utils/usage_logger.py +0 -0
  106. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  107. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/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.dev561}/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.dev561}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  110. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/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.dev561}/tests/__init__.py +0 -0
  112. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/tests/test_constants.py +0 -0
  113. {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/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.dev561
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.dev561"
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"
@@ -782,6 +782,9 @@ class HomeAssistantClient:
782
782
  else:
783
783
  return False, "Invalid response from Home Assistant"
784
784
  except Exception as e:
785
+ # Intentional broad-catch: is_connected() contract maps any failure
786
+ # to (False, error_msg); styleguide § "broad except at top-level
787
+ # setup/teardown handlers" applies (connection probe is the analog).
785
788
  logger.error(f"Failed to connect to Home Assistant: {e}")
786
789
  return False, str(e)
787
790
 
@@ -823,7 +826,7 @@ class HomeAssistantClient:
823
826
  f"Converted entity_id {identifier} to unique_id {unique_id}"
824
827
  )
825
828
  return str(unique_id)
826
- except Exception as e:
829
+ except HomeAssistantError as e:
827
830
  raise HomeAssistantAPIError(
828
831
  f"Failed to resolve automation {identifier}: {str(e)}",
829
832
  status_code=404,
@@ -853,8 +856,8 @@ class HomeAssistantClient:
853
856
  "GET", f"/config/automation/config/{unique_id}"
854
857
  )
855
858
  return response
856
- except Exception as e:
857
- if "404" in str(e):
859
+ except HomeAssistantAPIError as e:
860
+ if e.status_code == 404:
858
861
  raise HomeAssistantAPIError(
859
862
  f"Automation not found: {identifier} (unique_id: {unique_id})",
860
863
  status_code=404,
@@ -915,18 +918,22 @@ class HomeAssistantClient:
915
918
  if entity_not_verified:
916
919
  result["entity_not_verified"] = True
917
920
  return result
918
- except Exception as e:
919
- if "400" in str(e):
921
+ except HomeAssistantAPIError as e:
922
+ if e.status_code == 400:
920
923
  raise HomeAssistantAPIError(
921
924
  f"Invalid automation configuration: {str(e)}", status_code=400
922
925
  ) from e
923
926
  raise
924
927
 
928
+ # 3-attempt × 6s upper-bound budget; first poll 0.1s catches the
929
+ # typical sub-1s entity-publish window.
930
+ _POLL_CADENCE: tuple[float, ...] = (0.1, 1.0, 4.9)
931
+
925
932
  async def _poll_for_automation_entity(self, unique_id: str) -> str | None:
926
933
  """Poll HA state to find the entity_id assigned to a newly created automation."""
927
934
  try:
928
- for attempt in range(3):
929
- await asyncio.sleep(1 * (attempt + 1))
935
+ for sleep_time in self._POLL_CADENCE:
936
+ await asyncio.sleep(sleep_time)
930
937
  states = await self.get_states()
931
938
  for state in states:
932
939
  if not state.get("entity_id", "").startswith("automation."):
@@ -937,9 +944,14 @@ class HomeAssistantClient:
937
944
  f"Found actual entity_id for unique_id {unique_id}: {entity_id}"
938
945
  )
939
946
  return entity_id
940
- except Exception as e:
947
+ except HomeAssistantError as e:
948
+ # Narrow catch: programming bugs (TypeError/KeyError/etc.) propagate.
949
+ # Mirrors test-side _POLLING_TRANSIENT_ERRORS in
950
+ # tests/src/e2e/utilities/wait_helpers.py and styleguide §
951
+ # "Exception Handling in Test Polling Loops".
941
952
  logger.warning(
942
- f"Failed to query actual entity_id for unique_id {unique_id}: {e}"
953
+ f"Failed to query actual entity_id for unique_id {unique_id}: {e}",
954
+ exc_info=True,
943
955
  )
944
956
  return None
945
957
 
@@ -1127,6 +1139,73 @@ class HomeAssistantClient:
1127
1139
  "DELETE", f"/config/config_entries/options/flow/{flow_id}"
1128
1140
  )
1129
1141
 
1142
+ async def start_config_subentry_flow(
1143
+ self,
1144
+ entry_id: str,
1145
+ subentry_type: str,
1146
+ *,
1147
+ subentry_id: str | None = None,
1148
+ show_advanced_options: bool | None = None,
1149
+ ) -> dict[str, Any]:
1150
+ """Start a config subentry create or reconfigure flow."""
1151
+ # HA requires the handler as [parent_entry_id, subentry_type].
1152
+ payload: dict[str, Any] = {"handler": [entry_id, subentry_type]}
1153
+ if subentry_id is not None:
1154
+ payload["subentry_id"] = subentry_id
1155
+ if show_advanced_options is not None:
1156
+ payload["show_advanced_options"] = show_advanced_options
1157
+
1158
+ logger.debug(
1159
+ "Starting config subentry flow for entry %s and type %s",
1160
+ entry_id,
1161
+ subentry_type,
1162
+ )
1163
+ return await self._request(
1164
+ "POST",
1165
+ "/config/config_entries/subentries/flow",
1166
+ json=payload,
1167
+ )
1168
+
1169
+ async def submit_config_subentry_flow_step(
1170
+ self, flow_id: str, user_input: dict[str, Any]
1171
+ ) -> dict[str, Any]:
1172
+ """Submit data for a config subentry flow step."""
1173
+ logger.debug("Submitting config subentry flow step for flow_id: %s", flow_id)
1174
+ return await self._request(
1175
+ "POST",
1176
+ f"/config/config_entries/subentries/flow/{flow_id}",
1177
+ json=user_input,
1178
+ )
1179
+
1180
+ async def abort_config_subentry_flow(self, flow_id: str) -> dict[str, Any]:
1181
+ """Abort an in-progress config subentry flow."""
1182
+ logger.debug("Aborting config subentry flow: %s", flow_id)
1183
+ return await self._request(
1184
+ "DELETE", f"/config/config_entries/subentries/flow/{flow_id}"
1185
+ )
1186
+
1187
+ async def list_config_subentries(self, entry_id: str) -> dict[str, Any]:
1188
+ """List subentries for a config entry."""
1189
+ logger.debug("Listing config subentries for entry: %s", entry_id)
1190
+ return await self.send_websocket_message(
1191
+ {"type": "config_entries/subentries/list", "entry_id": entry_id}
1192
+ )
1193
+
1194
+ async def delete_config_subentry(
1195
+ self,
1196
+ entry_id: str,
1197
+ subentry_id: str,
1198
+ ) -> dict[str, Any]:
1199
+ """Delete a config subentry."""
1200
+ logger.debug("Deleting config subentry %s for entry %s", subentry_id, entry_id)
1201
+ return await self.send_websocket_message(
1202
+ {
1203
+ "type": "config_entries/subentries/delete",
1204
+ "entry_id": entry_id,
1205
+ "subentry_id": subentry_id,
1206
+ }
1207
+ )
1208
+
1130
1209
  async def get_config_entry(self, entry_id: str) -> dict[str, Any]:
1131
1210
  """
1132
1211
  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.