laundry: cover a device that advertises cloud cycles it has never loaded

A survey of every laundry diagnostics dump attached to an issue turned up 14
devices, 4 of which carry cloud-course tokens. Two were already known; the
two new ones are both useful, and one contradicts something the
investigation write-up asserted.

A DW5000C dishwasher (issues #113/#123) advertises four downloaded programs
and carries no payload token for any of them. That is a shape the corpus
didn't have: the feature is not washer-only (DA_DW, not DA_WM), and a device
can name programs whose payloads have never been observed. The existing code
already handles it correctly -- nothing learnable, nothing offered, gap still
counted for the Repairs issue -- so this adds the fixture, golden, and tests
that keep it that way.

A second WW5000C (issues #259/#343, firmware _B048) holds the same saved
program as the first one's captured "Towels", and the two payloads differ at
exactly one byte: byte 3, 04 against 06. Everything else -- id, slot, all
four varying tag values, the whole tail -- is identical. So byte 3 is neither
a per-board constant nor a property of the program, and the doc's claim that
it is always 04 on this board was wrong.

That is also the strongest argument yet for learning payloads per device: a
catalog keyed on program id would have shipped one unit's byte 3 to the
other. Nothing changes in the implementation as a result -- it never had a
catalog -- but the reasoning is now backed by evidence rather than caution.

Also recorded: both WW5000C units advertise the byte-identical slot list
despite different firmware, so the program set looks factory- or
region-assigned rather than user-curated; and a sentinel's byte 2 equals the
selected course on one dump but not the other, so it stays unused.
This commit is contained in:
Marc Billow
2026-08-09 21:46:29 +00:00
parent 22b4508f95
commit 9d28b088cb
5 changed files with 472 additions and 14 deletions
+76 -14
View File
@@ -7,15 +7,29 @@ records the byte-level work behind it, including a decode that fit one
device perfectly and collapsed on the second — the reason nothing in the
shipped code interprets a program payload at all.
Two real devices carry these tokens:
Four devices in the corpus carry these tokens (a survey of every laundry
diagnostics dump attached to an issue turned up 14 devices; the other 10 have
no cloud tokens at all, so this is a minority feature):
| dump | model | slots advertised | blob width |
| --- | --- | --- | --- |
| `washer_ww5000c_cloud` | WW5000C, `DA_WM_TP1_21_COMMON`, Table_02 | 9 | 20 bytes |
| `washer_wa55a7700av` | WA55A7700AV, `DA_WM_TP1_21_COMMON`, Table_02 | 2 | 16 bytes |
| dump | model | slots advertised | payloads seen | blob width |
| --- | --- | --- | --- | --- |
| `washer_ww5000c_cloud` | WW5000C `_B06C`, `DA_WM_TP1_21_COMMON`, Table_02 | 9 | 2 | 20 bytes |
| `washer_wa55a7700av` | WA55A7700AV, `DA_WM_TP1_21_COMMON`, Table_02 | 2 | 1 | 16 bytes |
| `dishwasher_dw5000c_cloud` | DW5000C, `DA_DW_TP1_21_COMMON` | 4 | **0** | — |
| (not fixtured) | WW5000C `_B048`, issues #259/#343, Table_02 | 9 | 1 | 20 bytes |
Both are Table_02. Note that already: same course table, different
everything else.
Three things follow immediately from that table:
- **It isn't washer-only.** The DW5000C is a `DA_DW_` dishwasher.
- **A device can advertise programs it has never loaded.** The DW5000C names
four slots and carries no `CloudCourse_`/`OneTimeCloudCourse_` token at
all. Nothing about it is learnable until its owner runs one, which is
exactly the situation the Repairs issue exists to explain.
- **Both WW5000C units advertise the byte-identical slot list**
(`0A5C286B2D0C55301A`, same nine slots in the same order) despite different
firmware builds. Either the set is a factory/regional default rather than
something each owner curates, or the two dumps share an owner — unresolved,
but worth knowing before assuming a user picked their own programs.
## The three tokens
@@ -136,11 +150,44 @@ WA55 00 | 1C59 | 05 | 49:16 4D:11 4A:22 4C:20 | 37:F0 | AC:
- Then a fixed tail, terminated on both by an `AC:<value>` pair. The entire
width difference is two trailing pairs the WA55 doesn't carry.
The tail is not program data. Across all nine WW5000C programs, byte 3 is
always `04` and bytes 12–19 are byte-identical — every trailing pair carries
value `F0` except the `AC` terminator, and `35` vs `37` looks like a board or
profile marker rather than a field. (Byte 3 is not a field count: the board
with the *higher* value has *fewer* pairs.)
The tail is not program data. Across all nine WW5000C programs bytes 12–19
are byte-identical — every trailing pair carries value `F0` except the `AC`
terminator, and `35` vs `37` looks like a board or profile marker rather than
a field. (It is not a field count either: the board with the *higher* leading
byte has *fewer* pairs.)
### Byte 3 is not part of a program's identity
Worth its own heading, because it is the single strongest argument against
ever shipping a table of payloads. The second WW5000C (issues #259/#343,
firmware `_B048`) has its saved `CloudCourse` set to the same program as the
first one's "Towels" capture — and the two payloads differ at exactly one
byte:
```
_B06C "Towels" 00 04 0A 04 49 40 4D 13 4A B8 4C 00 35 F0 04 F0 05 F0 AC 00
_B048 CloudCourse 00 04 0A 06 49 40 4D 13 4A B8 4C 00 35 F0 04 F0 05 F0 AC 00
^^
```
Same program id, same slot, same values on all four varying tags, same tail.
Only byte 3 moves, `04` → `06`. So it is neither a per-board constant (both
are WW5000C) nor a property of the program (identical in every other
respect) — most likely a download revision or sequence counter.
A hardcoded catalog keyed on program id would therefore have shipped one
unit's byte 3 to the other unit. Whether the appliance would reject that, or
accept it and do something unintended, is untested and does not need to be:
every payload is learned from the device it will be replayed to.
### Sentinels
The `FFFF` "nothing loaded" payload takes its board's own width (16 bytes on
the WA55, 20 on the WW5000C `_B048`) and always carries byte 3 = `00`. Its
byte 2 is *not* reliably meaningful: it equals the currently selected course
on the WA55 (`01`, on `Course_01`) and does not on the `_B048` (`1B`, on
`Course_1C`). Nothing keys off it — a sentinel is rejected on its `FFFF`
prefix, and its byte 2 is not an advertised slot in either dump anyway.
So the payload is tag/value, not fixed offsets — but knowing the grammar
doesn't recover the values. The same four tags carry non-overlapping ranges
@@ -194,5 +241,20 @@ sensors this decode would have enabled were dropped for the same reason.
Care for "Baby care intensive", `30` Cloudy Day for "Cloudy heaven") and
sometimes doesn't (`21` Colors for "Sports"). Probably a base-course
reference; not reliable enough to use.
3. A third device with downloaded programs would settle the tag/value
reading of the payload.
3. What does byte 3 count? It moves between two units holding the identical
program (`04` vs `06`) and is `00` on every sentinel. A revision or
download counter is the obvious guess; a dump taken before and after
re-downloading the same program would confirm it.
4. **The value encoding is still open, and a third *washer* won't
necessarily settle it.** The survey found one, but its saved program is a
duplicate of one already captured, so it adds no new tag values. What is
actually needed is a dump from a board whose `/washer/vs/0` speaks in
named levels (Cold/Warm/Hot, Low/High) *while that unit is sitting on a
downloaded program* — then the payload's `49`/`4A` values can be read
against settings that describe the same program. The WA55 is such a
board but was captured on a local course, which is why it can't be used
(see the trap above).
5. The DW5000C's four slots (`8E 8D 8F 02`) are in the same numeric range as
dishwasher course codes (its selected course is `86`), unlike the
washers' slots. One payload from that machine would show whether slot ids
are drawn from the course-code space on some boards.
+297
View File
@@ -0,0 +1,297 @@
{
"device0": [
{
"rt": [
"x.com.samsung.devcol",
"oic.wk.col"
],
"if": [
"oic.if.baseline",
"oic.if.ll",
"oic.if.b"
]
},
{
"href": "/realtimenotiforclient/vs/0",
"rep": {
"x.com.samsung.da.timeforshortnoti": "0",
"x.com.samsung.da.periodicnotisubscription": "true"
}
},
{
"href": "/alarms/vs/0",
"rep": {
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "0",
"x.com.samsung.da.description": "Alarm",
"x.com.samsung.da.alarmType": "Device",
"x.com.samsung.da.code": "DishA_Disable",
"x.com.samsung.da.triggeredTime": "2022-07-28T13:45:57",
"x.com.samsung.da.state": "Deleted"
}
]
}
},
{
"href": "/diagnosis/vs/0",
"rep": {
"x.com.samsung.da.diagnosisStart": "Ready"
}
},
{
"href": "/energy/consumption/vs/0",
"rep": {
"x.com.samsung.da.cumulativeSavedPower": "0"
}
},
{
"href": "/energy/consumption/0",
"rep": {}
},
{
"href": "/course/vs/0",
"rep": {
"x.com.samsung.da.supportedModes": [
"HOMECARE_WIZARD_V2"
],
"x.com.samsung.da.options": [
"DeviceType_0812",
"UpdateAllow_NotAllowed",
"CourseDefaultTimeSet_008B008E009A006D003C000A006600A50059009D",
"CourseDefaultTempSet_3C41363F41413737373700003C3C464649493C3C",
"CourseDefaultFahrenheitSet_8C95819195958383838300008C8C9E9EA3A38C8C",
"Course_86",
"DetergentOnce_0",
"DetergentLeft_0",
"DetergentBase_0",
"DetergentAlarm_Off",
"DetergentType_0",
"DetergentTotal_0",
"ProgressTimeSet_420622820456B20384",
"SendToDevice_Off",
"GMT_F2",
"SavingModeCondition_01010207828485868E8D020300",
"SavingMode_Off",
"DownloadCourseList_8F",
"StormWashZone_Off",
"AutoDoorRelease_On",
"Sound_On",
"WaterLevelSet_050404050302010205010400",
"CloudExtraCourse_8E8D8F02",
"EnergyLevelSet_050403050202010305040300",
"UsagesDB_ok",
"EnergyKW_396",
"DrumCleanLog_Empty",
"TimeSync_NotSupported"
],
"x.com.samsung.da.supportedOptions": [
"482830CB002C002D00283830CB002C002D00284830CB002C002D00285830CB000C000D00086830CB002C002D002908308B000C000D0008E830CB000C000D0008D830CB002C002D0028F8308B000C000D00002830CB002C002D002"
]
}
},
{
"href": "/power/vs/0",
"rep": {
"x.com.samsung.da.power": "On"
}
},
{
"href": "/power/0",
"rep": {
"value": true
}
},
{
"href": "/kidslock/vs/0",
"rep": {
"x.com.samsung.da.kidsLock": "Ready"
}
},
{
"href": "/kidslock/0",
"rep": {
"value": false
}
},
{
"href": "/operational/state/vs/0",
"rep": {
"x.com.samsung.da.state": "Run",
"x.com.samsung.da.remainingTime": "00:44:00",
"x.com.samsung.da.progressPercentage": "28",
"x.com.samsung.da.delayStartTime": "00:00:00",
"x.com.samsung.da.progress": "Wash",
"x.com.samsung.da.supportedProgress": [
"None",
"Predrain",
"Wash",
"Rinse",
"Drying",
"Finish"
]
}
},
{
"href": "/operational/state/0",
"rep": {
"currentMachineState": "**REDACTED**",
"machineStates": "**REDACTED**",
"jobStates": [
"None",
"Predrain",
"Wash",
"Rinse",
"Drying",
"Finish"
],
"currentJobState": "Wash",
"remainingTime": "00:44:00",
"progressPercentage": "28"
}
},
{
"href": "/information/vs/0",
"rep": {
"x.com.samsung.da.modelNum": "DA_DW_TP1_21_COMMON|30010741|40000200001711004981000000200000",
"x.com.samsung.da.description": "DA_DW_TP1_21_COMMON_DW5000C/DD92-0010741_0001",
"x.com.samsung.da.serialNum": "**REDACTED**",
"x.com.samsung.da.otnDUID": "**REDACTED**",
"x.com.samsung.da.diagProtocolType": "WIFI_HTTPS",
"x.com.samsung.da.diagLogType": [
"errCode",
"dump"
],
"x.com.samsung.da.diagDumpType": "file",
"x.com.samsung.da.diagEndPoint": "SSM",
"x.com.samsung.da.diagMnid": "0AJT",
"x.com.samsung.da.diagSetupid": "WD0",
"x.com.samsung.da.diagMinVersion": "1.0",
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "0",
"x.com.samsung.da.description": "DA_DW_TP1_21_COMMON|30010741|40000200001711004981000000200000",
"x.com.samsung.da.type": "Software",
"x.com.samsung.da.number": "00081A230213(A214)",
"x.com.samsung.da.newVersionAvailable": "0"
},
{
"x.com.samsung.da.id": "1",
"x.com.samsung.da.description": "Firmware_1_DB_30010741230602142FFFFFFFFFFFFFFFFFFFFFFFFFFE(081230010741FFFFFFFF_30000000)(FileDown:0)(Type:0)",
"x.com.samsung.da.type": "Firmware",
"x.com.samsung.da.number": "23060214,FFFFFFFF",
"x.com.samsung.da.newVersionAvailable": "0"
}
]
}
},
{
"href": "/file/information/vs/0",
"rep": {
"x.com.samsung.timeoffset": "+00:00"
}
},
{
"href": "/wm/editcourse/vs/0",
"rep": {}
},
{
"href": "/wm/setinfo/vs/0",
"rep": {
"x.com.samsung.da.isModelSettingWithoutSC": "false",
"x.com.samsung.da.isModelSettingPowerOnOff": "false"
}
},
{
"href": "/dishwasher/vs/0",
"rep": {
"x.com.samsung.da.highTemperatureDry": "Off",
"x.com.samsung.da.sanitize": "Off",
"x.com.samsung.da.selectedZone": "ON_ON",
"x.com.samsung.da.rinseLevel": "0",
"x.com.samsung.da.supportedSelectedZone": [
"OFF_ON",
"ON_ON"
],
"x.com.samsung.da.supportedSanitize": [
"Off",
"On"
],
"x.com.samsung.da.supportedHighTemperatureDry": [
"Off",
"On"
],
"x.com.samsung.da.supportedRinseLevel": [
"0",
"1",
"2",
"3",
"4",
"5",
"6"
]
}
},
{
"href": "/otninformation/vs/0",
"rep": {
"x.com.samsung.da.target": "Micom",
"x.com.samsung.da.newVersionAvailable": "false"
}
},
{
"href": "/remotectrl/vs/0",
"rep": {
"x.com.samsung.da.remoteControlEnabled": "true"
}
},
{
"href": "/remotectrl/0",
"rep": {
"value": true
}
},
{
"href": "/configuration/vs/0",
"rep": {
"x.com.samsung.da.region": "0000000000",
"x.com.samsung.da.countryCode": "CA"
}
},
{
"href": "/filter/waterfilter/vs/0",
"rep": {}
},
{
"href": "/drlc/0",
"rep": {
"DRLevel": 0,
"start": "0000-00-00T00:00:00Z",
"duration": 0,
"override": false
}
},
{
"href": "/drlc/vs/0",
"rep": {
"x.com.samsung.da.drlcLevel": "0",
"x.com.samsung.da.durationminutes": "0",
"x.com.samsung.da.start": "0000-00-00T00:00:00Z",
"x.com.samsung.da.override": "Off",
"x.com.samsung.da.realSaving": "Off"
}
},
{
"href": "/water/consumption/vs/0",
"rep": {
"x.com.samsung.da.cumulativeWater": "2662000"
}
},
{
"href": "/wm/submode/vs/0",
"rep": {
"setTemperatureUnit": "F"
}
}
]
}
+26
View File
@@ -0,0 +1,26 @@
{
"state_keys": [
"alarm_code",
"auto_release_dry",
"child_lock",
"completion_minutes",
"cycle",
"cycle_active",
"delay_start_hours",
"diagnosis_status",
"energy_saved_kwh",
"filter_status",
"filter_usage",
"finish_time",
"firmware_update",
"heated_dry",
"machine_state",
"power_switch",
"progress",
"progress_percentage",
"remote_control",
"sanitize",
"storm_wash",
"water_liters"
]
}
+53
View File
@@ -122,6 +122,59 @@ class TestRealDumps:
store.observe(rep)
assert store.download_candidates() == []
def test_a_dishwasher_advertises_programs_it_has_never_loaded(self):
"""DW5000C (issues #113/#123): CloudExtraCourse_ names four slots
with no CloudCourse_/OneTimeCloudCourse_ token anywhere in the array.
Two things at once -- the feature is not washer-only (DA_DW, not
DA_WM), and a device can advertise programs whose payloads have never
been observed. Nothing is learnable here, so nothing is offerable,
but the gap is still countable and still worth telling the user
about."""
rep = _load_device("dishwasher_dw5000c_cloud")["/course/vs/0"]
assert cloudcourse.advertised_slots(rep) == ["8E", "8D", "8F", "02"]
assert cloudcourse.supports_cloud_courses(rep) is True
store = cloudcourse.CloudCourses()
assert store.observe(rep) is False
assert store.view() == {}
assert cloudcourse.undiscovered(rep, store.snapshot()) == ["8E", "8D", "8F", "02"]
def test_byte_three_is_not_part_of_a_programs_identity(self):
"""Two WW5000C units on different firmware (issue #342's _B06C and
issues #259/#343's _B048) report the same program -- same id, same
slot, same field values, same tail -- differing only at byte 3.
This is the evidence against a shipped catalog of payloads: keyed on
program id, one unit's capture would have carried the other unit's
byte 3. Both are recorded and replayed per device instead, so
whatever byte 3 tracks never has to be understood."""
b06c = "00040A0449404D134AB84C0035F004F005F0AC00"
b048 = "00040A0649404D134AB84C0035F004F005F0AC00"
assert b06c != b048
# Same program by every identifier the device gives us.
assert cloudcourse.slot_of(b06c) == cloudcourse.slot_of(b048) == "0A"
assert b06c[:6] == b048[:6]
assert b06c[8:] == b048[8:]
# Learned separately per device, and each replays what it saw.
for blob in (b06c, b048):
store = cloudcourse.CloudCourses()
store.observe(_rep(["CloudExtraCourse_0A", f"CloudCourse_{blob}"]))
assert store.blob("0A") == blob
def test_a_sentinel_slot_byte_is_not_the_current_course(self):
"""The two sentinels in the corpus disagree about this -- WA55's byte
2 happens to equal its selected course, the WW5000C _B048's does not
(1B against Course_1C). Nothing keys off it: a sentinel is rejected
on its FFFF prefix, and its byte 2 is not an advertised slot either
way."""
wa55 = "FFFF010049004D004A804C0037F0AC00"
b048 = "FFFF1B0049004D004A804C0035F004F005F0AC00"
assert cloudcourse.slot_of(wa55) is None
assert cloudcourse.slot_of(b048) is None
# Different board widths, same sentinel rule.
assert len(wa55) != len(b048)
def test_undiscovered_counts_against_what_the_device_advertises(self):
rep = _load_device("washer_ww5000c_cloud")["/course/vs/0"]
store = cloudcourse.CloudCourses()
+20
View File
@@ -1519,3 +1519,23 @@ def test_registry_reproduces_golden_state_keys_for_washer_ww5000c_cloud():
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_dishwasher_dw5000c_cloud():
"""A DW5000C dishwasher (issues #113/#123) that advertises four
downloaded programs it has never loaded -- CloudExtraCourse_ with no
CloudCourse_/OneTimeCloudCourse_ payload alongside it (issue #342).
Proves the cloud-cycle machinery is not washer-only and, more usefully,
that a device can advertise programs whose payloads have never been
seen. It adds no entity of its own either way."""
from tests.conftest import _load_device
resources = _load_device("dishwasher_dw5000c_cloud")
golden = json.loads((GOLDEN / "dishwasher_dw5000c_cloud.json").read_text())
state_keys = _new_state_keys("dishwasher_dw5000c_cloud", resources)
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))}"
)