Pr/ac_common - #122
Open
3735943886 wants to merge 10 commits into
Open
Conversation
The framing check accepts only byte 6 = 0x87, but some device families (ceiling cassettes, portable ACs) send 0xa7 there instead. POT_056905_WW worked around this by copying the whole first branch of processData() into an override just to widen that one comparison. Turn the comparison into an overridable isHeaderByte6() and drop the copy. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> (cherry picked from commit 3a3978a)
RAC_056905_WW is not one model's handler. It is the DualCool TLV scheme - 0x1f7 power, 0x1f9 mode, 0x1fa fan, 0x1fd/0x1fe temperatures, the 0x2cc / 0x2cd / 0x2d3 capability bitmaps, the pipe and ODU diagnostics, the basic-filter priv-command - with one model's particulars mixed into it. Anything else on the same scheme could either copy the file or be translated into RAC's expectations, and both have been tried in this project. So the protocol moves to ac_common and RAC keeps what is actually its own: its 0x1f9 / 0x1fa scales, its setpoint range, the +50 bias on its power reading, and the fact that it carries a basic-filter counter. Nothing here is tuned to a unit; where models are known to differ, the difference is stated by the subclass as an override. Behaviour-preserving, including the two fixes RAC took last week, which move across rather than being lost in the shuffle: - the filter reset queries the counter before clearing it (02cf15c), so a reset does not record a usage figure up to 24 hours short of what the filter ran - a hook already on modeChangeHooks is not also registered on powerChangeHooks (2641ccd), since power reads as a mode change too and the appliance was getting the same write twice on every power-up Both are now covered by RAC tests that fail without them: one counts the jet writes a power-up produces, the other reads the priv-command subcommands and asserts the query goes out before the reset. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The bitmap tests read `!!(caps & 0x02)` at each use, so what a bit meant lived only in the comment beside it and had to be right in every copy. The tags and the bits are named once at the top of the file and tested through one helper, which also answers the "unit reports no bitmap at all" case in one place instead of four: it has none of the features, which is the useful answer, because the entities behind them would have nothing to show. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both speak the same DualCool TLV scheme as RAC and CST - the same 0x1f7 /
0x1f9 / 0x1fa / 0x1fd / 0x1fe, the same 0x2da marking a capability response,
and in POT's case the same 0xa7 header byte as CST. They were standalone
TLVDevices only because ac_common did not exist when they were written: each
built its whole discovery config in its constructor, before a single packet
arrived, so nothing either unit reports had any say in it.
POT's capability response turns out to carry most of what was written out by
hand: 0x2c1 = 7 is exactly its cool/dry/fan_only, 0x2c2 = 0x54 sets exactly
the fan wire values 2/4/6, and 0x2e1 / 0x2e2 = 32 / 60 is the 16 - 30 C the
handler clamped to. Those are now read rather than assumed. Gating on what
the unit reports also gives it the entities it was always describing and
nobody was showing: error, nominal capacity, EEV opening, the two ODU
temperatures, instantaneous power from 0x2b3, the turn-on / turn-off timers
that 0x2d3 bit 2 advertises (bit 0 is clear, so no sleep timer), and a
climate action from 0x6c. The setpoint range is now published to HA instead
of only being enforced on write.
Two things POT keeps as they were, because the capability data does not
settle them:
- 0x2cd = 16 leaves every swing bit clear although the unit plainly swings,
so the axis is stated explicitly rather than derived.
- that axis stays on 0x322, which is the tag this handler has always
written, even though the captured values response carries 0x321 and not
0x322 - so the state is never read back. The existing test already noted
this. Which tag the unit acts on needs checking against hardware.
WIN gains the same treatment, and with it a fix: selecting "off" in HA did
nothing at all. The handler called setProperty('power', ...) where the field
is registered as 'climate-power', so that call only logged a warning, and the
mode write that followed returned undefined for 'off' - which setProperty
treats as "do not send". No frame left the bridge. The unit could not be
turned off. For the same reason a mode selected while the unit was off never
powered it on: the 0x1f7 attached to the write just re-sent whatever was last
seen. powerOnWithModeWrite states that intent properly. Also dropped is a
dry -> 8 write with no matching entry in the read table and no 'dry' in the
advertised mode list, and the device name moves from the discovery root, where
it did nothing, into the HA device where the other handlers put it.
Neither unit is verified against hardware - there is neither one here.
For POT what was done instead is a differential check: the captures already in
its test, replayed through the old handler and the new one, diffing the
discovery config, every field, every transform and the frames emitted for ten
HA commands. That says the migration changes nothing; it does not say the
original was right. The only intended wire change is that powering on now
attaches fan and setpoint, as it does on RAC.
WIN has less than that. No capture from an LW1823HRSM exists here, so its test
uses synthetic frames and deliberately sets no capability bitmap: it pins the
climate core and the writes, and can say nothing about which optional entities
real hardware would get. Other hardware is known to report this same ThinQ
model id with a different feature set. A capability dump from either is the
missing piece, and the "off" fix above is reasoned from the code rather than
watched working. One further wire change is unverified: a mode write no longer
re-sends 0x322 alongside, matching every other model here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 2f0849f)
Three things the previous pass left inconsistent. The mode and fan scales were declared three times per model - a wire-indexed table, a label-keyed record, and the list offered to HA - all carrying the same fact and all having to be kept in step by hand. The file already had the answer for this: WireLevels plus wireMaps(), one [label, wire] list from which the option list and both directions are derived, which the swing axes and addValueSelect were already using. The two most important mappings in the class were the ones not using it. Each model now writes modeLevels and fanLevels, in the order HA should offer them, and the six declarations become two. The maps are built on first use, not in a field initialiser, because a base-class field is initialised during super() - before the subclass has assigned the list it would be built from. Which implementation of a feature a unit has was asked two different ways per feature: a capability bit for auto dry's binary form but a plain flag for its select form, a method reading 0x2f1 for the priv-command filter but a plain flag for the value-tag one. Two selectors each, of different shapes, for one question - and nothing stopping a model answering yes twice, so the order this class happened to test them in silently decided the outcome. They become autoDryStyle() -> 'none' | 'binary' | 'select' and filterStyle() -> 'none' | 'priv' | 'valueTags'. One question, one answer. initMakeSetConfig() was 459 lines, a third of the file, and a linear script whose seams were already marked by blank lines and comments. It is now 16 lines calling one method per concern - climate core, diagnostics, capability features, auto dry, action, filter, power - each of which says in its own comment what decides whether it does anything. Also: ClimateComponent gains `modes`, which it was missing although HA's MQTT climate accepts it next to fan_modes and swing_modes; it had been slipped past the type with a conditional spread. Verified as before, by replaying the RAC, CST, POT and WIN frames through the handlers and diffing the discovery config, every registered field, each transform across raw 0-255 and every label any model offers, the published properties, and the bytes emitted for ten HA commands. One difference, in RAC's config only: it now states its mode list explicitly where it used to omit it and let HA fall back to a default. HA's default for a climate component is the same six modes, so what HA is told is unchanged; RAC was the only model relying on that, and deriving the list from modeLevels makes it uniform with the other three. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> (cherry picked from commit 176e9c9)
…s' shapes Two reviewers went over this while a stand-unit handler was being written against it (issue anszom#105), and between them they found that the shared base decides several things it has no business deciding, and that two of its capability bits are the wrong bits. The mode and fan lists are now narrowed by the 0x2c1 and 0x2c2 bitmaps. One modelId covers units that differ: the wall units' list declares heat and auto, and a cooling-only unit under the same modelId answers 7 - cool, dry, fan only - so Home Assistant has been offering two modes that the appliance silently ignores when they are picked. The list stays the vocabulary, since only the model knows which wire value carries which name; the bitmap is this unit's answer about which of them it has. A bitmap that is absent, zero, or describes none of the declared levels leaves the list alone: these bitmaps carry bits above the level cluster whose meaning is not established, and one that is not recognised is a reason to publish the model's list unchanged rather than to publish nothing. CST opts out of the fan half, where the bitmap offers wire 0 and the list derived from driving the appliance says 1. Two capability bits were tested at the wrong position. The model description names both bitmaps - 0x2cd is support.racSubMode and 0x2d3 is support.reserve - and labels each bit, writing bit N as key N+1. The turn-on / turn-off timer pair is @TIMEBS_ONOFF at bit 4, not bit 2; horizontal swing has @AC_MAIN_WIND_DIRECTION_SWING_LEFT_RIGHT_W at bit 5. The keys for the bits tested here do not appear in either enum, and of the three capability replies on file - a cassette read live, and the wall and stand units reported in the issue - all three set bit 4 for the timers and none sets bit 2, so no unit on hand has ever been given those entities. Both positions are accepted rather than swapped: the fixtures asserting the old bits are synthetic, and accepting both can only add an entity to a unit that advertises the documented bit, never take one away from an install relying on the other. The rest is shape the base could not express, each of which forced a stand-unit handler to reimplement something around it: - a wire level may now carry a repeated label. The stand units report fan speed 8 while drying and 7 while jet runs, both meaning "the appliance is driving the fan itself"; the first is what a write sends, both read back, and HA is offered the label once. - switches take a wiring description. Not every switch tag is 0/1: an AI dry enable is 0/255, a display or beep tag stores 1 for "off", and a cleaning cycle is started with one value and reports another while it runs. The read is an exact comparison, since truthiness is the wrong question for an inverted tag. - a switch may sit outside Configuration. HA files an entity by entity_category, and there is no string meaning Controls - the key has to be absent - so an everyday control had no way to be one. - autoDryStyle() gains 'switchLevel': an enable tag plus a separate strength select, which is neither of the two forms already here. - filterStyle() gains 'valueTagsReset'. The value-tag pair was assumed read-only; on the stand units the official app resets the counter by writing 0 to 0x355 and the appliance answers with a full life. The button is wired through fields_by_ha rather than addField, which would claim fields_by_id[0x355] from the read hook that derives the sensors, and it sends its own frame so that a reset the appliance ignores leaves HA still telling the truth. The rated life is published too, having been read and thrown away. Also documented on isHeaderByte6() that an override must widen the accepted set rather than replace it. Byte 6 is not a per-device constant - on the stand units the async state frames are 0xa7 while the private-channel acknowledgements stay 0x87 - and a handler that narrowed the hook would lose them the day the private branches are routed through it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The reserve bitmap advertises a turn-on / turn-off pair on 0x21c / 0x21b and these units do set the bit, so honouring the bitmap gave every one of them two more entities. They are not worth having: a schedule that lives inside the appliance is one Home Assistant cannot see the effect of until it fires, and anyone who wants the unit on at seven already has a better way to arrange it. The sleep timer stays. That one is a live countdown the appliance acts on by itself, so reporting it says something the user cannot otherwise see. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…appened to set support.racSubMode labels bit N as key N+1, and it gives the vertical axis two keys, 3 and 5. Only bit 2 was accepted here, so a unit advertising the axis on bit 4 was quietly denied the entity. No capture could have found that: the bit is missing from every capability reply on file, which is exactly the case where reading the model beats staring at bytes. Horizontal keeps bit 3 alongside bit 5 even though no enum defines its key 4. These tables are per-model and do not share a key space - the PAC stand unit has no support.racSubMode at all, and numbers the sleep timer 8 in support.reserve where the CST cassette numbers it 1 - so a key absent from one model's table says nothing about another's, and a RAC wall unit on file does set bit 3. Widening an axis can only add it to a unit that advertises it; narrowing one takes it away from a unit that works today. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Same DualCool TLV scheme as the residential units, so the protocol comes from
ac_common and this file is only the ways a cassette differs from a wall unit. It is
what the base class was split out for.
Mapping the modelId straight onto RAC_056905_WW was tried first, and does not work.
Five separate reasons, each measured on the units here:
- the cassette frames its async and query TLV with header byte 6 = 0xa7, so RAC's
parser drops every state frame. That is what isHeaderByte6 is for.
- it reports no 0x2cc at all; its feature bitmap is 0x2cb, and read through RAC's
accessor every feature behind it silently switches off
- 0x2cd is not the jet/positional-swing bitmap here. Its value has many unrelated
bits set on a unit that has neither jet nor positional swing, so honouring it
invents entities the appliance does not have
- the fan scale differs: six steps, and auto is wire 8 where RAC uses 3
- no basic-filter priv-command. Filter life is on the value tags 0x355 / 0x356,
read-only, and RAC's counter reads as absent
The mode list is likewise the cassette's own: 0x2c1 advertises {0,1,2,3} and the
unit reports 0x1f9=3 for auto, where the wall units use 6 - and a cassette on a
cooling-only outdoor unit has no heat hardware at all.
The one place a capability bitmap is deliberately not honoured is the fan list. 0x2c2
reports 469, i.e. wire 0, 2, 4, 6, 7, 8, which agrees with the driven list on five of
six steps and offers 0 where this has 1. All six were confirmed by driving the
appliance, so the list wins over an unexplained disagreement about the slowest step,
and fanCaps() says so rather than silently dropping "very low".
Decode vectors from a live unit; entities verified against the panel.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The third shape on the DualCool scheme, and the one that decided how two of ac_common's per-model questions are asked. Auto dry is neither of the forms the base class had. It is not the diagnostic on/off the 0x2cc bit advertises, and not the writable duration the cassette has: 0x20e is a plain enable that reads 0 or 255 and nothing else, the strength is its own 1..5 select on a second tag, and 0x225 counts the running cycle down in minutes. Offering it as the duration variant's "10 min / 30 min / 60 min" would be inventing options the appliance does not have, so autoDryStyle() gained a third answer instead. The filter is on the value tags like the cassette, but on this unit they are not read-only: the official app resets the counter with a plain TLV write of 0 to 0x355 and the appliance answers by reporting 0x355 = 0x356, a full life again. That is not something to probe for - a wrong guess writes a lifetime that contradicts the appliance's own display - so it is a separate filterStyle rather than a behaviour every value-tag unit inherits. The unit is also an argument against trusting the capability bitmaps: 0x2cc is absent entirely and 0x2cd reads 0, so ac_common's derivations answer "no" to every feature it demonstrably has. The per-model hooks exist for that, and it states each of them itself rather than being made to lie. NOT VERIFIED AGAINST HARDWARE. There is no such appliance here. Every value comes from kkqq9320's captures - 557 frames over four sessions of operating the unit by hand, reported in issue anszom#105 - and the file cites which frames each value came from; where something is inferred rather than seen, it says so. The tests replay those frames: the capability reply, a 94-tag values dump, the mode, fan and switch notifications, and both a cooling and a stopped 0xa8 record. What a capture cannot show is whether a write lands, so somebody with the unit should confirm those. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
kkqq9320
pushed a commit
to kkqq9320/rethink
that referenced
this pull request
Aug 4, 2026
0x225 has been published as '%' since this handler was written - inherited from upstream, which still declares it, and never measured here: all six earlier observations read 0 because no cycle ever ran while recording. It was briefly changed to 'min' on the sibling units' evidence and reverted deliberately, with a test pinning '%' so the next change would have to be a decision. The cycle has now been run (wall-autodry-20260804.jsonl). 0x225 started at 8 while the official app showed 8 minutes, and stepped 8 -> 7 in 59 s before the stop button took it to 0. The app's display and the decrement rate are independent of one another, and a percentage of an unknown-length cycle does not begin at 8. Same tag and same raw value as upstream - neither side applies a read_xform - so this is a label being corrected, not a decode. Worth noting for upstream: anszom#122's ac_common keeps '%' for the 'binary' auto-dry style, which is the style this unit's 0x2cc bit selects, and switches to 'min' only for 'select' and 'switchLevel'. That split is what the measurement contradicts, on this unit; other batches of the same modelId are not claimed. device_class 'duration' comes along to match PAC_910604_WW, whose comment cross-referencing RAC's '%' is updated here too.
Owner
|
Thank you for the PR. I've just returned from vacation and I'm slowly working through the PR & issue backlog. I'd appreciate your patience :) |
BluSyn
pushed a commit
to BluSyn/rethink-rs
that referenced
this pull request
Aug 21, 2026
Port from upstream Node PR #122 (anszom/rethink#122). TLV climate on 0xA7 using TlvDeviceCore; modes 0/1/5 and fans 2-6 plus 7/8-as-auto from issue #105 captures. Power is tenths of a watt (0x2b3); filter is the 0x355/0x356 value-tag pair; 307-byte 0xA8 records supply compressor running for hvac_action.
BluSyn
pushed a commit
to BluSyn/rethink-rs
that referenced
this pull request
Aug 21, 2026
Stop aliasing the ceiling cassette to RAC_056905_WW. Upstream PR #122 (anszom/rethink#122) documents a different mode/fan/swing map (auto=3 not 6; six fan steps; on/off vanes on 0x205/0x206), humidity as 0x336/10, and cassette-only wind-mode / autodry-select entities. Capture fixtures from that PR drive the shipped decoder.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This is the
ac_commonbase class that came out from the review on #101.CST_570004_WW(ceiling cassette, deviceType 401) is the only unit confirmed on realhardware — I own these, and every wire value was driven and read back.
Everything else — RAC, POT, WIN, and PAC — is existing code moved onto
ac_common. The tests pass, but none of it is verified on hardware; I have none of thoseunits. PAC's values come from @kkqq9320's captures in #105.
Corrections and "this does nothing on my unit" reports are very welcome, and I am happy to
rework or split this however you prefer.
Closes #105