Skip to content

Add support for the F3L7CYK5W_US_WIFI front-load washer - #135

Open
Danimal4326 wants to merge 1 commit into
anszom:masterfrom
Danimal4326:add-f3l7cyk5w-washer
Open

Add support for the F3L7CYK5W_US_WIFI front-load washer#135
Danimal4326 wants to merge 1 commit into
anszom:masterfrom
Danimal4326:add-f3l7cyk5w-washer

Conversation

@Danimal4326

Copy link
Copy Markdown

Adds a driver for the LG F3L7CYK5W_US_WIFI front-load washer (deviceType 201, thinq2).

It shares the AABB record layout of F3L2CYU__ — 25-byte record led by a 0x18 marker, 0xEC two-record status frame with record B at offset 29, 0xEB single record at offset 3 — but is deliberately a separate handler.

Why it isn't an alias

The course table is different. Not a superset, not a renaming — a different assignment, disagreeing at nine of twelve dial positions. 0x06 is NORMAL here, where the F3L2CYU__ table says Heavy Duty.

This was the trap worth flagging for anyone adding a sibling model: a first pass at this driver inherited the sibling's table on the assumption that a shared record layout implies a shared course enum. It doesn't. Every position is now confirmed against the cloud's apCourseFLUpper25inchBaseUS and cross-checked against the printed control panel, with a regression test pinning it.

Three more divergences:

  • rec[11] packs two fields — low nibble is a live rinse count (it decrements as rinses complete), high nibble is the extra-rinse setting. F3L2CYU__ reads only the high nibble and documents the low nibble as a constant, which isn't true here.
  • rec[15] has two option bits the sibling lacks: child lock (0x01) and Rinse+Spin (0x20). Both F3L2CYU__ and the RV13B6BSD dryer document child lock as unfindable in device frames — it's simply at a different bit on this model.
  • rec[4:6] is a real initial-cycle-time estimate that pins when a cycle starts while rec[2:4] counts down. F3L2CYU__ states that model has none and reuses the countdown bytes for both purposes.

Also decoded: the delay reserve clock, TCLCount (rec[22]), loadLevel (rec[24]), and phase 0x15 = ADD_DRAIN, the door-unlocked state behind the Add Garments button. Exercising Add Garments also confirmed phase 0x06 = PAUSE, which F3L2CYU__ currently lists as unverified.

0xE2 frames are intentionally not decoded

Worth calling out because it looks like an oversight. 0xE2 is 28 bytes with a valid 0x18 record at offset 3 — structurally identical to 0xEB — but it's emitted in a burst after a cycle finishes and replays a stale snapshot of that cycle's start (phase Sensing, the pre-run estimate, the pre-increment TCLCount). Decoding it knocks the machine back from Complete to Sensing every time a wash ends. There's a test asserting it stays ignored.

How it was confirmed

Live capture with tools/rethink-capture.ts --cloud while driving the physical panel: full dial sweep, every option toggled one at a time, a delay-wash set and cancelled, and complete cycles including Add Garments mid-run.

One note for anyone repeating this: filter cloud notifications on the capture tool's matchesDevice flag. A dryer on the same account emits washerDryer updates whose keys collide with the washer's (state, preState, temp), and merging those in silently corrupts the mapping.

Omitted rather than guessed:

  • remote start — the cloud reports it, but it moved in lockstep with the rec[16] door-lock bit throughout, so no independent bit could be isolated
  • TurboWash — this model has no such button, so the sibling's rec[15] bit 0x80 is left unmapped rather than inherited
  • Signal — this model transmits no frame at all when it changes, verified both mid-cycle and idle with the machine powered on
  • error codes — never triggered

Testing

All fixtures are real captured frames, each with the confirming cloud field named in a comment above it. Full suite passes (360 tests). The driver has been running against the physical appliance via MQTT discovery in Home Assistant, reporting 22 entities that track the panel through complete cycles.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EdP8L3BhHJFWX1ZKfQLCcM

Shares the AABB record layout of F3L2CYU__ (25-byte record led by a 0x18
marker) but is deliberately a separate handler rather than an alias, for four
reasons:

  * The course table is different -- not a superset or a renaming, a different
    assignment. It disagrees at nine of twelve dial positions; 0x06 is NORMAL
    here, where the F3L2CYU__ table says Heavy Duty. This was the trap: a first
    pass at this driver inherited the sibling's table on the assumption that a
    shared record layout implies a shared course enum, and mislabelled almost
    every position. There is a regression test pinning it.
  * rec[11] packs two fields: the low nibble is a live rinse count (it
    decrements as rinses complete) and the high nibble is the extra-rinse
    setting. F3L2CYU__ reads only the high nibble and documents the low nibble
    as a constant, which is not true of this model.
  * rec[15] carries two option bits F3L2CYU__ does not have: child lock (0x01)
    and Rinse+Spin (0x20). Both F3L2CYU__ and the RV13B6BSD dryer document
    child lock as unfindable in device frames, "appears cloud-side only" -- it
    is simply at a different bit here.
  * rec[4:6] is a persistent initial-cycle-time estimate that pins when a cycle
    starts while rec[2:4] counts down. F3L2CYU__ states that model has no such
    field and reuses the countdown bytes for both purposes.

Also decoded: the delay reserve clock, TCLCount (rec[22]), loadLevel (rec[24]),
and phase 0x15 = ADD_DRAIN, the door-unlocked state behind the Add Garments
button. Exercising Add Garments also confirmed phase 0x06 = PAUSE, which
F3L2CYU__ lists as unverified.

0xE2 frames are deliberately left undecoded, and there is a test asserting so.
They are 28 bytes with a valid 0x18 record at offset 3, structurally identical
to 0xEB, but they are emitted in a burst AFTER a cycle finishes and replay a
stale snapshot of that cycle's start -- decoding them knocks the machine back
from Complete to Sensing every time a wash ends.

Every offset was confirmed by capturing live traffic with
tools/rethink-capture.ts --cloud while driving the physical control panel, and
matching each byte change against the LG cloud field that moved with it. One
note for anyone repeating this: filter cloud notifications on the capture tool's
matchesDevice flag. A dryer on the same account emits washerDryer updates whose
keys collide with the washer's (state, preState, temp), and merging those in
silently corrupts the mapping.

Omitted rather than guessed: remote start (the cloud reports it, but it moved in
lockstep with the rec[16] door-lock bit so no independent bit could be
isolated), TurboWash (this model has no such button, so the sibling's rec[15]
bit 0x80 is left unmapped), the Signal setting (this model transmits no frame at
all when it changes, verified both mid-cycle and idle), and error codes.

All test fixtures are real captured frames, with the confirming cloud field
named in a comment above each one.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant