Complete dryer coverage from issue #14 screenshots (DV90BB5245AES1)

Name the two remaining unidentified course codes (0x21 Hygiene Care,
0x4C Air Refresh) by matching the SmartThings app's course-list order
against editCourseList in the reporter's dump.

Also close three entity gaps the screenshots surfaced that v0.6.0
missed: dry level was read-only despite the app exposing a writable
Level 1/2/3 picker; the app's "AI pattern" toggle (AiOption_On/Off)
and "Auto cycle link" toggle (/cycleinterface/vs/0, previously
globally ignored as always-empty on washer dumps) were both unbound.
Scope that ignore to washer.py now that the dryer binds it for real.
This commit is contained in:
Marc Billow committed 2026-07-24 00:43:58 +00:00
1 parent cf18cc3ecc
commit e07d6ed706
10 files changed
+121 -18

No files matched your search

@@ -26,5 +26,6 @@ REGISTRY = DeviceRegistry(
dryer.DRYER_SETTINGS,
dryer.DRYER_COURSE,
dryer.DRYER_DIAGNOSIS,
dryer.AUTO_CYCLE_LINK,
]),
)
@@ -15,5 +15,6 @@ REGISTRY = DeviceRegistry(
common.WATER_METER,
operational.OPERATIONAL_STATE,
dishwasher.DIAGNOSIS,
washer.CYCLE_INTERFACE_IGNORED,
]),
)
@@ -4,13 +4,15 @@ Dryer-specific controls only. The shared laundry surface -- power/kids-lock/
remote-control fallback pairs, buzzer, energy meter, job-beginning-status, and
the /course/vs/0 cycle select -- lives in laundry.py.
/washer/vs/0 -> DRYER_SETTINGS (dryLevel, dryTime, dryerType, wrinklePrevent)
/course/vs/0 -> DRYER_COURSE (shared cycle select; see below)
/diagnosis/vs/0 -> DRYER_DIAGNOSIS
/washer/vs/0 -> DRYER_SETTINGS (dryLevel, dryTime, dryerType, wrinklePrevent)
/course/vs/0 -> DRYER_COURSE (shared cycle select, plus the AI-pattern
toggle read off the same options[] array; see below)
/diagnosis/vs/0 -> DRYER_DIAGNOSIS
/cycleinterface/vs/0 -> AUTO_CYCLE_LINK
"""
from ..capability import Capability
from ..entities import SensorDesc, SwitchDesc
from .laundry import cycle_select
from ..entities import SelectDesc, SensorDesc, SwitchDesc
from .laundry import bool_option_switch, cycle_select
def _wrinkle_write(p, rep, href=None):
@@ -23,8 +25,15 @@ DRYER_SETTINGS = Capability(
href='/washer/vs/0',
poll_tier='warm',
entities=(
SensorDesc(key='dry_level', field='x.com.samsung.da.dryLevel',
name='Dry level', icon='mdi:water-percent'),
# supportedDryLevel values (e.g. 'None'/'Less'/'Normal'/'More') are
# already human-readable on this device, unlike the combo-unit
# numeric-minute codes washer.py's washer_dry_level select needs
# translations for -- no translation_key needed here.
SelectDesc(key='dry_level', field='x.com.samsung.da.dryLevel',
name='Dry level', icon='mdi:water-percent',
options_field='x.com.samsung.da.supportedDryLevel',
write_fn=lambda p, rep, href=None: (
['washer', 'vs', '0'], {'x.com.samsung.da.dryLevel': p})),
SensorDesc(key='dry_time', field='x.com.samsung.da.dryTime',
name='Dry time', icon='mdi:timer'),
SensorDesc(key='dryer_type', field='x.com.samsung.da.dryerType',
@@ -40,15 +49,21 @@ DRYER_SETTINGS = Capability(
# laundry.cycle_select (options read live from /wm/editcourse/vs/0, written as
# an RMW on the options array). Course display names live in translations
# under entity.select.dryer_cycle (Table_03, DV5000-class, captured
# 2026-05-29). Codes 0x21 and 0x4C appear in the issue #14 DV90BB5245AES1
# editCourseList but aren't identified yet -- they render as the raw code
# until named. The /st/dryercourse/vs/0 resource re-encodes the same selected
# course and is ignored (ignored.py) -- the mirror of how /st/washercourse/vs/0
# is ignored for washers.
# 2026-05-29; codes 0x21/0x4C named from the issue #14 DV90BB5245AES1
# SmartThings course-list screenshots on 2026-07-24). The /st/dryercourse/vs/0
# resource re-encodes the same selected course and is ignored (ignored.py) --
# the mirror of how /st/washercourse/vs/0 is ignored for washers.
#
# The same options[] array also carries AiOption_On/Off -- the app's "AI
# pattern" toggle ("Get cycle recommendations based on your usage patterns"),
# confirmed against the issue #14 screenshots. Same On/Off token shape as
# washer.py's bubble-soak/pre-wash/intensive toggles; not previously bound.
DRYER_COURSE = Capability(
href='/course/vs/0',
entities=(
cycle_select(translation_key='dryer_cycle', icon='mdi:tumble-dryer'),
bool_option_switch('ai_pattern', 'AI pattern', 'mdi:creation', 'AiOption',
entity_category='config', gate_on_presence=True),
),
)
@@ -60,3 +75,26 @@ DRYER_DIAGNOSIS = Capability(
name='Diagnosis', entity_category='diagnostic'),
),
)
# /cycleinterface/vs/0 -- the app's "Auto cycle link" toggle ("automatically
# set to the best drying cycle by communicating with the washer cycle"; both
# devices must stay connected). Empty ({}) on every washer dump seen so far,
# which is why it used to sit in the global ignored.IGNORED list -- the
# issue #14 dryer dump is the first to populate it. washer.py scopes its own
# ignore locally now (washer.CYCLE_INTERFACE_IGNORED) to avoid a collision.
def _auto_cycle_link_write(p, rep, href=None):
if p not in ('On', 'Off'):
return None
return ['cycleinterface', 'vs', '0'], {'x.com.samsung.da.cycleInterfaceEnabled': p}
AUTO_CYCLE_LINK = Capability(
href='/cycleinterface/vs/0',
entities=(
SwitchDesc(key='auto_cycle_link', field='x.com.samsung.da.cycleInterfaceEnabled',
name='Auto cycle link', icon='mdi:link-variant',
entity_category='config',
value_fn=lambda v: v == 'On',
write_fn=_auto_cycle_link_write),
),
)
@@ -117,11 +117,15 @@ IGNORED: list[Capability] = [
# data -- common.ENERGY_METER on /energy/consumption/vs/0 is the only
# real source for this control.
Capability(href='/energy/consumption/0'),
# Empty ({}) on every washer dump seen so far -- nothing to expose.
Capability(href='/cycleinterface/vs/0'),
# OCF-native duplicate of /drlc/vs/0 above -- same utility-program
# dependency this integration doesn't support locally.
Capability(href='/drlc/0'),
# NOTE: /cycleinterface/vs/0 used to be listed here as empty ({}) on
# every washer dump seen so far. The issue #14 dryer dump populates it
# (cycleInterfaceEnabled) as the app's "Auto cycle link" toggle, so it's
# now a real capability there (dryer.AUTO_CYCLE_LINK) -- washer.py scopes
# its own ignore locally (washer.CYCLE_INTERFACE_IGNORED) to avoid a
# global-ignore/real-capability href collision.
# OCF-native duplicate of /operational/state/vs/0, which is already
# modeled by operational.OPERATIONAL_STATE (a richer, write-capable
# capability with start/pause/stop buttons and a delay-start control)
@@ -354,3 +354,10 @@ WASHER_COURSE = Capability(
'IntensiveSetting', 'IntensiveAvailableSet'),
),
)
# /cycleinterface/vs/0 -- washer-to-dryer "Auto cycle link" pairing signal
# (dryer.AUTO_CYCLE_LINK). Empty ({}) on every washer/combo dump seen so far
# (plain washers, FlexWash, and washer/dryer combos alike), so it's scoped
# out here rather than left in the global ignored.IGNORED list, which would
# collide with the real dryer-side capability.
CYCLE_INTERFACE_IGNORED = Capability(href='/cycleinterface/vs/0')
+3 -1
View File
@@ -115,10 +115,12 @@
"1e": "Outdoor",
"1f": "Mixed Load",
"20": "Iron Dry",
"21": "Hygiene Care",
"23": "Quick Dry 35",
"24": "Cool Air",
"25": "Warm Air",
"27": "Time Dry"
"27": "Time Dry",
"4c": "Air Refresh"
}
},
"washer_dosing_quantity": {
@@ -115,10 +115,12 @@
"1e": "Outdoor",
"1f": "Mixed Load",
"20": "Iron Dry",
"21": "Hygiene Care",
"23": "Quick Dry 35",
"24": "Cool Air",
"25": "Warm Air",
"27": "Time Dry"
"27": "Time Dry",
"4c": "Air Refresh"
}
},
"washer_dosing_quantity": {