Full Telemetry for Midea-based Mini-Splits & Centrally Ducted Units

@fmckee

How are you decoding 0x03[4] and 0x03[5] to get those kinds of values? I’m interested in these bytes because mine look different and so far all your decoding is the same for every Testport byte even with the NTC conversion I now use on S1S2. You decode those 2 bytes then you might be decoding 2 of my S1S2 bytes.

Have you tried RAW NTC? Maybe separate byte conversions or maybe some form of high / low bytes. See if something converts to make them fit between 114 and 179.

The last chart I posted contains the raw values for 0x03[4] and 0x03[5] - no conversion applied.

Can you elaborate what you mean when you say RAW NTC?

I’ve collected 7 days worth of raw data (all message types, all bytes). I will pack it up and see if Claude comes up with a plausible explanation for both bytes.

@fmckee

RAW NTC, K Ohms but I wouldn’t go down that rabbit hole. IPM temp 0x01[4] still bothers me, and I’m pretty sure it isn’t a true IPM temp. It acts more like a fault state byte.

This is assuming this byte is the same for both of our Testports. In that screenshot you posted, your byte was sitting between 179 and 188. Over the summer, my byte hit a max of 196 and quickly went down, but never threw a code. If you look at the IDU coil fault charts in “our wonderful manual”, 200 seems to be the fault threshold.

So it probably breaks down like this:

179 - 184: Hold

185 - 199: Protection threshold

200+: Fault tripped <-- This tells us what this byte actually is

The manual shows the IDU coil faults at 60C (NTC 200) and the IPM faults at 90C. It makes perfect sense they’d use the exact same byte-scaling formula for similar thermal protections sensors.

Maybe using the Dr Smart Tool to override the IPM temp to above my assumed threshold would makes since to prove. I don’t know how with my ESP but if you know I can test on my unit?

@kydonian

Per these images it looks like your testport that might be CN24 and its looks usable! Second image top right red port.


board # 17122000047742
Referenced in this manual pages 18 - 20 even though it might not be yours specific, same board numbers: https://comfortstarusa.com/wp-content/uploads/2022/08/CLA36CD-Service-Manual-compressed.pdf

Just make sure you know exactly what you are dealing with so you don’t blow something up. You ever get a chance to look at this yet?

Going forward, I think I won’t expose 0x01[4] as IPM temp any more. I agree that it looks more like a status byte.

Can you shoot me a link to the manual you are talking about?

My inverter tester can only set the following parameter:

  • Compressor frequency
  • Outdoor fan Speed
  • EEV opening steps
  • 4-way valve position

Mine neither supports setting nor display of the IPM temperature. Looks like there are multiple revisions of the inverter tester in circulation. Some of them seem to support IPM temperature (as you can see on the piece of the manual that NyxVale63 posted earlier in the thread).

@fmckee This is what my unit is considered from the websites manuals. Senville Support - Customer Service R454B service manual. If you look on page 65 of the unlabeled pages you will find “3.4.2 Evaporator Coil Temperature Protection” This is what I’m referencing and specifically the “PTC ON”? The chart below the other chart in that section…

Please do not confuse this with the page before, “page 64” where you also have “3.3.3 Evaporator Temperature Protection”. The “3.3 Cooling Mode” and “3.4 Heating Mode(Heat Pump Units)” headers obviously settles what those actually are, but that doesn’t matter with the decoding. Those charts are how they determine protection limits, if its exposed to us or not.

In my manual we have 2 versions of the IDU coil temp protection. Look at that, your pages are properly numbered!!! https://support.mrcool.com/hc/en-us/articles/35173218846356-DIY-12k-5th-Gen-Condenser-115v, Page 31, easy to find. Your chart is the chart I was not using. Maybe my thresholds are a little off.

For the IPM temp. I have that just documented. Its been my best value number based on these manuals but nothing I can point directly to. Makes way more since than the current decoding of max 140F unless this byte means something else.

Those other 2 bytes. What do those other bytes sound like to you? and what does your 4-way valve position look like?

