Personal notes. Worked out against one Mitsubishi indoor unit (MSZ-GS09NA) using a Broadlink IR blaster to capture and replay. Other Mitsubishi models use related but not identical frames, and some fields below were confirmed only for the states this remote can reach. The key takeaway is the temperature-mapping gotcha up top. No warranty, your mileage may vary, corrections welcome.

At 10,000 feet

Setting: building an infrared codeset to control a US-market Mitsubishi mini-split heat pump (the kind of code table used by SmartIR, ESPHome, or any IR-blaster setup) for a remote that displays Fahrenheit.

Problem: the IR frame encodes temperature in Celsius, so a Fahrenheit-keyed codeset needs an F-to-C conversion, and the remote’s actual conversion is a lookup table with irregular full-degree jumps, not the arithmetic formula. Any codeset built by interpolating emits wrong codes at several temperatures.

What this note establishes: the measured F-to-C table a US remote actually transmits over IR, including the exact jump points (67→68 F, 68→69 F, 87→88 F). The non-linearity itself was already known in the wired CN105 serial ecosystem; the IR-side Fahrenheit mapping was not published anywhere I could find. The full 18-byte frame format is included as background.

Takeaway: build Fahrenheit codesets from this table (or your own captures), never from a linear fit.

The gotcha: the Fahrenheit-to-Celsius map is NON-LINEAR. Do not interpolate.

If you build a Mitsubishi codeset keyed in Fahrenheit and assume a formula for the Celsius the remote transmits, you will silently corrupt it. The remote’s Fahrenheit display does not map linearly onto Celsius: it mostly steps by 0.5 C per 1 F, but jumps a full 1.0 C at a few irregular points. Fit a straight line through the endpoints and interpolate, and you emit the wrong code at those temperatures (and anywhere the accumulated offset has since shifted).

Prior art, and what is actually new here

The non-linear F<->C phenomenon itself is not new: it is already known and implemented in the wired Mitsubishi CN105 serial ecosystem. echavet/MitsubishiCN105ESPHome exposes a fahrenheit_compatibility option (“standard” / “alt” lookup tables) and states outright that Mitsubishi uses a custom F->C lookup that does not match the plain arithmetic in all cases; the same quirk is discussed in SwiCago/HeatPump and mitsubishi2mqtt. Credit for the phenomenon belongs there.

What was not published, as far as I can find, is the same lookup on the IR / byte-7 path for a US Fahrenheit remote. The mainstream IR libraries (IRremoteESP8266, arduino-heatpumpir) model this protocol in Celsius only, so the Fahrenheit lookup currently lives only in the wired CN105 world, which is a different transport. This note contributes the measured mapping on the IR side: which Celsius value (and therefore which byte-7 code) a US remote actually transmits per degree Fahrenheit, including the specific full-degree jump points.

The measured mapping

The temperature byte (byte 7) encodes Celsius: low nibble = (degrees C - 16), and bit 0x10 = +0.5 C. So the decode is C = 16 + (byte7 & 0x0F) + (0.5 if byte7 & 0x10 else 0). That part is linear and clean; it is the F-to-C step that is not.

On this unit the full-degree jumps are at 67->68 F, 68->69 F, and 87->88 F. The Celsius column below is what the unit actually targets and is the reference; the three bold rows are where the step is a full degree. This table is reconstructed from the encoding rule plus the enumerated irregular jumps, not a linear fit:

F C   F C
61 16.0   75 24.0
62 16.5   76 24.5
63 17.0   77 25.0
64 17.5   78 25.5
65 18.0   79 26.0
66 18.5   80 26.5
67 19.0   81 27.0
68 20.0   82 27.5
69 21.0   83 28.0
70 21.5   84 28.5
71 22.0   85 29.0
72 22.5   86 29.5
73 23.0   87 30.0
74 23.5   88 31.0

Sanity check on the shape: 27 steps from 61 to 88, mostly +0.5 C, with three promoted to +1.0 C, which is exactly what lands you on 31.0 C at 88 F instead of the 29.5 C a naive all-half-step line would give. That gap is the whole point.

Once you have the Celsius value, byte 7 follows the rule directly (for example 21.5 C -> (21-16) | 0x10 = 0x15; 31.0 C -> 31-16 = 0x0F).

Integrity caveat

This table is reconstructed from the rule and the captured jump points, not a raw dump of every button press. Verify it against real captures before trusting it, and never interpolate to fill gaps. Every value that ships should trace back to a real capture (or the documented per-value rule), never to a straight-line fit.

