Two fixes for issue #9 (WW90T634DHE washer):
- washer dosing selects: the four detergent/softener dosing selects read their
current value from `<Prefix>LevelCtrl_<code>` (un-padded, e.g. "3") but their
options from `Supported<Prefix>LevelCtrl_<hexpairs>` (zero-padded, e.g. "03").
HA's SelectEntity renders a select "unknown" whenever current_option is not in
options, so all four sat "unknown" (idle and running) even though every other
select worked -- which is why it was only those four. Normalize the current
value to the supported code with the same integer value so it matches an
option (and its translation); convert back to the device's native un-padded
format on write.
- diagnostics: pkg_version("smartthings-local") reads package metadata off disk
(listdir + open + read_text), tripping HA's event-loop blocking-call detector.
Offload it to the executor. Audited the rest of the package: config_flow's
socket/crypto and every coordinator DTLS call are already offloaded via
async_add_executor_job -- this was the only blocking call left on the loop.
Also documents air-conditioner support in the README (device table, capability
module list, platform list), missed when that support landed.
Updates the washer dosing tests to the corrected value/write format and adds a
current-option-is-a-valid-option regression; adds a diagnostics test asserting
the version lookup runs off the event loop.
First full dryer dump (issue #14, DV90BB5245AES1) surfaced 5 unbound
hrefs and an "incomplete capability coverage" repair. Handle them and,
while here, make the washer/dryer/dishwasher families consistent instead
of each carrying a bespoke variant of the same controls.
Dryer coverage:
- /power/0, /kidslock/0, /remotectrl/0: bind via the OCF-native + vendor
fallback pairs (prefer the standard OCF resource, fall back to -vs).
- /buzzersound/vs/0: new Buzzer sound select.
- /course/vs/0: cycle select shared with washer/dishwasher; ignore the
/st/dryercourse/vs/0 re-encoding (mirror of /st/washercourse/vs/0).
Consistency / de-duplication:
- Move generic OCF controls (power/kids-lock/remote-control fallback
pairs, energy meter) into common.py; every registry uses them.
- Move shared laundry controls (buzzer, job-beginning-status, and the
/course/vs/0 cycle-select machinery) into laundry.py; washer and
dishwasher stop hand-rolling their own copies.
- Energy meter is now sentinel-aware everywhere: the dead '-500'
instantaneousPower reading no longer shows a misleading 0 W (fixes it
on dryers and dishwashers, matching the earlier washer fix).
- Job-beginning-status reads x.com.samsung.da.currentStatus, the field
every dump actually carries; the dryer sensor was previously blank.
Adds a scrubbed dryer fixture, golden, and capability tests, plus an
.claude/skills/adding-device-support skill capturing the dump-reading,
OCF-vs-vendor, entity-taxonomy, and coverage workflow. Bumps to 0.6.0.
Assumes the LevelCtrl code scheme is None/Low/Medium/High (00-03) on both
dispensers -- code 00 has no on-screen equivalent in the app's 3-choice
Faible/Moyen/Élevé picker, assumed to be what "Activation" off collapses
to -- and Level2Ctrl is Soft/Medium/Hard for detergent water hardness,
1x/2x/3x for softener concentration. detergent_quantity and
softener_quantity share one translation_key (same vocabulary), same
pattern as fridge.py's shared 'brightness_level' key.
Not cross-device verified: only one dump + screenshot set (issue #9) to go
on, and the softener concentration reading doesn't cleanly match its
screenshot (assumed to be a setting changed between dump and screenshots,
not a different code scheme -- see the comment in washer.py).
progress_percentage lacked the active-state gate already applied to
progress/cycle_active/finish_time, so it kept showing a stale device value
(e.g. 1%) while idle -- now zeroed the same way. Shared by dryer/dishwasher/
oven via operational.py's OPERATIONAL_STATE.
Also exposes detergent/softener auto-dispense quantity, water hardness/
concentration, and low-reservoir alarms from /course/vs/0's options array,
using the same decode/RMW helpers already used for course selection and
drum-clean tracking. Gated by exists_fn so washer models without these
fields (e.g. the existing test fixture) are unaffected.
Water consumption is unaffected -- common.WATER_METER is already wired
into the washer registry; this reporter's device just doesn't expose
/water/consumption/vs/0.
Washer (#6): instantaneousPower is a dead sentinel ('-500') on every
TP1-class washer dump collected so far, and cumulativePower is absent
outright on at least one model. WASHER_ENERGY_METER now hides both
sensors instead of showing a misleading "0 W"/perpetual "unavailable".
Fridge (#7): temperature sensors/setpoints hardcoded '°F', ignoring the
unit each device actually reports per-reading -- fixed via a new
unit_fn hook read live from the resource. Also corrects
DEFROST_BLOCK_STATUS's polarity (DEFROST_BLOCK_ON means actively
defrosting, not "blocked", confirmed against live dumps) and adds
REFRIGERATION_FALLBACK for /refrigeration/0, closing the last unbound
href surfaced by issue #7's diagnostic dump.
Oven: applies the same live-unit-reading fix defensively to
OVEN_SETPOINT, which shares the same aggregate resource shape.
Adds cycles-until-due and last-cleaned sensors decoded from the same
options[] array the course selector already reads (DrumCleanProposal_N -
WashingTimes_N for cycles remaining, DrumCleanLog_<iso> for last-cleaned),
verified byte-for-byte against a live app screenshot ("Potreba cistenia po
37 cykloch" / "Naposledy cistene pred 10 dnami").
Course selection is now a writable select entity sourced from each
device's own x.com.samsung.da.editCourseList (via a new options-as-callable
form on SelectDesc, reading the coordinator's full resource snapshot
instead of just the entity's own href), rather than a hardcoded course
table baked into Python. A MostUsed_ field on the same resource was
considered as a fallback but rejected after byte-level analysis showed it
doesn't reliably encode a course list. When a device never populates
editCourseList, the selector isn't created at all (exists_fn now takes
(rep, resources) to check a sibling href, matching the existing
match_fn(rep, resources) pattern).
Display names moved out of Python into strings.json/translations under
entity.select.{washer,dishwasher}_cycle.state.*, matching this
integration's existing translation_key convention (fridge.py's ice_type,
flex_zone_mode, etc.) instead of hardcoding English names in code.