Amazing, I’ve never seen such a detailed service manual for the DIY 12k condenser before. I still have no clue what 0x01[4], 0x03[4] or 0x03[5] mean.

I’m wondering if one of those is the Frequency limit symbol (MRCOOL service manual, page 38).

I actually have a capture where the evaporator coil protection kicked in:

0x02[4] carries error codes. Bit 7 triggers error code L0 (Frequency limit caused by High or low evaporator temperature).

That turns 0x04[6] into a candidate for frequency limit symbol:

16 = 00010000 → this maps to “Frequency limit caused by low temperature of T2” in the Frequency limit symbol table from the service manual.

I have not looked into how the 4-way valve is mapped yet.

Pushed a couple of updates:

  • IPM Temperature is no longer exposed as sensor
  • Documentation and Grafana dashboard now annotate sensors with the Midea codes (T1 / T2 / …)

Next on my TODO list:

  • Provide all raw response bytes as optional entities to Home Assistant (the JSON endpoint already exposes all raw bytes to the Grafana dashboard)
  • Map and expose error code bytes as entitites

LOL That is the chart on my page 64, your page 29 bottom right but basically the same decoding. I see the temp went to 37.4F which is 3C. On the charts on 4C - 0C “Decrease: Decreases the running frequency to lower level per 1 minute”! That’s pretty clear to me. 0x02[4] and 0x04[6] makes since!!! Why do you think your compressor Hz went up?

Just a observation from my unit. The lowest temp my indoor coils have gotten is 0x9 during a defrost cycle in the winter. NTC(9) = -29C = -21F. That’s freaking cold. I always assumed during maintenance cycles these thresholds and limits can be ignored or maybe the limits are adjusted.

What is 0x05[5]? Have you ever seen values change on 0x05[3]?

I have no idea. I see some action on those bytes for 2 of my units:



It’s hard to see (because the charts are not perfectly aligned due to the varying width of the vertical legend). The compressor frequency went down after the low temperature protection kicked in:

@MidATRIX Unrelated - for the compatibility section in my README, would you mind sharing the type of your unit that is compatible to the test port dongle?

3TON / 36000 BTU Central Air Conditioner Heat Pump System
Whole system model number: SENDC-36-HF


My assumption on 0x05 (and it might overflow to your 0x06 that I don’t have). Both of our units 0x05[2] is the byte that adds the extra details to 0x02[3]. I assume the others are some kind of indoor load request per zone by the indoor unit/units. The thing with that is you would see these load request on your Reverse Engineering Midea’s LNS Communication Protocol.

On my S1S2 project, which is single zone I see this communication and it always starts with the indoor unit requesting load changes. On my Testport I see this indoor unit request on 0x05[3]. That byte and the 0x05[2] are the only 2 bytes that change on my 0x05 frame and I see both of those on S1S2. I do also have an additional byte on my S1S2 project that I do not see on the Testport. Its a byte that the outdoor unit echos back to the indoor that says what the load its actually go to with overrides.

Just some charts to see how confusing this is with a single zone unit.

Red = 0x05[2]
Light Blue = 0x02[3]

These create the detailed version of Hz. 0x02[3] + 0x05[2] / 100 These bytes live next to each other on S1S2 and the decoding just looks good. Maybe these bytes can represent something else.


Yellow = 0x05[3] Single zone indoor unit load request,
Blue = Not on my Testport. Single zone indoor unit load request override by outdoor unit.

That’s a headache just to look at when you realize it makes load request steps. I think all of those extra bytes in you 0x05 frame somehow determine the overall load requirement for the heatpump if its single zone or multi-zoned. That would be the yellow line above. There’s way more to it than that like when the yellow line is 0, 40 and 20 but at 2 different steps. 0x01[8] kinda helps to explain that though.



MRCOOL service manual, page 38 Number of presses 15.

Check out what Bit4 represents. Potentially a full mapping of your threshold range reached byte and a hopefully fault reached byte.

Third unit reporting in, and it is a multi-head, so most of this is corroboration for @MidATRIX plus one conflict and one offer.

