laundry: show the names assigned so far during guided setup

A nine-program walk is hard to keep your place in. The counter alone doesn't
say what you've already done, and the programs still to do can't be listed --
they're unnamed by definition, which is the whole premise. So "named so far"
is the only orientation available, and it now appears on all three guided
screens.

It doubles as duplicate avoidance on the naming form: a repeated name is
rejected, so seeing the others while typing beats being bounced afterwards.

Listed in the appliance's own advertised order rather than the order they
were named -- a stable order either way, and not numbered: whether it matches
the dial is plausible but unverified, and implying it would be worse than
saying nothing.
This commit is contained in:
Marc Billow
2026-08-10 09:57:21 +00:00
parent a4981f40a2
commit 78a545341b
9 changed files with 73 additions and 23 deletions
+19 -2
View File
@@ -1011,12 +1011,29 @@ class LocalThingsOptionsFlow(config_entries.OptionsFlow):
return None
def _cloud_progress_placeholders(self, coord) -> dict[str, str]:
"""Counts plus the names assigned so far.
Listing them is what makes a nine-program walk followable -- it is
the only orientation available, since the programs still to do are
unnamed by definition. Shown while naming too, where it doubles as
duplicate avoidance: the form rejects a repeated name, so seeing the
others first beats being bounced.
In the appliance's own advertised order, which is at least a stable
order, without numbering them -- whether that order matches the dial
is plausible but unverified, and implying it would be worse than
saying nothing.
"""
rep = coord.cloud_course_rep()
courses = cycle_options(coord.canonical_resources(MAIN))
record = coord.cloud_courses.snapshot()
slots = cloudcourse.cloud_slots(rep, courses)
named = sum(1 for s in slots if (record["slots"].get(s) or {}).get("name"))
return {"named": str(named), "total": str(len(slots))}
names = [n for s in slots if (n := (record["slots"].get(s) or {}).get("name"))]
return {
"named": str(len(names)),
"total": str(len(slots)),
"named_list": ", ".join(names) if names else "none yet",
}
async def async_step_cloud_name(
self, user_input: dict[str, Any] | None = None
@@ -1466,14 +1466,14 @@
},
"cloud_name": {
"title": "Name this cycle",
"description": "The appliance switched to a downloaded cycle (slot {slot}) and reports {remaining} remaining.\n\nGive it the name you want to see in Home Assistant, then select the next one on the appliance. Leave it blank to keep this cycle out of the list.\n\n{named} of {total} named — you can close this dialog whenever you like, names are saved as you go.",
"description": "The appliance switched to a downloaded cycle (slot {slot}) and reports {remaining} remaining.\n\nGive it the name you want to see in Home Assistant, then select the next one on the appliance. Leave it blank to keep this cycle out of the list.\n\nNamed so far ({named} of {total}): {named_list}\n\nNames must be unique. You can close this dialog whenever you like — names are saved as you go.",
"data": {
"name": "Name"
}
},
"cloud_timeout": {
"title": "No cycle selected",
"description": "Nothing was selected on the appliance. Make sure it's set to the Download cycle, then step through the downloaded programs.\n\n{named} of {total} named — everything named so far is already saved.",
"description": "Nothing was selected on the appliance. Make sure it's set to the Download cycle, then step through the downloaded programs.\n\nNamed so far ({named} of {total}): {named_list}\n\nEverything named so far is already saved.",
"menu_options": {
"cloud_guided": "Wait again",
"cloud_finish": "Finish"
@@ -1490,7 +1490,7 @@
"not_loaded": "Toto zařízení ještě není připojeno. Zkuste to znovu, až se načte."
},
"progress": {
"cloud_wait": "Select a downloaded cycle on the appliance now.\n\n{named} of {total} named — you can close this dialog at any time, names are saved as you go."
"cloud_wait": "Select a downloaded cycle on the appliance now.\n\nNamed so far ({named} of {total}): {named_list}\n\nYou can close this dialog at any time — names are saved as you go."
}
},
"issues": {
@@ -1466,14 +1466,14 @@
},
"cloud_name": {
"title": "Name this cycle",
"description": "The appliance switched to a downloaded cycle (slot {slot}) and reports {remaining} remaining.\n\nGive it the name you want to see in Home Assistant, then select the next one on the appliance. Leave it blank to keep this cycle out of the list.\n\n{named} of {total} named — you can close this dialog whenever you like, names are saved as you go.",
"description": "The appliance switched to a downloaded cycle (slot {slot}) and reports {remaining} remaining.\n\nGive it the name you want to see in Home Assistant, then select the next one on the appliance. Leave it blank to keep this cycle out of the list.\n\nNamed so far ({named} of {total}): {named_list}\n\nNames must be unique. You can close this dialog whenever you like — names are saved as you go.",
"data": {
"name": "Name"
}
},
"cloud_timeout": {
"title": "No cycle selected",
"description": "Nothing was selected on the appliance. Make sure it's set to the Download cycle, then step through the downloaded programs.\n\n{named} of {total} named — everything named so far is already saved.",
"description": "Nothing was selected on the appliance. Make sure it's set to the Download cycle, then step through the downloaded programs.\n\nNamed so far ({named} of {total}): {named_list}\n\nEverything named so far is already saved.",
"menu_options": {
"cloud_guided": "Wait again",
"cloud_finish": "Finish"
@@ -1490,7 +1490,7 @@
"not_loaded": "Dieses Gerät ist noch nicht verbunden. Versuchen Sie es erneut, sobald es geladen wurde."
},
"progress": {
"cloud_wait": "Select a downloaded cycle on the appliance now.\n\n{named} of {total} named — you can close this dialog at any time, names are saved as you go."
"cloud_wait": "Select a downloaded cycle on the appliance now.\n\nNamed so far ({named} of {total}): {named_list}\n\nYou can close this dialog at any time — names are saved as you go."
}
},
"issues": {
@@ -1466,14 +1466,14 @@
},
"cloud_name": {
"title": "Name this cycle",
"description": "The appliance switched to a downloaded cycle (slot {slot}) and reports {remaining} remaining.\n\nGive it the name you want to see in Home Assistant, then select the next one on the appliance. Leave it blank to keep this cycle out of the list.\n\n{named} of {total} named — you can close this dialog whenever you like, names are saved as you go.",
"description": "The appliance switched to a downloaded cycle (slot {slot}) and reports {remaining} remaining.\n\nGive it the name you want to see in Home Assistant, then select the next one on the appliance. Leave it blank to keep this cycle out of the list.\n\nNamed so far ({named} of {total}): {named_list}\n\nNames must be unique. You can close this dialog whenever you like — names are saved as you go.",
"data": {
"name": "Name"
}
},
"cloud_timeout": {
"title": "No cycle selected",
"description": "Nothing was selected on the appliance. Make sure it's set to the Download cycle, then step through the downloaded programs.\n\n{named} of {total} named — everything named so far is already saved.",
"description": "Nothing was selected on the appliance. Make sure it's set to the Download cycle, then step through the downloaded programs.\n\nNamed so far ({named} of {total}): {named_list}\n\nEverything named so far is already saved.",
"menu_options": {
"cloud_guided": "Wait again",
"cloud_finish": "Finish"
@@ -1490,7 +1490,7 @@
"not_loaded": "This device isn't connected yet. Try again once it has loaded."
},
"progress": {
"cloud_wait": "Select a downloaded cycle on the appliance now.\n\n{named} of {total} named — you can close this dialog at any time, names are saved as you go."
"cloud_wait": "Select a downloaded cycle on the appliance now.\n\nNamed so far ({named} of {total}): {named_list}\n\nYou can close this dialog at any time — names are saved as you go."
}
},
"issues": {
@@ -113,14 +113,14 @@
},
"cloud_name": {
"title": "Name this cycle",
"description": "The appliance switched to a downloaded cycle (slot {slot}) and reports {remaining} remaining.\n\nGive it the name you want to see in Home Assistant, then select the next one on the appliance. Leave it blank to keep this cycle out of the list.\n\n{named} of {total} named — you can close this dialog whenever you like, names are saved as you go.",
"description": "The appliance switched to a downloaded cycle (slot {slot}) and reports {remaining} remaining.\n\nGive it the name you want to see in Home Assistant, then select the next one on the appliance. Leave it blank to keep this cycle out of the list.\n\nNamed so far ({named} of {total}): {named_list}\n\nNames must be unique. You can close this dialog whenever you like — names are saved as you go.",
"data": {
"name": "Name"
}
},
"cloud_timeout": {
"title": "No cycle selected",
"description": "Nothing was selected on the appliance. Make sure it's set to the Download cycle, then step through the downloaded programs.\n\n{named} of {total} named — everything named so far is already saved.",
"description": "Nothing was selected on the appliance. Make sure it's set to the Download cycle, then step through the downloaded programs.\n\nNamed so far ({named} of {total}): {named_list}\n\nEverything named so far is already saved.",
"menu_options": {
"cloud_guided": "Wait again",
"cloud_finish": "Finish"
@@ -137,7 +137,7 @@
"not_loaded": "Este dispositivo aún no está conectado. Inténtalo de nuevo cuando se haya cargado."
},
"progress": {
"cloud_wait": "Select a downloaded cycle on the appliance now.\n\n{named} of {total} named — you can close this dialog at any time, names are saved as you go."
"cloud_wait": "Select a downloaded cycle on the appliance now.\n\nNamed so far ({named} of {total}): {named_list}\n\nYou can close this dialog at any time — names are saved as you go."
}
},
"issues": {
@@ -1466,14 +1466,14 @@
},
"cloud_name": {
"title": "Name this cycle",
"description": "The appliance switched to a downloaded cycle (slot {slot}) and reports {remaining} remaining.\n\nGive it the name you want to see in Home Assistant, then select the next one on the appliance. Leave it blank to keep this cycle out of the list.\n\n{named} of {total} named — you can close this dialog whenever you like, names are saved as you go.",
"description": "The appliance switched to a downloaded cycle (slot {slot}) and reports {remaining} remaining.\n\nGive it the name you want to see in Home Assistant, then select the next one on the appliance. Leave it blank to keep this cycle out of the list.\n\nNamed so far ({named} of {total}): {named_list}\n\nNames must be unique. You can close this dialog whenever you like — names are saved as you go.",
"data": {
"name": "Name"
}
},
"cloud_timeout": {
"title": "No cycle selected",
"description": "Nothing was selected on the appliance. Make sure it's set to the Download cycle, then step through the downloaded programs.\n\n{named} of {total} named — everything named so far is already saved.",
"description": "Nothing was selected on the appliance. Make sure it's set to the Download cycle, then step through the downloaded programs.\n\nNamed so far ({named} of {total}): {named_list}\n\nEverything named so far is already saved.",
"menu_options": {
"cloud_guided": "Wait again",
"cloud_finish": "Finish"
@@ -1490,7 +1490,7 @@
"not_loaded": "Questo dispositivo non è ancora connesso. Riprova dopo il caricamento."
},
"progress": {
"cloud_wait": "Select a downloaded cycle on the appliance now.\n\n{named} of {total} named — you can close this dialog at any time, names are saved as you go."
"cloud_wait": "Select a downloaded cycle on the appliance now.\n\nNamed so far ({named} of {total}): {named_list}\n\nYou can close this dialog at any time — names are saved as you go."
}
},
"issues": {
@@ -1466,14 +1466,14 @@
},
"cloud_name": {
"title": "Name this cycle",
"description": "The appliance switched to a downloaded cycle (slot {slot}) and reports {remaining} remaining.\n\nGive it the name you want to see in Home Assistant, then select the next one on the appliance. Leave it blank to keep this cycle out of the list.\n\n{named} of {total} named — you can close this dialog whenever you like, names are saved as you go.",
"description": "The appliance switched to a downloaded cycle (slot {slot}) and reports {remaining} remaining.\n\nGive it the name you want to see in Home Assistant, then select the next one on the appliance. Leave it blank to keep this cycle out of the list.\n\nNamed so far ({named} of {total}): {named_list}\n\nNames must be unique. You can close this dialog whenever you like — names are saved as you go.",
"data": {
"name": "Name"
}
},
"cloud_timeout": {
"title": "No cycle selected",
"description": "Nothing was selected on the appliance. Make sure it's set to the Download cycle, then step through the downloaded programs.\n\n{named} of {total} named — everything named so far is already saved.",
"description": "Nothing was selected on the appliance. Make sure it's set to the Download cycle, then step through the downloaded programs.\n\nNamed so far ({named} of {total}): {named_list}\n\nEverything named so far is already saved.",
"menu_options": {
"cloud_guided": "Wait again",
"cloud_finish": "Finish"
@@ -1490,7 +1490,7 @@
"not_loaded": "이 기기는 아직 연결되지 않았습니다. 기기를 불러온 후 다시 시도하세요."
},
"progress": {
"cloud_wait": "Select a downloaded cycle on the appliance now.\n\n{named} of {total} named — you can close this dialog at any time, names are saved as you go."
"cloud_wait": "Select a downloaded cycle on the appliance now.\n\nNamed so far ({named} of {total}): {named_list}\n\nYou can close this dialog at any time — names are saved as you go."
}
},
"issues": {
@@ -1466,14 +1466,14 @@
},
"cloud_name": {
"title": "Name this cycle",
"description": "The appliance switched to a downloaded cycle (slot {slot}) and reports {remaining} remaining.\n\nGive it the name you want to see in Home Assistant, then select the next one on the appliance. Leave it blank to keep this cycle out of the list.\n\n{named} of {total} named — you can close this dialog whenever you like, names are saved as you go.",
"description": "The appliance switched to a downloaded cycle (slot {slot}) and reports {remaining} remaining.\n\nGive it the name you want to see in Home Assistant, then select the next one on the appliance. Leave it blank to keep this cycle out of the list.\n\nNamed so far ({named} of {total}): {named_list}\n\nNames must be unique. You can close this dialog whenever you like — names are saved as you go.",
"data": {
"name": "Name"
}
},
"cloud_timeout": {
"title": "No cycle selected",
"description": "Nothing was selected on the appliance. Make sure it's set to the Download cycle, then step through the downloaded programs.\n\n{named} of {total} named — everything named so far is already saved.",
"description": "Nothing was selected on the appliance. Make sure it's set to the Download cycle, then step through the downloaded programs.\n\nNamed so far ({named} of {total}): {named_list}\n\nEverything named so far is already saved.",
"menu_options": {
"cloud_guided": "Wait again",
"cloud_finish": "Finish"
@@ -1490,7 +1490,7 @@
"not_loaded": "Dit apparaat is nog niet verbonden. Probeer het opnieuw zodra het is geladen."
},
"progress": {
"cloud_wait": "Select a downloaded cycle on the appliance now.\n\n{named} of {total} named — you can close this dialog at any time, names are saved as you go."
"cloud_wait": "Select a downloaded cycle on the appliance now.\n\nNamed so far ({named} of {total}): {named_list}\n\nYou can close this dialog at any time — names are saved as you go."
}
},
"issues": {
+33
View File
@@ -589,3 +589,36 @@ async def test_closing_the_dialog_stops_probing_the_appliance(hass: HomeAssistan
settled = probes
await asyncio.sleep(0.05)
assert probes == settled
async def test_guided_lists_the_names_assigned_so_far(hass: HomeAssistant):
"""The only orientation available on a long walk -- the programs still to
do are unnamed by definition, so 'done so far' is all there is to show.
It doubles as duplicate avoidance, since the form rejects a repeat."""
coordinator = await _coordinator(hass)
handler = await _options_handler(hass, coordinator)
assert handler._cloud_progress_placeholders(coordinator)["named_list"] == "none yet"
coordinator.apply_cloud_courses({"55": "Sports", "6B": "Jeans"}, "87")
await _flush(hass)
placeholders = handler._cloud_progress_placeholders(coordinator)
assert placeholders["named"] == "2"
assert placeholders["total"] == "9"
# The appliance's own advertised order, not naming order: 6B precedes 55
# in CloudExtraCourse_0A5C286B2D0C55301A.
assert placeholders["named_list"] == "Jeans, Sports"
async def test_the_name_form_shows_the_other_names_while_typing(hass: HomeAssistant):
coordinator = await _coordinator(hass)
coordinator.apply_cloud_courses({"6B": "Jeans"}, "87")
await _flush(hass)
handler = await _options_handler(hass, coordinator)
await handler.async_step_cloud_guided()
_stub_probe(coordinator, ["55"])
await _run_wait(handler)
form = await handler.async_step_cloud_name()
assert form["description_placeholders"]["named_list"] == "Jeans"