Making the Olimpia Splendid UNICO Smart (part 1)

Making the Olimpia Splendid UNICO Air Conditioner Smart by Reverse engineering and emulating the Infra-Red Remote Codes

Making the Olimpia Splendid UNICO Smart (part 1)

The Olimpia Splendid 12 SF Airconditioning Unit is "dumb". In this post I look at the foundation steps to make it smarter by decoding, understanding and emulating the infrared control signals and state management.

Introduction

At home we have a number of Olimpia Splendid 12 SF "all in one" style air conditioning units. Despite being difficult to install as each requires two 202 mm holes core-drilled in the wall, they do a good job without all the pipework, power supply (often 3 phase) and gas requirements of a more conventional setup.

They are however not "smart" - they are controlled by an infra-red remote which manages all elements of the AC unit state.

Even the internal clock drifts badly!

Olimpia's "smart" solution

While you can buy a WiFi control board "Olimpia Splendid B1015 Smart Home Control" it is utterly pointless. After buying 3 from Amazon and installing them I discovered that the app was useless and nothing would work to the point that I still don't know exactly what it was supposed to do.

Subsequently having gone back and forth with Olimpia's support they opted to refund me for the purchase - without requesting the boards back, thankfully, as they are not easy to install or remove.

A retrofit solution

Despite this set-back I still want to make them smart, and as the IR remote controls them exclusively, I set out to replicate the signal it transmits so that I can then use an ESP32 or similar to manage the AC units in response to temperature, solar power, whether doors are open and so on.

The remote sends the whole current state in every pulse. The AC unit tracks time from that reference point and handles the built-in schedules, but any signal from the remote will reset all of this to whatever the remote's view of the world is, time or otherwise.

This post breaks down the IR coding with C++ samples for control.

I will cover the 'smarter' aspects in another post.

Decoding the Payload

The payload was decoded using the AC Remote to generate the IR signal, and an IR Receiver with a microcontroller (in this case an ESP32-S3) to decode and print the raw data.

The IRremote library is great for working with IR protocols, but unsurprisingly it does not include encoding or decoding code for all devices despite having support for many common brands.

Building on the sample ReceiveDump project I was able to get an idea of the timing and then adapt the main loop to print out the high and low bits.

A high bit appears to be a 400µs 38KHz burst followed by 1300µs gap, and low bit is a 400µs burst followed by a 1000µs gap.

The raw data can be rendered with the following code, with a bit of rounding to 'guess' the high and low bit gaps, as well as allowing for noisy bits:

auto decodedIRData = IrReceiver.decodedIRData;
uint8_t tRawlen = decodedIRData.rawDataPtr->rawlen;

for (int i = 1; i < tRawlen; i += 2)
{
  auto rawMark = decodedIRData.rawDataPtr->rawbuf[i];
  auto rawVal = decodedIRData.rawDataPtr->rawbuf[i + 1];
  int mark = round(((float)rawMark) / 2.5f);
  int val = round(((float)rawVal) / 2.5f);

  if (mark > 5)
  {
    Serial.println("Timing mark above 5!");
    break;
  }

  if (val < 10)
  {
    Serial.print("0 ");
  }
  else
  {
    Serial.print("1 ");
  }

  if (((i + 1) / 2) % 10 == 0) {
    Serial.print(" | ");
  }
}

The full code snippet is available as a gist.

Decoding output

It produces the output above which shows each bit in groups of 10.

To figure out the payload it's a classic reverse engineering case of making one change at a time and seeing what changes - for example move the temperature up 1 degree, see what changes, do it again and establish the pattern.

There's no silver bullet here - it just takes some time and a logical approach.

The screenshot above has some of the adjacent differences in it which are easy enough to spot due to the formatting (eg the 3rd block from the left).

Decoding the time-related fields were most tedious mainly because of the menu system on the remote.

IR Payload Format

Each setting is either a bit field that is toggled, or a range of bits representing a state - some with classic binary encoding, but the time fields use a weird hybrid format between classic base-2 and multiples of 10.

There are some bits that don't appear change or do anything - but the remote also has some buttons and options that do not relate to this unit.

The bit addresses below are described as 1-indexed (i.e. starting at 1 not 0).

The first 3 bits control individual functions - Home, Night Mode and Swing.

Bits 4 (high) and 5 (low) control the fan speed:

  • 0 = Low
  • 1 = Medium
  • 2 = High
  • 3 = Auto

Bits 6 (high), 7 and 8 (low) set the mode:

  • 0 = Off
  • 1 = Cool
  • 2 = Heat
  • 3 = Dehumidifier
  • 4 = Fan
  • 5 = Auto

Bits 69 (high) through 72 (low) set the temperature, the encoded temperature is the desired temperature less 15 - for example for 18 degrees the value sent would be 18 - 15 = 3.