Unit: ACiQ ES-48Z-M6C, five heads on a 6-zone-capable ODU, R-454B, manufactured 05/2025. I read it through a custom CN45 adapter using a vendored fork of the component pinned at 9302edc7. It has been running pretty much continuously since August. (@fmckee, if any of this is useful for the compatibility table, tell me what detail you want and I will pull it.)

The tap, before I tidied it up. Little grey module on the lid is the CN45 adapter, and the black cable drops through the lid grommet into the control compartment. The two-digit display on the board at left is DSP1, which is what I anchored the current and DC bus formulas against further down. Head terminals are the numbered blocks on the right. Disconnect was pulled for this, which is the only time that panel should be open.

Multi-head: four decoded fields are hard sentinels

Constant 0x00 across 807 rows of a capture spanning 31 to 71 Hz, anchored against the ODU’s own DSP1 display, and unchanged in the five weeks since:

  • 0x00[2] T1 indoor ambient, 0x00
  • 0x00[3] T2 indoor coil, 0x00
  • 0x00[7..8] outdoor fan speed (uint16), 0x0000
  • 0x01[7] TT indoor setpoint, 0x00

Three of those make sense. There is no single indoor unit to report. The fan-speed one really surprised me, since the fan is plainly running… on this unit the fan state seems to live in the DSP1 index instead (code 48, a step code 0 to 7) rather than as an RPM word on the wire.

Everything else in FRAME-BYTES.md decodes correctly here. So the protocol is not different on multi-head. Four fields just have no referent.

0x01[4] really does look like IPM temperature

I saw this got pulled after @MidATRIX raised that 0x01[4], 0x03[4] and 0x03[5] might be thermal thresholds rather than readings. On this unit 0x01[4] through the NTC beta model behaves like a heatsink, not a constant:

  • range 33.5 to 50 C over the last several days
  • sits consistently 9 to 19 C above outdoor ambient
  • rises with compressor load and falls with it, with a visible lag

A threshold would not move with load like that. I have no independent reference for it though, since the DSP1 index on this unit does not expose an IPM temperature, so it is behavioural evidence and not proof. If either of you has a frame where 0x01[4] sits flat while ambient swings, that kills it.

Conflict: 0x02[8] reads 0x02 while this unit is COOLING

FRAME-BYTES.md has 0x02[8] as operating mode, with 2 = HEAT. This ODU emits b8 = 0x02 continuously while every head is set to cool, the reversing valve is in cooling, and it is making cold air. By the table that is HEAT.

Mode and fan step are degenerate in everything I have (both would read 2), so I cannot separate them without a heat-mode test, which I have not run. I am flagging it mainly so nobody trusts a mode reading off a multi-head unit without checking it against something physical.

L0 (0x02[4] bit 7): confirmed here, with an indoor coil measurement

Your L0 identification matches what I see, and it named a byte I had been calling an unknown dormant flag for two weeks. Thank you for that.

I have nine fully characterised episodes now, 2026-09-15 through 09-17, every one of them slot2:b4 = 0x80 with slot 2 reading 0x5502100F800000000208 at onset:

onset (CDT)        duration     preceding 10 min, Hz
09-15 09:10:23      21.2 min    15 to 51
09-15 11:18:13      27.5 min    15 to 51
09-16 04:07:24      26.7 min    flat 15
09-16 05:04:04     113.7 min    flat 15
09-16 08:37:24       5.7 min    flat 15
09-16 10:31:54       1.0 min    0 to 30
09-16 10:45:34       6.2 min    15 to 16
09-17 05:59:24       3.8 min    flat 15
09-17 06:43:45      34.5 min    15 to 51

Here is the part I think is really worth your time, with the caveat that it splits two ways and the split matters more than the average.

Per episode, indoor coil inlet on the living-room head, against EEV position:

