At 10,000 feet

Setting: building an infrared codeset (SmartIR, ESPHome, any IR blaster) for a US-market Mitsubishi mini-split whose remote displays Fahrenheit.

Problem: the IR frame encodes Celsius, and the remote’s F-to-C conversion is an irregular lookup table, not the arithmetic formula - interpolated codesets emit wrong codes at several temperatures.

What this note establishes: the measured F-to-C table a US remote actually transmits over IR, with the jump points (67→68 F, 68→69 F, 87→88 F). The non-linearity was known in the wired CN105 serial projects, which end the table at 30.5 C for 88 F. The IR capture reads 31.0 C there. The mainstream IR libraries are Celsius-only.

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

Notes. Worked out against one Mitsubishi indoor unit (MSZ-GS09NA) with a Broadlink IR blaster. Other Mitsubishi models use related but not identical frames. No warranty, corrections welcome.

Prior art, and what is actually new

The quirk is known in the wired CN105 world: echavet/MitsubishiCN105ESPHome ships a fahrenheit_compatibility lookup option. SwiCago/HeatPump and mitsubishi2mqtt discuss it - credit for the phenomenon belongs there. Their tables give 30.5 C for 88 F where the IR capture reads 31.0 C. The mainstream IR libraries (IRremoteESP8266, arduino-heatpumpir) are Celsius only. The IR / byte-7 encoding of the mapping below was measured independently here.

The measured Fahrenheit-to-Celsius mapping. Do not interpolate.

Byte 7 encodes Celsius: low nibble = (C - 16), bit 0x10 = +0.5 C (21.5 C -> 0x15, 31.0 C -> 0x0F). That decode is linear. The F-to-C step is not - mostly +0.5 C per 1 F, with full-degree jumps (bold rows) at 67->68 F, 68->69 F, and 87->88 F:

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

27 steps from 61 to 88 F, three of them full-degree - ending at 31.0 C for 88 F where a straight 0.5 C per F line gives 29.5 C and the F-to-C formula 31.1 C.

SmartIR’s pending unit-conversion PR (#1502, open since 2025-11-30) converts with the standard formula through Home Assistant’s converter. Rounded to the nearest 0.5 C, the formula disagrees with this table at 14 of 28 rows, by 0.5 C each. A Fahrenheit-keyed device file, which that PR accepts via "temperatureUnit": "°F", sidesteps the conversion.

Integrity caveat: the table is reconstructed from the encoding rule plus the captured jump points, not a raw dump of every press. Verify against real captures. Never fill gaps by interpolation.

Background: the frame format (prior art: IRremoteESP8266)

Fully documented in IRremoteESP8266 ir_Mitsubishi.cpp (mirrored in SmartIR, arduino-heatpumpir, SwiCago/HeatPump, ESPHome) - context, not a discovery. An 18-byte frame (144 bits), sent twice per press, LSB-first. Header ~3400 µs mark + ~1700 µs space.

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 captured state
17 Checksum

Checksum: byte17 = sum(bytes 0..16) & 0xFF.

Mode (byte 6) with companion byte 8:

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

(Fan-only reads 0x38 in byte 6. Codes for it were not built.)

Fan and vane share byte 9, 0x40 | fan | (vane << 3): fan auto 0, low 1, medium 2, high 3, super-high 4, quiet 5. Vane auto 0, positions 1-5, swing 7. Quirk: at fan-auto/vane-auto the physical remote alternates byte 9 between 0x40- and 0x80-base forms (both accepted), so decoders must fold them. The generated codeset emits 0x40.

Background: capture a few presses, synthesize the rest

The fields are orthogonal (byte 8 tracks byte 6), so ~55 captured presses generated 4704 commands (28 temperatures x 4 modes x 6 fans x 7 vanes) plus off: one captured blaster packet is the byte-level timing template, only the 288 data-bit positions (2 frames x 144 bits) are substituted, and every generated packet must decode back to its intended frame before shipping.

References and upstream publication

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.