Reverse engineering the YK-H/531E AC remote control IR protocol
My air conditioning unit uses the YK-H/531E remote controller. My unit is an Electrolux, but this remote is evidently an OEM part used by many other brands and units. I wanted to control my AC with Home Assistant, which meant getting a so-called IR blaster that can both receive and send IR signals, which let me reverse engineer the remote controller’s IR protocol in order to create IR signals myself that could control the AC. In this post I’ll describe what I found about the protocol and how it (mostly) works.
In the next post I’ll describe how I use it with Home Assistant.
Receiving IR messages and decoding them
To help with the reverse engineering process, I wrote a script that receives a stream of IR messages from the IR blaster and prints them out, which can be redirected into a file. This script is very simple and designed only for my IR blaster, so you might have to adapt it for your environment if you want to use it.
For the actual decoding, I used a script that can read multiple received IR messages from a file and decode them all. It displays each decoded message as a binary string with helpful bit location guides, and for multiple messages it displays where any of them have a different bit set than any other message for that bit location. Finally it decodes all the AC settings from the messages and displays them. It should be able to catch most forms of corrupted messages, such as messages being too short, the raw IR signal timings being off or some setting not decoding properly.
IR blaster
The IR blaster I ended up using is the Moes UFO-R11. It is a rebrand of the similar Tuya model, except it is entirely battery powered with two AA-batteries instead of requiring external power input like the Tuya model. It is well supported in Zigbee2MQTT, which is what I use as a Zigbee gateway. It can be put into a “learning” mode, where it listens for an IR signal, and returns it in an encoded form. It can also transmit IR signals given in this same encoded form.
I bought mine from AliExpress, its list price hovers around 25-35€ but I got mine for a whopping 5,22€ with free shipping thanks to some sale and those coupons AliExpress loves handing out.
Tuya’s IR signal encoding
The IR blaster returns the IR signals in an encoded form as such:
BwAjnhEyAo4GgAPgBwHAF+ADB0AB4AcP4AkBAWUC4Bkj4BUBQD9AAcAH4IcB4EeXwE9AB0ADCzICMgKOBjICjgYyAg==
This is obviously base64-encoded data but how do we get the actual IR signal out of it? Luckily someone else has already done the work for us, explaining how the base64 decodes into a tinyLZ-compressed sequence of 16-bit little-endian integers that represent the “raw” IR signal. They even provided a Python script to decode and encode your own “Tuya”-signals, for which I’m eternally grateful because I don’t think I could’ve ever figured it out myself.
IR signal
Once base64-decoded and tinyLZ-decompressed, the resulting list of integers looks like:
8973, 4505, 561, 1685, 561, 1685, 561, 561, 561, 561, 561, 561, 561, 561, 561, 1685, 561, 1685, ...
For this particular protocol, there should be exactly 211 numbers in the list. As the gist earlier also explains, these numbers are the “raw” IR signal. The numbers correspond to durations for how long, in microseconds, the IR signal was first high, and how long low afterwards. The signal starts by being high, then alternating between low and high. Since there’s an odd number of numbers in the list, the last duration in the list is a high signal, which acts as a kind of footer to end the signal without encoding any data.
The first two durations, 8973
and 4505
are a kind of preamble to notify the target unit that a signal is about to be sent, they don’t contain any data. Afterwards the durations should be split into adjacent pairs (skipping the last footer):
561, 1685
561, 1685
561, 561
561, 561
561, 561
561, 561
561, 1685
561, 1685
...
These durations are either “short”, around 600µs for this protocol, or “long”, around 1700µs. Each of these pairs encode a single bit, such that the first duration is always “short”, then followed by either another “short” or a “long”. A short-short pair encodes the bit 0
and a short-long pair encodes the bit 1
. Decoding these bits least-significant-bit first creates the actual control message we’re interested in. This is the decoded message displayed most-significant-bit first, with bit indices displayed above:
100 96 92 88 84 80 76 72 68 64 60 56 52 48 44 40 36 32 28 24 20 16 12 8 4 0
1101_1010_0000_0000_0000_0000_0010_0000_0000_0000_0000_0000_0000_0000_0000_0000_1010_0000_0000_0000_1110_0000_0111_0111_1100_0011
AC control protocol
From here on out reverse engineering the control protocol was fairly straight-forward. I already knew that this one message contains all the parameters for the AC; the remote control has a display for the current settings, and by including all settings in the message it prevents the AC unit desynchronising from the remote. The remote control is an OEM product, so a lot of different AC units also use this same protocol. I was able to find someone else’s guide on having reverse engineered a similar remote, which meant I largely knew what I was looking for in the message.
The message encodes the following settings:
Start bit | Length | Setting | Example |
---|---|---|---|
0 | 8 bits | Preamble | 1100 0011 |
8 | 3 bits | Swing | 111 |
11 | 5 bits | Temperature (C) | 01101 |
21 | 3 bits | Unknown | 111 |
32 | 2 bits | Timer pt. 1 | 01 |
37 | 3 bits | Fan speed | 101 |
41 | 4 bits | Timer pt. 2 | 1111 |
49 | 1 bit | Celsius/Fahrenheit | 0 |
50 | 1 bit | Sleep | 0 |
53 | 3 bits | AC mode | 000 |
77 | 1 bit | On/off | 1 |
81 | 7 bits | Temperature (F) | 100 1101 |
88 | 3 bits | Button | 001 |
92 | 1 bit | Unknown | 0 |
96 | 8 bits | Checksum | 1101 1010 |
Let’s go through all the different settings.
Preamble
The first byte is a preamble, in this unit’s case 1100 0011
. It doesn’t contain any actual information, it’s likely just an identifier for this particular AC unit.
Swing
Bits 8 to 10 are swing; the AC unit has a flap that can be set to “swing”, i.e. oscillate up and down. In this unit, 111
means swing off, 000
means swing on. Other AC units might have multiple swing options, hence there being more than two possible options.
Target temperature (Celsius)
Bits 11 to 15 is the set target temperature in Celsius. The AC unit has a range of set target temperatures from 16C to 32C, however the encoded value here is 8C to 24C, so offset by -8. I’m not entirely sure why its offset, but my guess is to save one bit in the value - the upper end of the range, 32C, would require six bits. Note the AC’s target temperature can also be set in weird units Fahrenheit, which is encoded later in the message.
Timer
The AC has a timer functionality that lets you set a timeout after of which it’ll turn on automatically. I didn’t bother reverse engineering this one, since I’ve never used it and would now achieve the same thing with Home Assistant. It probably works the same as in the article I previously linked.
Fan speed
Bits 37 to 39 set the AC’s fan’s speed. There are four possible options for this unit:
Bit pattern | Fan speed |
---|---|
001 1 |
High |
010 2 |
Mid |
011 3 |
Low |
101 5 |
Auto |
Note that not all options are avaible depending on the AC’s mode.
Celsius/Fahrenheit
The AC’s set target temperature can be set in either Celsius or Fahrenheit. If the 49th bit is 0
, the target temperature is in Celsius and 1
if it is in Fahrenheit. Note that both units of temperature can be encoded in the message at once; this bit decides which one is used.
Sleep
The AC has a “sleep”-mode, which is a boolean toggle on the 50th bit; 0
if off, 1
if on.
AC mode
Bits 53 to 56 is the AC’s mode, which is one of four options:
Bit pattern | Mode | Notes |
---|---|---|
000 0 |
Auto | Fan speed can only be set to Auto |
001 1 |
Cool | |
010 2 |
Dry | Fan speed can only be set to Low |
110 6 |
Fan | a) Fan speed can not be Auto b) The set target temperature is 0 |
On/off
The 77th bit is the AC’s on/off state, 0
if off, 1
if on.
Target temperature (Fahrenheit)
Bits 81 to 87 is the set target temperature in Fahrenheit. The range for Fahrenheit is 60F to 90F, but oddly the range in the protocol is 68F to 98F, so offset by +8. This is similar to the -8 offset in the Celsius temperature field, however this doesn’t save any bits in the message. I’m not sure at all why this offset is there, but I don’t use the weird units myself anyway so.
Button
Bits 88 to 91 tells which button was pressed on the remote to generate this message. To be honest, I have no clue why the protocol would require this information. This article I found has a somewhat similar situation, except it’s only for the power button. This unit’s protocol already includes all the necessary information in the payload, so maybe the button is just a leftover thing for some other kinds of units? Anyway, since it’s there, I have to deal with it.
Bit pattern | Button |
---|---|
0000 0 |
+ (temperature up) |
0001 1 |
- (temperature down) |
0010 2 |
Swing |
0100 4 |
Fan speed |
0101 5 |
On/off |
0110 6 |
Mode |
0111 7 |
Unit (C/F) Holding down + and - for 3 seconds changes the temperature unit between C and F |
1011 11 |
Sleep |
1101 13 |
Timer |
Checksum
The last byte in the message is a simple checksum of the message’s payload; it is the sum of every byte in the payload, truncated to one byte. It seems this same method is used in many other IR remotes as well.
Mystery bits
There are still a couple bits in the message I don’t know what they mean, but the unit doesn’t seem to mind what they are so I’m not bothered about them.
Bits 21 to 23 seem to always be set as all 1
. I’ve never seem them be anything else, so I’ve included them in my own encoded messages later.
Bit 92 seems to flip between 0
and 1
, no idea why and when exactly. I’ve always set it to 0
, the unit doesn’t seem to mind. It’s right next to the button bits, maybe something to do with it?
Encoding my own control messages
Now that I understand how the protocol works, it’s simple to encode my own messages. I wrote an encoder script that takes all the different settings as input, generates the 104-bit control message, converts it into signal timings for IR and feeds it to the Tuya IR encoder program to finally get a base64-string out that can be fed into the IR blaster to send out.
For example, taking these settings:
Setting | Value |
---|---|
On/off | On |
Target temperature | 20C |
Mode | Cool |
Fan speed | High |
Swing | No |
Sleep | No |
Button | On/off |
Would produce the following 104-bit message (again, most-significant-bit first):
100 96 92 88 84 80 76 72 68 64 60 56 52 48 44 40 36 32 28 24 20 16 12 8 4 0
0110_1111_0000_0101_0000_0000_0010_0000_0000_0000_0000_0000_0010_0000_0000_0000_0010_0000_0000_0000_1110_0000_0110_0111_1100_0011
Converting this message into the short-short/short-long duration scheme I explained earlier produces the following list of IR signal durations:
9100, 4500, 600, 1700, 600, 1700, 600, 600, 600, 600, 600, 600, 600, 600, 600, 1700, 600, 1700, ..., 600
You’ll note I used approximate values for each duration, including the first two values for the preamble. The AC unit seems to allow quite a lot of leeway on the exact durations for the values, and it could be the IR blaster can’t exactly reproduce these durations anyway. Note to include the single short-value as a footer at the very end to finalise the last bit in the message.
Encoding this list as a Tuya-message results in:
B4wjlBFYAqQGgAPgBwHAF+ADA+AHG+APAeALM+AjAeAvN+A/P+BDn8E34Q8v4Qtj4QM7
This message is a lot shorter than the ones from the remote, since the uniformity of each duration value means the list compresses very well (the encoding uses tinyLZ level 2 by default).
Sending this message to the IR blaster with the AC unit nearby, after a short delay the AC beeps and turns on with these settings! In the next post I’ll describe how I integrated this work into Home Assistant.