Extend AirDresser support to DA_DF_TP2_20_COMMON (#157)

A different board generation from issue #162's DA_DF_A51_20_COMMON, also
carrying the '_DF_' modelNum token and so already routed into the
air_dresser registry -- but reporting two resources #162's board doesn't:

  /st/airdressercourse/vs/0 -- the course table id (Table_00), read the
    same way washer/dryer read /st/washercourse|dryercourse/vs/0. Wired up
    as AIR_DRESSER_COURSE's table_href and added to the global ignore list,
    mirroring that existing pair exactly.
  /airdresseroption/sanitize/vs/0 -- a genuine on/off setting (not covered
    by any existing capability), added as AIR_DRESSER_SANITIZE.

Introducing table_href means the course select's translation_key is now
always the table-lookup callable, so the bare 'air_dresser_cycle' catalog
entry added for #162 (only ever used when no table_href was passed) is
unreachable in every case and is removed -- both boards fall back to the
shared 'cycle' entry until their course tables get named, same as
washer/dryer's own precedent for an unrecognized table.
This commit is contained in:
Marc Billow
2026-07-28 13:50:59 +00:00
parent 053e15bba6
commit a3d9cce164
9 changed files with 456 additions and 25 deletions
@@ -16,6 +16,7 @@ REGISTRY = DeviceRegistry(
*common.POWER,
air_dresser.AIR_DRESSER_SETTINGS,
air_dresser.AIR_DRESSER_COURSE,
air_dresser.AIR_DRESSER_SANITIZE,
laundry.JOB_BEGINNING_STATUS,
operational.OPERATIONAL_STATE,
dishwasher.DIAGNOSIS,
@@ -1,30 +1,36 @@
"""Capabilities specific to the AirDresser family (Samsung DA_DF_A51-class,
issue #162).
"""Capabilities specific to the AirDresser family (Samsung DA_DF-class,
issues #162/#157).
This board reports no oneUiVersion and no /information/vs/0 token any
existing family routes on, so it gets its own device type -- but most of
the resources it exposes are already handled by the shared laundry
This board family reports no oneUiVersion and no /information/vs/0 token
any existing family routes on, so it gets its own device type -- but most
of the resources it exposes are already handled by the shared laundry
machinery:
/washer/vs/0 -> AIR_DRESSER_SETTINGS (wrinkle_prevent only -- a
dedicated capability rather than reusing
dryer.DRYER_SETTINGS wholesale: this board's dump
never populates dryLevel/dryTime/dryerType at all,
and those are permanently-empty-not-just-absent
dryer.DRYER_SETTINGS wholesale: this board never
populates dryLevel/dryTime/dryerType at all, and
those are permanently-empty-not-just-absent
dryer-only fields on an AirDresser, not merely unset
ones, so binding them here would ship three sensors
that can never read anything on this device type)
/course/vs/0 -> AIR_DRESSER_COURSE (cycle select). This board has no
/wm/editcourse/vs/0 at all, so cycle_options() falls
through entirely to its supportedOptions fallback --
confirmed against the issue #162 dump: header nibble
'0' + 10 self-indexed one-byte records
(01/02/04/03/05/1A/1B/1C/07/08), all distinct, current
selection 'Course_01' among them. Course names aren't
identified yet (no code->name mapping was reported),
so they render as their raw codes until named in
translations, same as dryer.py's unidentified codes.
/course/vs/0 -> AIR_DRESSER_COURSE (cycle select), table id from
/st/airdressercourse/vs/0 (issue #157's
DA_DF_TP2_20_COMMON reports "Table_00"; issue #162's
DA_DF_A51_20_COMMON has no table resource at all and
falls back to the name-only 'cycle' key, same as an
unrecognized table on washer/dryer). Options come
from laundry.cycle_options -- #157's board populates
/wm/editcourse/vs/0's editCourseList directly; #162's
has no editcourse resource at all and falls through
to cycle_options' supportedOptions decode instead.
Course names aren't identified yet for either table
(no code->name mapping was reported), so they render
as their raw codes until named in translations, same
as dryer.py's unidentified codes.
/diagnosis/vs/0 -> reuses dishwasher.DIAGNOSIS
/airdresseroption/sanitize/vs/0 -> AIR_DRESSER_SANITIZE (issue #157 only;
#162's board doesn't report this resource at all)
"""
from ..capability import Capability
from ..entities import SwitchDesc
@@ -37,6 +43,12 @@ def _wrinkle_write(p, rep, href=None):
return ['washer', 'vs', '0'], {'x.com.samsung.da.wrinklePrevent': p}
def _sanitize_write(p, rep, href=None):
if p not in ('On', 'Off'):
return None
return ['airdresseroption', 'sanitize', 'vs', '0'], {'x.com.samsung.da.sanitize': p}
AIR_DRESSER_SETTINGS = Capability(
href='/washer/vs/0',
poll_tier='warm',
@@ -51,6 +63,18 @@ AIR_DRESSER_SETTINGS = Capability(
AIR_DRESSER_COURSE = Capability(
href='/course/vs/0',
entities=(
cycle_select(translation_key='air_dresser_cycle', icon='mdi:tshirt-crew'),
cycle_select(translation_key='air_dresser_cycle', icon='mdi:tshirt-crew',
table_href='/st/airdressercourse/vs/0'),
),
)
AIR_DRESSER_SANITIZE = Capability(
href='/airdresseroption/sanitize/vs/0',
poll_tier='warm',
entities=(
SwitchDesc(key='sanitize', field='x.com.samsung.da.sanitize',
icon='mdi:weather-sunny',
value_fn=lambda v: v == 'On',
write_fn=_sanitize_write),
),
)
@@ -107,6 +107,11 @@ IGNORED: list[Capability] = [
# exposed by dryer.DRYER_COURSE at /course/vs/0
# (x.com.samsung.da.st.dryerMode is "Table_03_Course_<same hex code>").
Capability(href='/st/dryercourse/vs/0'),
# AirDresser counterpart of the above (issue #157): read only for its
# courseTable id (air_dresser.AIR_DRESSER_COURSE's table_href), no
# entity of its own -- same "no entity, just the table id" role as
# /st/washercourse/vs/0 and /st/dryercourse/vs/0.
Capability(href='/st/airdressercourse/vs/0'),
# Empty on every washer dump seen so far.
Capability(href='/wm/welcomemsg/vs/0'),
# User-saved custom course slots (F1-FA). No controllable/observable
@@ -193,9 +193,6 @@
"ai_energy_level": {
"name": "AI Energy Mode level"
},
"air_dresser_cycle": {
"name": "Cycle"
},
"beverage_zone_mode": {
"name": "Beverage zone mode",
"state": {
@@ -193,9 +193,6 @@
"ai_energy_level": {
"name": "Niveau AI Energy Mode"
},
"air_dresser_cycle": {
"name": "Cyclus"
},
"beverage_zone_mode": {
"name": "Modus drankenzone",
"state": {