Bit Role
1 Home (On / Off)
2 Night Mode (On / Off)
3 Swing (On / Off)
4 Fan (2)
5 Fan (1)
6 Mode (4)
7 Mode (2)
8 Mode (1)
9
10
11
12 Clock Hours (20)
13 Clock Hours (10)
14 Clock Hours (8)
15 Clock Hours (4)
16 Clock Hours (2)
17 Clock Hours (1)
18
19
20 Clock Mins (40)
21 Clock Mins (20)
22 Clock Mins (10)
23 Clock Mins (8)
24 Clock Mins (4)
25 Clock Mins (2)
26 Clock Mins (1)
27
28 Timer 1 (On / Off)
29 Timer 1 On 30 Min
30 Timer 1 On Hours (20)
31 Timer 1 On Hours (10)
32 Timer 1 On Hours (8)
33 Timer 1 On Hours (4)
34 Timer 1 On Hours (2)
35 Timer 1 On Hours (1)
36
37
38 Timer 1 Off 30 Min
39 Timer 1 Off Hours (20)
40 Timer 1 Off Hours (10)
41 Timer 1 Off Hours (8)
42 Timer 1 Off Hours (4)
43 Timer 1 Off Hours (2)
44 Timer 1 Off Hours (1)
45
46 Timer 2 (On / Off)
47 Timer 2 On 30 Min
48 Timer 2 On Hours (20)
49 Timer 2 On Hours (10)
50 Timer 2 On Hours (8)
51 Timer 2 On Hours (4)
52 Timer 2 On Hours (2)
53 Timer 2 On Hours (1)
54
55
56 Timer 2 Off 30 Min
57 Timer 2 Off Hours (20)
58 Timer 2 Off Hours (10)
59 Timer 2 Off Hours (8)
60 Timer 2 Off Hours (4)
61 Timer 2 Off Hours (2)
62 Timer 2 Off Hours (1)
63
64
65
66
67
68
69 Temperature (8)
70 Temperature (4)
71 Temperature (2)
72 Temperature (1)

Emulation: Controlling the Unit

Controlling the AC unit is simply the case of building up the payload based on the information above, and sending it.

Building the payload:

const bool payload[] = {
    home,
    nightMode,
    swing,
    ((fanSpeed >> 1) & 0x01), (fanSpeed & 0x01),
    ((mode >> 2) & 0x01), ((mode >> 1) & 0x01), (mode & 0x01),
    0, 0, 0,
    clkH[0], clkH[1], clkH[2], clkH[3], clkH[4], clkH[5],
    0, 0,
    clkM[0], clkM[1], clkM[2], clkM[3], clkM[4], clkM[5], clkM[6],
    0,
    timer1Enabled,
    timer1On30Min,
    timer1OnH[0], timer1OnH[1], timer1OnH[2], timer1OnH[3], timer1OnH[4], timer1OnH[5],
    0, 0,
    timer1Off30Min,
    timer1OffH[0], timer1OffH[1], timer1OffH[2], timer1OffH[3], timer1OffH[4], timer1OffH[5],
    0,
    timer2Enabled,
    timer2On30min,
    timer2OnH[0], timer2OnH[1], timer2OnH[2], timer2OnH[3], timer2OnH[4], timer2OnH[5],
    0, 0,
    timer2Off30min,
    timer2OffH[0], timer2OffH[1], timer2OffH[2], timer2OffH[3], timer2OffH[4], timer2OffH[5],
    0, 0, 0, 0, 0, 0,
    ((t >> 3) & 0x01), ((t >> 2) & 0x01), ((t >> 1) & 0x01), (t & 0x01)
};

For ease of interaction, the above code is wrapped in a class with some set methods (setters).

In the setters, values are converted, such and clock and timer settings:

  void setClock(int hours, int minutes)
  {
    hoursToArray(hours, clkH);
    minutesToArray(minutes, clkM);
  }

We can't use conventional bit shifting due to the weird 10, 20, 30, 40 aspects.

This function will convert the hours:

void hoursToArray(int hours, bool *arr)
{
  int values[] = {20, 10, 8, 4, 2, 1};
  for (int i = 0; i < 6; ++i)
  {
    if (hours >= values[i])
    {
      hours -= values[i];
      arr[i] = true;
    } else {
      arr[i] = false;
    }
  }
}

And this function will convert the minutes:

void minutesToArray(int minutes, bool *arr)
{
  int values[] = {40, 20, 10, 8, 4, 2, 1};
  for (int i = 0; i < 7; ++i)
  {
    if (minutes >= values[i])
    {
      minutes -= values[i];
      arr[i] = true;
    } else {
      arr[i] = false;
    }
  }
}

While this all appears to work - to some extent it is hard to verify since we cannot query the AC unit for it's status, and other than on/off and a few lights that indicate timer or compressor state there's minimal feedback.

Bringing it all together...

Now that the payload is understood and the ability to emulate it exists with some structure, it can all be implemented as part of a wider solution.

For now I have the ESP join the current network and serve up a React Web UI (here is the related guide), from which I can test the code and control the AC.

I can easily extend this to take commands and share status via MQTT.