Skip to content

Add ThinQ2 device handler for CST_570004_WW (LG ceiling-cassette AC) - #101

Closed
3735943886 wants to merge 4 commits into
anszom:masterfrom
3735943886:cst-570004-cassette
Closed

Add ThinQ2 device handler for CST_570004_WW (LG ceiling-cassette AC)#101
3735943886 wants to merge 4 commits into
anszom:masterfrom
3735943886:cst-570004-cassette

Conversation

@3735943886

@3735943886 3735943886 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds MQTT translation for CST_570004_WW, an LG ceiling-cassette indoor unit
(deviceType 401), typically installed as several IDUs on one multi-split ODU.
Previously these only worked in bridge mode.

It speaks the same DualCool AC TLV protocol as RAC_056905_WW, so per the
preferred direction in #73 this extends RAC rather than duplicating it, and
drives features from the device's real capability frame wherever possible
not hardcoded per-model tables. It does not modify tlv_device.ts, and
CST_570004_WW is a new modelId, so no existing device is affected.

Why a plain CST_570004_WW: RAC_056905_WW alias doesn't work

I tried the alias first (as suggested in #73). It fails because CST diverges from
RAC in ways proven by the capability dump below:

  1. 0xA7 framing — CST sends its async/query TLV frames with UART header
    byte6 = 0xA7 instead of 0x87, so tlv_device drops every frame. Handled in
    CST's own processData (same approach as POT_056905_WW), not in the shared base.
  2. Feature bitmap moved — CST omits RAC's 0x2CC entirely.
  3. Mode table differs — CST's auto is wire value 3, not RAC's 6; RAC
    decodes wire 3 as undefined (blank mode) and writes auto as the unsupported 6.
  4. Filter data lives elsewhere — RAC reads filter usage from a basic-filter priv-command
    that returns an unpopulated counter on CST (used=0, life=720); the real usage is in value
    tags 0x356 (rated life 2400 h) / 0x355 (remaining).
  5. Fan scale differs, and RAC would expose a bogus jet switch + a heat mode this
    cooling-only unit doesn't have.

Capability-driven decoding (read, don't hardcode)

Rather than synthesize, the handler reads what CST actually advertises. Aligned caps
dump (RAC's test frame vs a live CST frame):

tag RAC CST meaning
0x2C1 0x57 = {0,1,2,4,6} 0x0F = {0,1,2,3} supported modes (bit N = wire mode N): CST = cool/dry/fan_only/auto, auto=3
0x2C2 0x17C = {2,3,4,5,6,8} 0x1D5 = {0,2,4,6,7,8} supported fan speeds (bit N = wire speed N)
0x2CB 0x1006 = {1,2,12} feature bitmap (CST's relocated 0x2CC): bit1 energy-save, bit2 auto-dry
0x2CC 0x6 RAC's feature bitmap; absent on CST
0x2CD 0xF 0x1FE037 RAC reads jet/swing here; on CST this holds unrelated data (13 bits set)
0x2D3 0x7 0x401011 timers — bit0 sleep + bit4 start/stop read correctly, no change
0x2E1/0x2E2 32 / 60 temp range 16–30 °C (raw/2)

Concretely:

  • Energy saving is read from 0x2CB (mapped into RAC's 0x2CC slot, masking bit2
    since CST exposes auto-dry as a duration select), replacing an earlier presence heuristic.
  • Temperature range is read from 0x2E1/0x2E2, fulfilling RAC's existing
    TODO: some devices report these temp ranges via tags 0x2e1 - 0x2ec, falling back
    to RAC's 18–30 when absent.
  • HVAC modes use CST's wire table (auto=3), confirmed by both 0x2C1 bit3 and a
    live 0x1F9=3 capture.
  • Filter usage is read from value tags 0x356 (rated life 2400 h) / 0x355 (remaining),
    exposed as a remaining-% and a used-time (h) sensor. RAC's basic-filter priv-command counter
    is unpopulated on CST (used=0, life=720), and this model's app exposes no filter reset or
    last-reset date, so RAC's priv-command filter entities (including the reset button) are dropped.
  • Jet is suppressed and swing is taken from the real on/off value tags 0x205/0x206,
    because CST's 0x2CD is not RAC's jet/swing bitmap. This one genuinely can't be read
    cleanly — documented rather than guessed.

Extra entities CST exposes

Humidity (0x336, raw/10 = %RH), always-on Power (0x2B3), auto-dry duration select
(0x20E) + remaining sensor (0x225, minutes), display brightness (0x21F), comfort
energy saving (0x23F), and a wind-mode / comfort-airflow select
(0x290/0x291/0x3D6/0x3D7, mutually exclusive one-hot flags).

Testing

  • tests/cloud/devices/CST_570004_WW.test.ts drives real captured packets
    (caps / values / filter reply) through the handler and asserts: 0xA7 normalization,
    caps-derived temp range and energy-save, auto wire value 3 (read + write), jet/heat
    suppression, CST fan scale, and the extra components. npm run test → 302/302;
    prettier --check and npm run build clean.
  • Physical verification on 4 live IDUs (LG multi-split): per-unit humidity matches the
    app; filter remaining/used match the app across all 4 units (2400 h rated); auto mode set
    from HA actually engages auto on the unit; power reads 0 W idle and rises while cooling.

Known limitations (not guessed)

  • 0x2C2's fan bitmap is understood but has an unresolved off-by-one at CST's lowest speed,
    so the fan list is the hand-verified {1,2,4,6,7,8} rather than auto-derived.
  • Energy in kWh is cloud-computed (reads 0 locally); derive in HA via a Riemann integral of
    the Power (W) sensor.

🤖 Generated with Claude Code

CST_570004_WW is an LG ceiling-cassette IDU (deviceType 401), typically
installed as several IDUs on one multi-split ODU. It speaks the same DualCool
AC TLV protocol as RAC_056905_WW, so this handler extends RAC and patches the
CST-specific differences, driving features from the device's real capability
frame wherever possible:

- 0xA7 async/query UART header (vs 0x87) is normalized before delegating.
- Feature bitmap: CST omits RAC's 0x2CC but reports the same bitmap under the
  adjacent tag 0x2CB, so it is read as the feature source (bit2 masked, since
  auto-dry is exposed as a duration select, not RAC's binary sensor).
- Temperature range is read from caps 0x2E1/0x2E2 (16-30 C), fulfilling RAC's
  existing 'TODO: read 0x2e1-0x2ec' instead of the hardcoded 18 C minimum.
- HVAC modes: CST advertises {0,1,2,3} in caps 0x2C1 and reports 0x1F9=3 for
  auto, so mode uses cool=0/dry=1/fan_only=2/auto=3 (no heat) rather than RAC's
  table (which decodes wire 3 as undefined and writes auto as an unsupported 6).
- Filter: CST reports 0x2F1 'no filter' yet its basic-filter priv-command
  returns real data, so the filter is probed regardless (with retries).
- Cooling-only: jet is suppressed (CST's 0x2CD is not RAC's jet/swing bitmap).
- CST-specific fan scale (1/2/4/6/7/8) and on/off vertical/horizontal swing.
- Extra entities: humidity (0x336), always-on Power (0x2B3), auto-dry duration
  select (0x20E) + remaining sensor (0x225), display brightness (0x21F),
  comfort energy saving (0x23F), and a wind-mode select (comfort airflow).

Verified on 4 live IDUs (LG multi-split). Includes a unit test driving real
caps/values/filter captures through the handler.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@3735943886
3735943886 force-pushed the cst-570004-cassette branch from 7bab4f5 to 97cea98 Compare July 23, 2026 04:34
3735943886 and others added 2 commits July 23, 2026 14:24
…mand filter

RAC reads filter usage from a basic-filter priv-command (0x02/0x02). On CST
that command returns an unpopulated counter — all four IDUs reply identically
used=0/life=720 regardless of actual usage — so it does not match the app.

The real filter usage is in value tags: 0x356 = rated life (2400 h, constant),
0x355 = remaining hours. valuesReceived now skips the priv-command probe (so
RAC creates none of its priv filter entities), and setConfig derives a
'Filter remaining' (%) and 'Filter used time' (h) sensor from 0x355/0x356
via a read hook on the live 0x355 counter. Verified against the app on all
four units (e.g. 0x355=1172 -> 1173 h remaining / 1227 used / 49%).

These cassettes' app has no filter reset and no last-reset date, so RAC's
reset button and changed-date entities (both priv-command based) are dropped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Setting the HVAC mode alone is ignored by the unit while it is powered off, so
selecting a mode in HA (off -> cool/dry/fan_only/auto) did nothing. Verified on
hardware: the app turns on by sending 0x1f7=1 together with the mode/fan/temp in
one frame. The mode write now forces power on (0x1f7=1) and attaches it, so a
single atomic frame turns the unit on in the selected mode — no separate power
command or inter-frame delay needed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@anszom

anszom commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Thanks for the PR.

The decision to base this implementation from RAC sounds good, but I think a better course of action would be to split out an AC_common base class - instead of translating the packets to match RAC's expectations. This way the common logic could stay the same, but the differences would be more clear. Can you do that?

This change could also be useful for #89

@3735943886

Copy link
Copy Markdown
Contributor Author

The decision to base this implementation from RAC sounds good, but I think a better course of action would be to split out an AC_common base class - instead of translating the packets to match RAC's expectations. This way the common logic could stay the same, but the differences would be more clear. Can you do that?

Sounds good, I'll do that.

@3735943886

Copy link
Copy Markdown
Contributor Author

Closing this — I'll open a new PR refactored onto a shared ac_common base class.

@3735943886 3735943886 closed this Jul 27, 2026
@3735943886
3735943886 deleted the cst-570004-cassette branch July 27, 2026 01:33
@anszom

anszom commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Note that you don't need to close & open a new PR. Each push (or force-push) to a branch updates an existing PR. You can reopen this one when you're ready as well.

Also, I recommend rebasing to follow the base branch instead of merging, this makes the commit history cleaner

tunarabuuu added a commit to tunarabuuu/rethink that referenced this pull request Aug 2, 2026
Splits the RAC-family TLV logic into an ac_common base class (as suggested in
review of the earlier ceiling-cassette attempt, anszom#101) instead of translating
packets to RAC's expectations. CST_170004_WW is its first user; every enum was
swept end to end on a live unit with bridge mode on. korean.ts carries the
Korean label helper these Korean-market drivers share.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@3735943886 3735943886 mentioned this pull request Aug 3, 2026
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.

2 participants