|
37 | 37 | invalidate_caps, |
38 | 38 | is_unknown_command, |
39 | 39 | ) |
| 40 | +from .config_entry_flow_form import ( |
| 41 | + _MISSING_DEFAULT, |
| 42 | + _step_owned_submission_value, |
| 43 | +) |
40 | 44 | from .helpers import ( |
41 | 45 | exception_to_structured_error, |
42 | 46 | log_tool_usage, |
|
71 | 75 | # (custom_components/ha_mcp_tools/const.py OPT_CHANNEL / OPT_PIP_SPEC). |
72 | 76 | _OPT_CHANNEL = "channel" |
73 | 77 | _OPT_PIP_SPEC = "pip_spec" |
74 | | -_OPT_SERVER_URL = "server_url" |
75 | | -_OPT_EXTERNAL_URL = "external_url" |
76 | | -_OPT_WEBHOOK_ID_OVERRIDE = "webhook_id_override" |
77 | | -_OPT_SECRET_PATH_OVERRIDE = "secret_path_override" |
78 | 78 | _VALID_CHANNELS = ("stable", "dev") |
79 | 79 |
|
80 | | -# Optional text fields the component's options flow pre-fills via |
81 | | -# suggested_value (so the UI can clear them). Because an OMITTED optional field |
82 | | -# reads as "cleared" rather than "unchanged", a partial update_source submit |
83 | | -# must resend these at their current values or it would blank the user's |
84 | | -# server-URL / connect-secret overrides. |
85 | | -_PRESERVED_OPTION_KEYS = ( |
86 | | - _OPT_PIP_SPEC, |
87 | | - _OPT_SERVER_URL, |
88 | | - _OPT_EXTERNAL_URL, |
89 | | - _OPT_WEBHOOK_ID_OVERRIDE, |
90 | | - _OPT_SECRET_PATH_OVERRIDE, |
91 | | -) |
92 | | - |
93 | 80 | # Delay before a self-affecting action (embedded entry reload / options |
94 | 81 | # submit) fires, so this tool's JSON response flushes to the MCP client |
95 | 82 | # before the serving thread is torn down. Mirrors |
@@ -368,17 +355,43 @@ def _fields_from_flow_schema(flow: dict[str, Any]) -> dict[str, Any]: |
368 | 355 | } |
369 | 356 |
|
370 | 357 |
|
| 358 | +def _preserved_flow_overrides(flow: dict[str, Any]) -> dict[str, Any]: |
| 359 | + """Overrides an open server-entry flow carries stored values for. |
| 360 | +
|
| 361 | + A partial ``update_source`` submit must resend these or it would blank |
| 362 | + them: an omitted optional field reads as "cleared", not "unchanged". The |
| 363 | + resend is harvested from ``description.suggested_value`` on the flow's own |
| 364 | + schema — the clearable text fields, per the same suggestion-over-default |
| 365 | + rule as the generic walker's ``keep_current_values`` backfill (issue |
| 366 | + #2254) — rather than a hardcoded key list, so a field added to the |
| 367 | + component's options flow later cannot be silently wiped here. ``channel`` |
| 368 | + carries only a schema default (= its current value), never a suggestion, |
| 369 | + so it drops out naturally; an empty or cleared override carries no |
| 370 | + resendable value and stays omitted, which is how a cleared field stays |
| 371 | + cleared. |
| 372 | + """ |
| 373 | + preserved: dict[str, Any] = {} |
| 374 | + for item in flow.get("data_schema") or []: |
| 375 | + if not isinstance(item, dict) or not item.get("name"): |
| 376 | + continue |
| 377 | + stored = _step_owned_submission_value(item) |
| 378 | + if stored is _MISSING_DEFAULT or stored in (None, ""): |
| 379 | + continue |
| 380 | + preserved[str(item["name"])] = stored |
| 381 | + return preserved |
| 382 | + |
| 383 | + |
371 | 384 | async def _open_server_entry_flow( |
372 | 385 | client: Any, entry_id: str |
373 | 386 | ) -> tuple[str, dict[str, Any], dict[str, Any]] | None: |
374 | 387 | """Open the options flow for a KNOWN server ``entry_id``; ``None`` on failure. |
375 | 388 |
|
376 | 389 | Builds ``current_options`` from the freshly-opened flow's own schema (via |
377 | 390 | ``_fields_from_flow_schema``) rather than the component's narrower |
378 | | - ``{channel, pip_spec}`` shape, so callers like ``_update_source`` that |
379 | | - resend ``_PRESERVED_OPTION_KEYS`` (``server_url`` / ``external_url`` / |
380 | | - ``webhook_id_override`` / ``secret_path_override`` — fields the |
381 | | - ``server_entry`` capability does not carry) still see them. |
| 391 | + ``{channel, pip_spec}`` shape, so callers like ``_update_source`` — whose |
| 392 | + preserved-overrides resend harvests the flow schema itself (``server_url`` |
| 393 | + / ``external_url`` / ``webhook_id_override`` / ``secret_path_override`` — |
| 394 | + fields the ``server_entry`` capability does not carry) — still see them. |
382 | 395 | """ |
383 | 396 | try: |
384 | 397 | flow = await client.start_options_flow(entry_id) |
@@ -2047,12 +2060,7 @@ async def _update_source( |
2047 | 2060 | ) |
2048 | 2061 | entry_id, flow, current = found |
2049 | 2062 |
|
2050 | | - # Resend the user's current overrides (see _PRESERVED_OPTION_KEYS) so a |
2051 | | - # channel/pip-spec change here does not blank them — an omitted optional |
2052 | | - # field reads as "cleared", not "unchanged". |
2053 | | - user_input: dict[str, Any] = { |
2054 | | - key: current[key] for key in _PRESERVED_OPTION_KEYS if current.get(key) |
2055 | | - } |
| 2063 | + user_input: dict[str, Any] = _preserved_flow_overrides(flow) |
2056 | 2064 | if channel is not None: |
2057 | 2065 | user_input[_OPT_CHANNEL] = channel |
2058 | 2066 | if pip_spec is not None: |
|
0 commit comments