Add LG CordZero A9 stick vacuum HWWA9K_F2 - #116
Conversation
An AA..BB ThinQ2 stick vacuum (deviceType 504). The byte layout was recovered by injecting single-byte-changed state frames through the management API and reading LG cloud's decode back, one offset per observation. The appliance sends the current record last, with the previous record ahead of it after a setting changes, so the trailing record is decoded. Reads go through the base class's processAABB — the frame arrives with its leading AA/length and trailing CRC/BB stripped, so byte 0 is the class tag and the record offsets need no whole-frame adjustment. The 44-byte capability list the appliance also sends is rejected by its record-count guard. Warnings go through the logging utility. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Port from upstream Node PR #116 (anszom/rethink#116). AABB class 0xD2 14-byte records (trailing record is current); F024 writes for suction, mop, melody, volume, and brightness. Fixtures are the 2026-07-28 capture and LG-cloud oracle injections.
anszom
left a comment
There was a problem hiding this comment.
Please also add a note in the README.
| allowExtendedType({ | ||
| ...HADevice.config(meta, { name: 'LG Stick Vacuum' }), | ||
| components: { | ||
| status: sensor('status', 'State', { icon: 'mdi:robot-vacuum' }), |
There was a problem hiding this comment.
the enum sensors should use device_class: 'enum', options: ..., just like other devices do.
| const enumOf = (table: Record<number, string>, raw: number) => | ||
| table[raw] ?? (raw === 0 || raw === 0xff ? 'Unknown' : `Code ${raw}`) |
There was a problem hiding this comment.
If we switch to device_class: 'enum', then HA will reject non-matching enums, only the 'unknown' string will be an accepted fallback.
| // The appliance echoes a two-record state frame within a second, so the entity | ||
| // settles on what it actually took; this only keeps the UI from snapping back first. | ||
| this.publishProperty(prop, mqttValue) | ||
| } |
There was a problem hiding this comment.
would setting optimistic: true be a suitable replacement for this logic?
| * The base class strips the AA/length prefix before handing the body on, but every offset | ||
| * above is stated against the whole frame, the way the probes recorded them, so the frame is | ||
| * taken here unstripped. | ||
| */ |
There was a problem hiding this comment.
outdated comment. Please scan the whole PR for outdated comments and remove them or bring them up-to-date. A misleading comment is worse than no comment at all :)
| import HADevice from './base' | ||
| import AABBDevice from './aabb_device' | ||
|
|
||
| /* |
There was a problem hiding this comment.
please shorten the header block. LLMs do have a tendency to leave a history trail of older attempts & revisions, this is not useful here.
| /** processAABB receives the frame with its leading AA/length and trailing CRC/BB stripped, | ||
| * so byte 0 is the class tag and there are no leftover header/trailer bytes to skip. */ | ||
| const HEADER_LEN = 2 | ||
| const TRAILER_LEN = 0 |
There was a problem hiding this comment.
this serves no purpose now, remove the constant and the useless - TRAILER_LEN expressions
| test('every user-facing name and option is English', () => { | ||
| const { ha, thinq } = makeDevice() | ||
| thinq.emit('data', STATE) | ||
| const components = ha.devices[DEVICE_ID].config!.components as Record<string, { name?: string | null }> | ||
| for (const [key, comp] of Object.entries(components)) { | ||
| if (comp.name == null) continue | ||
| assert.match(comp.name, /[A-Za-z]/, `${key} name is English: ${comp.name}`) | ||
| } | ||
| const p = ha.devices[DEVICE_ID].properties | ||
| for (const key of ['status', 'clean_mode', 'suction_force', 'battery', 'nozzle', 'volume', 'brightness']) { | ||
| assert.match(String(p[key]), /[A-Za-z]/, `${key} value is English: ${p[key]}`) | ||
| } | ||
| }) |
There was a problem hiding this comment.
/[A-Za-z]/ will happily accept non-english words :) Just drop the test.
| ['F3L2CYU__']: F3L2CYU__, // LG front-load washer | ||
| ['RV13B6BSD_D_US_WIFI']: RV13B6BSD_D_US_WIFI, // LG electric dryer | ||
| WTL_FXU_BDV_NA_01, // LG WashTower | ||
| HWWA9K_F2, // LG CordZero A9 stick vacuum (deviceType 504) |
There was a problem hiding this comment.
Please add the deviceType 504 to the list in panel.js too.
Fourth of the per-device-class splits from #111.
HWWA9K_F2 — an AA..BB ThinQ2 stick vacuum (deviceType 504). The byte layout was recovered by injecting single-byte-changed state frames through the management API and reading the LG cloud's decode back, one offset per observation. The appliance sends the current record last (with the previous record ahead of it after a setting changes), so the trailing record is decoded.
Addressing your review notes from #111:
processAABBnow; the frame arrives with its leading AA/length and trailing CRC/BB stripped, so byte 0 is the class tag and the record offsets need no whole-frame adjustment. The 44-byte capability list the appliance also sends is still rejected, by its record-count guard.console.warncalls now uselog(...).All labels are English.
npm testpasses;tscclean.