ep  onset (CDT)        coil med   coil min   EEV med
1   09-15 09:10:23        44.0       39.0       270   cold
3   09-16 04:07:24        41.0       40.0       246   cold
4   09-16 05:04:04        40.0       39.0       256   cold
8   09-17 05:59:24        40.0       40.0       258   cold
9   09-17 06:43:45        43.0       39.0       288   cold
2   09-15 11:18:13        63.0       61.0         -   warm
6   09-16 10:31:54        67.0       66.0         -   warm
5, 7                       no coil samples in window

Five of the nine sit at 40 to 44 F with the EEV wide open, which is the shape your L0 identification predicts. Two sit at 63 to 67 F, nowhere near a freeze threshold, and those two also have no EEV samples in window. I read that as the living-room head being idle, so for those episodes the sensor is not watching the evaporator that matters. My coil is on one head out of five.

Aggregated across all nine it comes out as roughly six degrees colder on the means and eight on the medians against unflagged time at the same compressor frequency, time-weighted. I would not lean on that number. An average over a cold cluster and a warm cluster describes neither, and I only quote it because somebody will ask.

What I would lean on is the cold cluster: five episodes, coil 40 to 44 F, EEV between 246 and 288 steps, compressor pinned at its floor. That is a coherent picture and it is yours, not mine.

Two things argue against reading it as a hard freeze limit. The coil touched 38 F once with the flag clear (09-17 02:39, a single sample), so whatever the threshold is, it is not simply “colder than 39”. And I do not have a sourced figure for the protection setpoint on this unit. I have seen 4 C for 250 s quoted, but I have not read it in a manual for this model, so treat it as hearsay until somebody produces the page.

What I got wrong on the way here, in case it saves you the detour

My first read was that L0 trails a demand excursion. The compressor swings hard, settles, and the flag lands. That is wrong, and four of the nine episodes killed it: the preceding ten minutes were completely flat at 15 Hz, no excursion at all.

Then I over-corrected and decided the flag must be causing the frequency floor. Also wrong, and worse, because it was a nice story. The floor is already running when the flag asserts, by 118 minutes in one case and 78 in another. A cause cannot lag its effect by two hours. On top of that, 81.6% of the time the plant spends at its minimum frequency is never flagged at all, and the longest unflagged stretch at the floor (119.7 minutes) is longer than the longest flagged episode.

So the honest version is that L0 and the frequency floor co-occur, and the thing that actually separates a flagged floor from an unflagged one is the indoor coil temperature. Which is what your byte map said in the first place.

Durations still vary 114 times over, from 1.0 minutes to 113.7, out of what looks like the same stimulus. I do not have a good story for that one yet.

One confirmation from the unit’s own display

I had DSP1 and a clamp CT on the branch circuit during the August capture, so these are independent of the formula rather than a restatement of it:

  • 0x01[2] current. 0.117 * b + 0.92 agrees with DSP1 code 35 (ADC x 0.2 A). At b2 = 78, formula gives 10.05 A against 10.0 A displayed. Tracks the line CT at r = 0.997 across a 1638 to 4632 W ramp.
  • 0x03[6] DC bus. round(b * 59 / 32 - 1) reproduces exactly (marked as hypothesis in the doc).

Small naming thought on the first one. The measurement point looks inverter-side, not line-side. A line CT always reads higher, and that gap is not a decode error… current_draw invites exactly that comparison, which cost me an afternoon I would like back.

I can run the 0x05 and 0x04 probes on a third unit

@MidATRIX, you asked whether values ever change on 0x05[3], and @fmckee posted charts from two units and said he had no idea. I think I can answer that one, because a five-head unit gives you six things to move independently instead of one.

My firmware already has the type 0x04, 0x05 and 0x06 queries plus a set of parameterised probes, including a zone-selector hypothesis on 0x04 with b2 = 0x01..0x06 that I wrote before I read your posts. I just do not publish those slots yet.

If it is useful I can enable capture and post raw 0x04, 0x05 and 0x06 frames from a five-head ACiQ alongside per-zone setpoints and room temps. Third unit, different brand, different head count. Happy to drive specific zones to specific setpoints if you want a controlled read on 0x05[3].

