fix(options): edit apps one row per step in a multiline field, and report discovery

The live Apps page showed the whole map as ONE long line with nowhere to add a
row. Cause: the field was a bare `vol.Optional(...): str`, and HA renders a
selector-less string as a SINGLE-LINE input (pre-fix wire field:
{"type": "string", "name": "app_name_map"} — no selector key). It is a
multiline TextSelector now: the real-HA test pins
selector.text.multiline on the wire and an N-line save/reopen round trip.

HA has no table selector, so the Apps sub-screen is a menu of row actions
(whole list as text / add / edit / remove) instead of one form:

- the EDIT row lists every app the user can act on: the rows they saved AND the
  apps discovery found, marked `[detected]`, each labelled "name (package id)"
  or "package id (no name yet)" — the user's Shield exposes no device labels at
  all, so the unnamed case is the common one and it must not read as its own id
  twice, and picking it is how a name gets set (saving stores the row)
- add: package + name + optional icon, refused by the SAME `_valid_app_row`
  rule the text field uses, with an error on the page
- edit: PREFILLED from the stored row (suggested values — a voluptuous
  `default=` never reaches the browser, so the form would have opened blank);
  the device's mdi: icon is never prefilled into the icon column
- remove: pick a SAVED row, then an explicit confirmation menu; cancelling
  keeps it (a detected app has no line to delete)
- edit/remove rows are offered only while something is stored
- every mutation writes ONE row through `_write_app_row`/`_drop_app_row`, so
  untouched rows and their icons stay byte-for-byte as they were

The Discover row was a silent no-op ("seems to load then do nothing"): it now
returns a one-line verdict the menu shows — N apps found / the Shield reported
none / the sweep could not reach the Shield / ADB is not set up — while still
never raising and never caching a failed sweep as an answer.

Also fixes `async_step_menu_again`, which the real FlowManager dispatches with
user_input and which raised TypeError for every menu offering that row (only
reachable through a stub before, hence unnoticed).

Docs: README and BOTH strings files (byte-identical) describe the editor, the
one-per-line format, the detected-app dropdown and the four Discover verdicts.
This commit is contained in:
James Mackay committed 2026-10-12 10:02:09 +01:00
1 parent eb62011ded
commit 46738063aa
8 files changed
+1369 -81

No files matched your search

+24
View File
@@ -80,6 +80,30 @@ def install_selector_faces(selector_module) -> None:
selector_module.SelectSelector = lambda *args, **kwargs: None
selector_module.SelectSelectorConfig = _Config
selector_module.SelectSelectorMode = types.SimpleNamespace(DROPDOWN="dropdown")
# t64: the apps table is a MULTILINE text field, and the row editor picks an
# app out of a select whose options are label/value pairs. Both are real HA
# shapes; ``SelectOptionDict`` is a TypedDict there (a plain dict at runtime).
selector_module.SelectOptionDict = dict
selector_module.TextSelector = TextSelectorStub
selector_module.TextSelectorConfig = _Config
class TextSelectorStub:
"""Stand-in for HA's TextSelector: accepts a string, rejects anything else.
Real HA validates with ``probatio.Schema(str)`` (so ``None`` and numbers are
Invalid) and carries ``multiline`` as a CONFIG flag — the flag is what makes
the browser render a textarea, which is the t64 fix, so it is inspectable
here the same way the entity selectors' domains are.
"""
def __init__(self, config=None):
self.config = config
def __call__(self, data):
if not isinstance(data, str):
raise ValueError("Value is not a string")
return data
class MediaPlayerState(enum.StrEnum):