Files
localthings/tests/test_select_display.py
T
Marc Billow b921bdbb28 laundry: fix six issues from review of the cloud-cycle branch
Also drops appliance-specific wording from the new user-facing strings. The
setup step said "your washer" and told people to "turn the dial", which is
wrong for the DW5000C dishwasher that advertises the same tokens.

The two that could have caused a wrong wash cycle:

- The Download-course candidate was counted on every poll that saw a loaded
  one-time payload, not on the polls where one was actually loaded. Since a
  stale token is never evicted, it keeps being reported through however long
  the appliance then sits on some ordinary course -- so "most frequent"
  ranked by dwell time. Reproduced: one poll on Course_87 then 200 on
  Course_1B suggests 1B, and accepting the suggestion makes picking a
  download program start a Cotton wash. Now only a change of the payload
  counts, which is the moment the device is known to accept a program.
- The Download-course dropdown had custom_value=True, contradicting its own
  comment, so a typed-in code went into the Course_ token of a real write
  unchecked. Off now, plus a server-side check against the device's own
  course list where the value is stored.

Two that quietly broke things beyond this feature:

- cycle_select now always supplies a display_fn (to label cloud programs),
  which defeated select._display's "no state table and no fallback -> return
  raw" exit. Every dryer, dishwasher and air dresser on an unrecognized
  course table would have had its options and state reshaped from '0E' to
  '0 E', breaking automations and recorder history. The exit now keys off
  whether anything actually named the value, not whether a fallback existed.
- The synthetic cloud field reached diagnostics, which reads
  canonical_resources -- publishing user-typed program names in a dump
  people paste into issues, directly against the comment claiming it never
  could. Dropped at the redaction boundary, with a matching strip for the
  debug read service, which wants device state unredacted but shouldn't
  present our bookkeeping as something the appliance said.

And two smaller ones:

- The repair fired on any device advertising slots, so the DW5000C -- four
  advertised, none ever loaded -- got a permanent warning nothing the owner
  did in Home Assistant could clear. It now waits until a payload has been
  seen, which is the only evidence that household uses downloaded programs.
- The name-collision check read only the translation catalog, missing the
  device's own personal-course labels, which the select renders identically.
2026-08-10 03:16:07 +00:00

69 lines
2.9 KiB
Python

"""Tests for select-option display casing (custom_components/localthings/select.py)."""
from custom_components.localthings.select import _display
_UNTRANSLATED = None
_TRANSLATED = "ice_type"
def test_display_titlecases_a_fully_lowercase_device_native_token():
"""Samsung's sound-mode field is genuinely lowercase on the wire
('voice'/'tone'/'mute') -- these have no other casing signal to key
off, so title-case them for display."""
assert _display("voice", _UNTRANSLATED) == "Voice"
assert _display("mute", _UNTRANSLATED) == "Mute"
def test_display_inserts_a_space_at_a_camelcase_boundary():
"""'ExtraHigh' (from supportedHeatedDry) should read as two words."""
assert _display("ExtraHigh", _UNTRANSLATED) == "Extra High"
def test_display_passes_through_an_already_human_friendly_value():
"""'AI Wash' etc. (dishwasher cycle names) already read fine and
have no camelCase boundary or all-lowercase pattern -- must not be
mangled."""
assert _display("AI Wash", _UNTRANSLATED) == "AI Wash"
assert _display("Low", _UNTRANSLATED) == "Low"
assert _display("Off", _UNTRANSLATED) == "Off"
def test_display_lowercases_for_translation_key_lookup():
"""An entity with a translation_key must match the catalog's
lowercase keys exactly -- unlike the untranslated cases above, this
is not a cosmetic transform."""
assert _display("Whiskey_IceBall_3", _TRANSLATED) == "whiskey_iceball_3"
def test_unknown_translated_vendor_value_keeps_readable_fallback():
"""A firmware-added value must remain readable instead of being mangled."""
assert _display("FutureVendorMode", _TRANSLATED) == "Future Vendor Mode"
def test_known_camel_case_state_uses_snake_case_translation_key():
assert _display("ExtraHigh", "heated_dry") == "extra_high"
def test_display_passes_through_non_string_values():
assert _display(None, _UNTRANSLATED) is None
def test_display_uses_fallback_when_translation_has_no_state_table():
assert _display("69", "cycle", lambda value: f"Unknown (0x{value})") == ("Unknown (0x69)")
def test_uncatalogued_value_stays_raw_when_the_fallback_declines_it():
"""The "no state table" escape hatch keys off whether anything actually
named the value, not off whether a fallback was supplied.
laundry.cycle_select always supplies one now (it labels cloud "Download"
programs) and returns None for everything else. Keyed on the fallback's
presence instead, every dryer/dishwasher/air-dresser on an unrecognized
course table would have its options and state cosmetically reshaped --
course '0E' rendered '0 E' -- silently breaking automations and recorder
history."""
assert _display("0E", "cycle", None) == "0E"
assert _display("0E", "cycle", lambda value: None) == "0E"
# A fallback that does name the value still wins.
assert _display("0E", "cycle", lambda value: f"Course {value}") == "Course 0E"