I also still have the 807-row CSV (raw frames, decoded values, and branch-circuit watts and amps at 10 second resolution) if anyone wants it.

Amazing - thanks for the detailed feedback! I’ll dig into everything you shared over the weekend. Really interesting to get feedback from someone running a multi-head unit.

Awesome! Glad to see more interest in the Testport.

I still consider 0x01[4] IPM temp and @fmckee answered about 0x05[3] and added his other units which led me down the multi zone / multi head bytes. Glad to see you have more bytes that move with a 5 head unit. Do you know what those bytes are doing?

I would like to know what your 0x03[4] and 0x03[5] look like?

Let me know if you want to talk about anything specific!

MidATRIX… glad this is useful.

I have 28 hours of continuous capture now, 10,762 frames, so I can give you counts instead of impressions.

Quick note on indexing so we are not talking past each other. I count from the 0x55 sync byte, so [0] is 0x55 and [1] is the frame id. All my frames are 10 bytes. If you index the payload after the frame id, subtract 2 from everything below.

Your question: 0x03[4] and 0x03[5]

Both are flat zero. Across 4,687 frames of type 0x03, bytes [2] [3] [4] [5] [7] and [8] never moved off 0x00. Not once.

The only byte that moves in 0x03 is [6]… and if you are indexing from the payload, that is your [4], which would explain why you asked.

It is bimodal, which is the interesting part:

0xCD  1179 frames
0xCE  1167
0xB9   451
0xBA   370

Two clusters about 20 counts apart, nothing in between. I have not proven what it is. My unit is a 6 zone with 5 heads connected, so my first guess is something head related… but that is a guess and I have not tested it.

Checksum, in case it saves anybody time

byte[9] == (256 - (sum(bytes[0..8]) & 0xFF)) & 0xFF

10,762 out of 10,762 frames, all four types I capture. Two’s complement of the running sum. Pretty boring, but nice to have nailed down.

Three decodes I can actually prove

I have the ODU telemetry decoded separately in Home Assistant, so I correlated the decoded values against the raw bytes over the same window:

0x02[2] = compressor frequency target (Hz)     195/195 exact
0x02[3] = compressor frequency actual (Hz)     195/195 exact
0x01[5] + 0x01[6] = EEV opening steps         3282/3282 exact

That EEV one is worth a flag. It is 16 bit little endian, low byte in [5] and high byte in [6]. I nearly missed it… [5] on its own matched 74% of the time and I almost wrote it off as a noisy byte. Then I looked at the misses, and every single one was a case where the real value was over 255. [6] is only ever 0x00 or 0x01. That gave it away.

What moves, and what I cannot explain

0x00[4] [5] [6]   all move, unmapped
0x01[2] [3] [4]   all move, unmapped
0x02[2] [3]       frequency target and actual (above)
0x03[6]           bimodal, unmapped

I ran the unmapped movers against discharge temp, IPM temp, ambient, coil, DC bus voltage, input voltage and compressor current, with the usual linear transforms. Nothing got above 8%. That is noise.

I think I know why, and it is a limitation on my end. My tap only exposes frame types 0x00 through 0x03 as raw bytes. Home Assistant is decoding discharge, IPM, ambient and coil just fine, so the firmware is clearly reading frames I am not passing through. Your 0x05[3] is past my window entirely.

So I really cannot confirm or deny 0x01[4] as IPM temp. If IPM lives in 0x04 or later I would never see it in my raw slots, and my correlation would come back empty exactly like it did.

If you can tell me which frame type carries the temperatures on your unit, I will widen the capture to match. Then I can correlate directly against decoded values instead of guessing at transforms.

On the multi head bytes

Honestly? Less than it sounds like. I can tell you which bytes move on a 5 head unit and how often. I have not proven what any of them do.

The bimodal 0x03[6] is my best candidate, purely because two clean states with nothing in between smells like a count or a flag rather than a temperature.

Happy to run experiments if that helps. I can force individual heads on and off and capture the frames around each transition… that should settle the multi head question a lot faster than me staring at histograms.