Mitsubishi IR remote °F to °C mapping is a lookup table, not a formula
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
- Mapping table published upstream: https://github.com/crankyoldgit/IRremoteESP8266/discussions/2278. A Fahrenheit-keyed SmartIR codeset was also drafted
- IRremoteESP8266
ir_Mitsubishi.cpp- frame-format prior art (Celsius-only) - MitsubishiCN105ESPHome, SwiCago/HeatPump, mitsubishi2mqtt - wired CN105 prior art for the F<->C lookup (mitsubishi2MQTT PR #272, merged 2024-08-25)
- AnalysIR (2015): https://www.analysir.com/blog/2015/01/06/reverse-engineering-mitsubishi-ac-infrared-protocol/
- Broadlink IR packet format (type
0x26, ~30.5 µs pulse units)
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.