Background: the frame format (already public, IRremoteESP8266 is prior art)

The 18-byte Mitsubishi “Kudo”-family frame is already fully documented and implemented in public: IRremoteESP8266 ir_Mitsubishi.cpp, mirrored across SmartIR, arduino-heatpumpir, SwiCago/HeatPump, and ESPHome. The compact reference below is reformatting of that prior art for context, not a discovery; the temperature mapping above is the part those sources (being Celsius-only on the IR side) do not carry.

Mitsubishi’s IR command is an 18-byte frame (144 bits), transmitted twice per button press, LSB-first. The leading header is a mark of roughly 3400 microseconds followed by a space of roughly 1700 microseconds, then the 144 pulse-pairs, then a gap, then the identical frame again.

Byte Meaning
0-4 Fixed preamble: 23 CB 26 01 00
5 Power: 0x20 = on, 0x00 = off
6 Mode (see below)
7 Temperature (the non-linear one, above)
8 Mode companion byte (paired with byte 6)
9 Fan (bits 0-2) + vane (bits 3-5), on a base of 0x40
10-16 Zero in every state I captured
17 Checksum

Checksum is byte17 = sum(bytes 0..16) & 0xFF: a running 8-bit sum of the first 17 bytes, truncated to one byte. No CRC, no scrambling.

Mode lives in byte 6 with a companion value in byte 8 that moves with it:

Mode Byte 6 Byte 8
heat 0x08 0x30
dry 0x10 0x32
cool 0x18 0x36
auto 0x20 0x36

(A dedicated fan-only mode reads 0x38 in byte 6 on this unit; codes for it were not built.)

Fan and vane share byte 9, 0x40 | fan | (vane << 3):

  • Fan (bits 0-2): auto 0, low 1, medium 2, high 3, super-high 4, quiet 5.
  • Vane (bits 3-5): auto 0, positions 1-5 = 1-5, swing 7.

One quirk relevant to receiving and decoding the physical remote: for the fan-auto / vane-auto state the remote alternates byte 9 between a 0x40-base and a 0x80-base form, and the unit accepts both. Frame matching has to fold both forms together. The generated codeset emits the 0x40 form consistently.

Background: capture-a-few, synthesize-the-rest (standard technique)

Also standard practice, kept brief. The fields are orthogonal (power, mode, temperature, fan, and vane each live in their own byte, aside from byte 8 tracking byte 6 by fixed lookup), so capturing the full combinatorial product is unnecessary: capture each field’s values once and combine in software.

From roughly 55 captured remote presses (a temperature ladder across the full range, one cycle through the fan settings, one cycle through the vane settings, and the mode/power presses), 4704 commands were generated (28 temperatures x 4 modes x 6 fans x 7 vanes) plus off. To keep timing bit-exact, take one real captured blaster packet as a byte-level template (its headers, inter-frame gaps, and pulse timings are known-good hardware output) and substitute only the 288 data-bit positions (2 frames x 144 bits) with the freshly-built 18-byte frame. Then round-trip every generated packet back through the same decoder that parsed the live captures, and assert the decode matches the intended frame. Nothing ships unless it decodes back to itself.

Published upstream

The Fahrenheit-to-Celsius mapping table for this unit is posted to the IRremoteESP8266 project’s discussions for searchability: https://github.com/crankyoldgit/IRremoteESP8266/discussions/2278. A SmartIR codeset contribution (Fahrenheit-keyed) was also drafted.

References

  • IRremoteESP8266 ir_Mitsubishi.cpp (Mitsubishi AC IR protocol: frame layout, checksum, Celsius-only temperature handling) - prior art for the frame format
  • echavet/MitsubishiCN105ESPHome (fahrenheit_compatibility option: prior art for the non-linear F<->C lookup, on the wired CN105 serial transport)
  • SwiCago/HeatPump and mitsubishi2mqtt (wired CN105 ecosystem, same F<->C quirk)
  • AnalysIR, “Reverse engineering the Mitsubishi AC infrared protocol” (2015): https://www.analysir.com/blog/2015/01/06/reverse-engineering-mitsubishi-ac-infrared-protocol/
  • SmartIR codeset JSON schema (Home Assistant custom component)
  • Broadlink IR packet format (type 0x26, pulse units of ~30.5 microseconds)

Licensed under the site footer’s CC BY 4.0. If this saved you a weekend of button mashing, the optional thanks link in the footer is appreciated, no obligation.