Merge pull request #403 from mbillow/claude/agents-triage-fixes-7vc6xl

laundry: add DV6800N's Table_00 dryer courses (issue #394)
This commit is contained in:
Marc Billow
2026-08-19 21:56:43 -05:00
committed by GitHub
12 changed files with 451 additions and 11 deletions
@@ -63,6 +63,12 @@ DRYER_SETTINGS = Capability(
# code. It shares no codes with Table_03 above -- 'a5' Bedding here and
# '01' Normal are both table-scoped, so a Table_03 dryer never picks up a
# Table_00 label or vice versa (see laundry.cycle_select's table_href).
# A DV6800N -- same DA_WM_A51_20_COMMON board, also Table_00 -- confirmed
# 14 more courses the same way (issue #394); its /course/vs/0 supportedOptions
# only advertises a different subset of this same table (each model exposes
# whichever courses its hardware supports), not a conflicting code family --
# the one code both reporters confirmed, 'a5', means Bedding on both. Folded
# into the same catalog entry below rather than a new one.
#
# Drum Clean+ maintenance tracking (issue #258) reuses washer.py's
# DrumCleanProposal_/WashingTimes_/DrumCleanLog_ tokens on this same
@@ -344,7 +344,20 @@
"a4": "Časové sušení",
"a6": "Rychlé sušení",
"a3": "Sportovní oblečení",
"a2": "Jemné prádlo"
"a2": "Jemné prádlo",
"9a": "Bavlna",
"ca": "Provětrání",
"db": "Super Speed",
"99": "Smíšená náplň",
"93": "Žehlení",
"b5": "Vlna",
"d7": "Outdoor péče",
"96": "Studený vzduch",
"97": "Teplý vzduch",
"7f": "Časové sušení",
"98": "Rychlé sušení 35",
"eb": "Jemné prádlo",
"b6": "Syntetika"
}
},
"dryer_cycle_table_03": {
@@ -344,7 +344,20 @@
"a4": "Zeittrocknen",
"a6": "Schnelltrocknen",
"a3": "Sportkleidung",
"a2": "Feinwäsche"
"a2": "Feinwäsche",
"9a": "Baumwolle",
"ca": "Frischluft",
"db": "Super Speed",
"99": "Mischwäsche",
"93": "Bügeltrocken",
"b5": "Wolle",
"d7": "Outdoor-Pflege",
"96": "Kaltluft",
"97": "Warmluft",
"7f": "Zeittrocknen",
"98": "Schnelltrocknen 35",
"eb": "Feinwäsche",
"b6": "Synthetik"
}
},
"dryer_cycle_table_03": {
@@ -344,7 +344,20 @@
"a4": "Time Dry",
"a6": "Quick Dry",
"a3": "Active Wear",
"a2": "Delicates"
"a2": "Delicates",
"9a": "Cotton",
"ca": "Air Wash",
"db": "Super Speed",
"99": "Mixed Load",
"93": "Iron Dry",
"b5": "Wool",
"d7": "Outdoor Care",
"96": "Cool Air",
"97": "Warm Air",
"7f": "Time Dry",
"98": "Quick Dry 35",
"eb": "Delicates",
"b6": "Synthetics"
}
},
"dryer_cycle_table_03": {
@@ -541,7 +541,20 @@
"a4": "Secado por tiempo",
"a6": "Secado rápido",
"a3": "Ropa deportiva",
"a2": "Delicados"
"a2": "Delicados",
"9a": "Algodón",
"ca": "Aireación",
"db": "Súper velocidad",
"99": "Carga mixta",
"93": "Planchado fácil",
"b5": "Lana",
"d7": "Cuidado para exterior",
"96": "Aire frío",
"97": "Aire caliente",
"7f": "Secado por tiempo",
"98": "Secado rápido 35",
"eb": "Delicados",
"b6": "Sintéticos"
}
},
"dryer_cycle_table_03": {
@@ -344,7 +344,20 @@
"a4": "Asciugatura a tempo",
"a6": "Asciugatura rapida",
"a3": "Abbigliamento sportivo",
"a2": "Delicati"
"a2": "Delicati",
"9a": "Cotone",
"ca": "Arieggiatura",
"db": "Super Speed",
"99": "Carico misto",
"93": "Pronto da stirare",
"b5": "Lana",
"d7": "Cura outdoor",
"96": "Aria fredda",
"97": "Aria calda",
"7f": "Asciugatura a tempo",
"98": "Asciugatura rapida 35",
"eb": "Delicati",
"b6": "Sintetici"
}
},
"dryer_cycle_table_03": {
@@ -344,7 +344,20 @@
"a4": "시간건조",
"a6": "쾌속건조",
"a3": "운동복",
"a2": "섬세의류"
"a2": "섬세의류",
"9a": "면의류",
"ca": "송풍",
"db": "쾌속건조",
"99": "혼합",
"93": "다림질건조",
"b5": "울",
"d7": "아웃도어케어",
"96": "송풍건조",
"97": "온풍건조",
"7f": "시간건조",
"98": "쾌속건조 35분",
"eb": "섬세의류",
"b6": "합성섬유"
}
},
"dryer_cycle_table_03": {
@@ -344,7 +344,20 @@
"a4": "Tijdprogramma",
"a6": "Snel drogen",
"a3": "Sportkleding",
"a2": "Fijne was"
"a2": "Fijne was",
"9a": "Katoen",
"ca": "Luchtverfrissing",
"db": "Super Speed",
"99": "Gemengde was",
"93": "Strijkdroog",
"b5": "Wol",
"d7": "Outdoorverzorging",
"96": "Koude lucht",
"97": "Warme lucht",
"7f": "Tijdprogramma",
"98": "Snel drogen 35",
"eb": "Fijne was",
"b6": "Synthetisch"
}
},
"dryer_cycle_table_03": {
+244
View File
@@ -0,0 +1,244 @@
{
"device0": [
{
"rt": [
"x.com.samsung.devcol",
"oic.wk.col"
],
"if": [
"oic.if.baseline",
"oic.if.ll",
"oic.if.b"
]
},
{
"href": "/alarms/vs/0",
"rep": {}
},
{
"href": "/configuration/vs/0",
"rep": {
"x.com.samsung.da.region": "0000000000",
"x.com.samsung.da.countryCode": "US\u0001"
}
},
{
"href": "/course/vs/0",
"rep": {
"x.com.samsung.da.supportedModes": [
"HOMECARE_WIZARD_V2"
],
"x.com.samsung.da.options": [
"DeviceType_0165",
"UpdateAllow_NotAllowed",
"Course_9A",
"MixedLoadBell_Disable",
"MixedLoadBellNoti_Nothing",
"LaundryOutTime_0",
"SeamlessControl_Disable",
"KidsLockBypass_On",
"DetergentOnce_0",
"DetergentLeft_0",
"DetergentBase_0",
"DetergentAlarm_Off",
"DetergentType_0",
"DetergentTotal_0",
"SpecialFunction_4",
"AvailableDelayTime_188",
"LaundryPlannerUserSetTime_0",
"ProgressTimeSet_B22D00B4003C",
"WrinklePreventRunning_Off",
"EnergyLevelSet_050502050503020304010204030203",
"MostUsed_9AD20EE000",
"MixedLoadBellSet_02FFFF02FFFFFFFFFFFFFFFFFF02",
"UsagesDB_ok",
"EnergyKW_396",
"TimeSync_NotSupported"
],
"x.com.samsung.da.supportedOptions": [
"29AD20EE000CAD10EE000DBD204E00099D20EE00093D102E000B5D102E000D7D204E000A5D204E00096D000E10E97D000E10E7FD000E33E98D000E000EBD204E000B6D20EE000"
]
}
},
{
"href": "/cycleinterface/vs/0",
"rep": {}
},
{
"href": "/diagnosis/vs/0",
"rep": {
"x.com.samsung.da.diagnosisStart": "Ready"
}
},
{
"href": "/energy/consumption/0",
"rep": {}
},
{
"href": "/energy/consumption/vs/0",
"rep": {
"x.com.samsung.da.instantaneousPowerUnit": "W",
"x.com.samsung.da.instantaneousPower": "-500",
"x.com.samsung.da.cumulativePower": "692600",
"x.com.samsung.da.cumulativeUnit": "Wh",
"x.com.samsung.da.cumulativeDate": "1786964400",
"x.com.samsung.da.cumulativeDateUTC": "1786960800"
}
},
{
"href": "/file/information/vs/0",
"rep": {
"x.com.samsung.timeoffset": "+01:00"
}
},
{
"href": "/information/vs/0",
"rep": {
"x.com.samsung.da.modelNum": "DA_WM_A51_20_COMMON|20221341|30010102001211000103000000000000",
"x.com.samsung.da.description": "DA_WM_A51_20_COMMON_DV6800N/DC92-01967B_0404",
"x.com.samsung.da.serialNum": "**REDACTED**",
"x.com.samsung.da.otnDUID": "**REDACTED**",
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "0",
"x.com.samsung.da.description": "DA_WM_A51_20_COMMON|20221341|30010102001211000103000000000000",
"x.com.samsung.da.type": "Software",
"x.com.samsung.da.number": "02198A230708(E257)",
"x.com.samsung.da.newVersionAvailable": "0"
},
{
"x.com.samsung.da.id": "1",
"x.com.samsung.da.description": "DA_WM_A51_20_COMMON",
"x.com.samsung.da.type": "Firmware",
"x.com.samsung.da.number": "17111305,17122616",
"x.com.samsung.da.newVersionAvailable": "0"
}
]
}
},
{
"href": "/kidslock/0",
"rep": {
"value": false
}
},
{
"href": "/kidslock/vs/0",
"rep": {
"x.com.samsung.da.kidsLock": "Ready"
}
},
{
"href": "/operational/state/0",
"rep": {
"currentMachineState": "**REDACTED**",
"machineStates": "**REDACTED**",
"jobStates": [
"None",
"Drying",
"Cooling",
"Finish"
],
"currentJobState": "None",
"remainingTime": "03:08:00",
"progressPercentage": "1"
}
},
{
"href": "/operational/state/vs/0",
"rep": {
"x.com.samsung.da.state": "Ready",
"x.com.samsung.da.remainingTime": "03:08:00",
"x.com.samsung.da.progressPercentage": "1",
"x.com.samsung.da.progress": "None",
"x.com.samsung.da.delayEndTime": "00:00:00",
"x.com.samsung.da.supportedProgress": [
"None",
"Drying",
"Cooling",
"Finish"
]
}
},
{
"href": "/power/0",
"rep": {
"value": false
}
},
{
"href": "/power/vs/0",
"rep": {
"x.com.samsung.da.power": "Off"
}
},
{
"href": "/realtimenotiforclient/vs/0",
"rep": {
"x.com.samsung.da.timeforshortnoti": "0",
"x.com.samsung.da.periodicnotisubscription": "true"
}
},
{
"href": "/remotectrl/0",
"rep": {
"value": false
}
},
{
"href": "/remotectrl/vs/0",
"rep": {
"x.com.samsung.da.remoteControlEnabled": "false"
}
},
{
"href": "/setting/vs/0",
"rep": {}
},
{
"href": "/st/dryercourse/vs/0",
"rep": {
"x.com.samsung.da.st.dryerMode": "Table_00_Course_9A",
"x.com.samsung.da.st.courseTable": "Table_00"
}
},
{
"href": "/washer/vs/0",
"rep": {
"x.com.samsung.da.wrinklePrevent": "Off",
"x.com.samsung.da.dryLevel": "2",
"x.com.samsung.da.supportedDryLevel": [
"None",
"1",
"2",
"3"
],
"x.com.samsung.da.dryTime": "00:00:00",
"x.com.samsung.da.supportedDryTime": [
"00:00:00",
"00:30:00",
"01:00:00",
"01:30:00",
"02:00:00",
"02:30:00"
],
"x.com.samsung.da.dryerType": "Electricity"
}
},
{
"href": "/wm/editcourse/vs/0",
"rep": {}
},
{
"href": "/wm/jobbeginingstatus/vs/0",
"rep": {}
},
{
"href": "/wm/setinfo/vs/0",
"rep": {
"x.com.samsung.da.isModelSettingWithoutSC": "false",
"x.com.samsung.da.isModelSettingPowerOnOff": "false"
}
}
]
}
+23
View File
@@ -0,0 +1,23 @@
{
"state_keys": [
"alarm_code",
"child_lock",
"completion_minutes",
"cycle",
"cycle_active",
"delay_start_hours",
"diagnosis",
"dry_level",
"dry_time",
"dryer_type",
"energy_kwh",
"finish_time",
"job_beginning_status",
"machine_state",
"power_switch",
"progress",
"progress_percentage",
"remote_control",
"wrinkle_prevent"
]
}
+61 -4
View File
@@ -1,7 +1,7 @@
"""Tests for dryer support and washer/dryer consistency (issue #14)."""
from custom_components.localthings.registry.adapter import flatten
from custom_components.localthings.registry.by_type import for_device_by_model
from custom_components.localthings.registry.by_type import for_device_by_model, resolve
from custom_components.localthings.registry.capabilities import dryer, ignored
from custom_components.localthings.registry.discovery import discover
from custom_components.localthings.registry.entities import SelectDesc
@@ -90,8 +90,12 @@ def test_course_bound_to_shared_course_vs_0():
def test_reported_table_00_course_codes_are_translated():
"""The reporter confirmed these codes on a DVE45R6300W/A3 by selecting
each cycle and reading back the raw course code (issue #357)."""
"""The DVE45R6300W/A3 reporter confirmed these codes by selecting each
cycle and reading back the raw course code (issue #357). A DV6800N --
same DA_WM_A51_20_COMMON board, also Table_00 -- later confirmed 14 more
(issue #394): a different subset of the same table, not a conflicting
code family (its one code in common with #357, 'a5', means Bedding on
both), so both sets share the one dryer_cycle_table_00 catalog entry."""
from custom_components.localthings.catalog import translated_states
desc = next(
@@ -99,7 +103,32 @@ def test_reported_table_00_course_codes_are_translated():
)
table_00 = {"/st/dryercourse/vs/0": {"x.com.samsung.da.st.courseTable": "Table_00"}}
assert desc.translation_key(table_00) == "dryer_cycle_table_00"
confirmed = {"01", "9c", "a5", "9e", "9b", "27", "a0", "a4", "a6", "a3", "a2"}
confirmed = {
"01",
"9c",
"a5",
"9e",
"9b",
"27",
"a0",
"a4",
"a6",
"a3",
"a2", # issue #357
"9a",
"ca",
"db",
"99",
"93",
"b5",
"d7",
"96",
"97",
"7f",
"98",
"eb",
"b6", # issue #394
}
assert confirmed <= translated_states("select", "dryer_cycle_table_00")
@@ -109,3 +138,31 @@ def test_st_dryercourse_is_ignored():
ignored_hrefs = {c.href for c in ignored.IGNORED}
assert "/st/dryercourse/vs/0" in ignored_hrefs
assert "/st/washercourse/vs/0" in ignored_hrefs
def _dv6800n():
resources = _load_device("dryer_dv6800n")
reg = resolve(resources, device_types=("oic.wk.d", "oic.d.dryer"))
return reg, resources
def test_dv6800n_no_unbound_hrefs():
"""Every resource in the issue #394 dump binds or is ignored."""
reg, resources = _dv6800n()
unbound = []
discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append)
assert unbound == []
def test_dv6800n_course_codes_read_from_dump():
"""A DV6800N (DA_WM_A51_20_COMMON, issue #394) reports 'Table_00' same
as #357's DVE45R6300W/A3, so it resolves to the same catalog entry --
its /course/vs/0 supportedOptions just advertises a different subset of
the same table (see test_reported_table_00_course_codes_are_translated)."""
_, resources = _dv6800n()
desc = next(
e for e in dryer.DRYER_COURSE.entities if e.key == "cycle" and isinstance(e, SelectDesc)
)
assert desc.translation_key(resources) == "dryer_cycle_table_00"
confirmed = ["9A", "CA", "DB", "99", "93", "B5", "D7", "A5", "96", "97", "7F", "98", "EB", "B6"]
assert desc.options(resources) == confirmed
+19
View File
@@ -121,6 +121,25 @@ def test_registry_reproduces_golden_state_keys_for_dryer_dve50a8600():
)
def test_registry_reproduces_golden_state_keys_for_dryer_dv6800n():
"""DA_WM_A51_20_COMMON/DV6800N (issue #394) resolves via /oic/d's
'oic.d.dryer' device type, not board-token guessing -- its modelNum's
board tokens ('DA', 'WM', 'COMMON') are all deliberately excluded from
_BOARD_TOKEN_TO_KEY (see registry/by_type's module docstring)."""
from tests.conftest import _load_device
resources = _load_device("dryer_dv6800n")
golden = json.loads((GOLDEN / "dryer_dv6800n.json").read_text())
state_keys = _new_state_keys(
"dryer_dv6800n", resources, device_types=("oic.wk.d", "oic.d.dryer")
)
assert set(state_keys) == set(golden["state_keys"]), (
f"state_keys mismatch:\n"
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
)
def test_registry_reproduces_golden_state_keys_for_airconditioner():
from tests.conftest import _load_device