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.
- {ha_mcp_dev-7.5.0.dev559/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.5.0.dev561}/PKG-INFO +1 -1
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/pyproject.toml +1 -1
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/client/rest_client.py +88 -9
- {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
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_config_helpers.py +162 -13
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_integrations.py +293 -25
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/LICENSE +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/README.md +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/setup.cfg +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/py.typed +0 -0
- {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
- {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
- {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
- {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
- {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
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/settings_ui.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/smart_search.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {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
- {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
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {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
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/tests/test_env_manager.py +0 -0
|
@@ -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.
|
|
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
|
|
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
|
|
857
|
-
if
|
|
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
|
|
919
|
-
if
|
|
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
|
|
929
|
-
await asyncio.sleep(
|
|
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
|
|
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.
|
{ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_config_entry_flow.py
RENAMED
|
@@ -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
|
-
|
{ha_mcp_dev-7.5.0.dev559 → ha_mcp_dev-7.5.0.dev561}/src/ha_mcp/tools/tools_config_helpers.py
RENAMED
|
@@ -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
|
-
"
|
|
2015
|
-
"
|
|
2016
|
-
"
|
|
2017
|
-
"
|
|
2018
|
-
"
|
|
2019
|
-
"
|
|
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
|
|
2328
|
+
Create or update Home Assistant helper entities and config subentries
|
|
2329
|
+
(28 types, unified interface).
|
|
2285
2330
|
|
|
2286
|
-
|
|
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
|
-
|
|
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
|
|
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.
|