Home Energy Management System Part 2: Hacking the iBoost+ protocol

The Marlec iBoost+ solar PV hot water diverter uses a current transformer to detect when there is surplus power being exported. This is wirelessly connected to the main iBoost+ unit, which adjusts the power fed to the hot water tank immersion heater so as to use any excess power to heat the water, until the immersion heater thermostat cuts out.

The iBoost+ Buddy display unit is also connected wirelessly to the main unit. By this means the Buddy can display the current grid import or export and the amount of power being diverted to the immersion heater, or “Water tank HOT” if the thermostat has cut out. It can also be used to turn on the immersion heater at full power temporarily for a set period of time.

The sender is battery powered and measures only the current being imported or exported. It sends a burst of data over a 896.3MHz wireless channel to the main unit every ten seconds. The iBoost+ main unit and the Buddy also communicate over 896.3MHz wireless.

To monitor and control the iBoost+, one option would be to fake the sender current transformer input, and add a current transformer to measure the power being fed to the immersion heater. But if we can decode the RF protocols, it would be simpler to fake the sender RF transmission, and read the messages sent by the main unit to the Buddy.

There have been previous attempts to decode these RF protocols, which are recorded at https://github.com/merbanan/rtl_433/issues/1739. These have been only partially successful. So far these decode attempts have been made using RF data captured from Software Defined Radio receivers.

Decoding the sender protocol

I decide to take a closer look at the iBoost hardware, in particular the sender unit. Removing the cover revealed a main circuit board with a microcontroller, and a RF daughter board with a CC1100 RF transceiver IC. This IC uses SPI to communicate with a microcontroller, and the SPI pins were conveniently connected directly to the standoff pins joining the daughter board to the main board. So I hooked up a logic analyser and captured some traces.

Here are some sample captures:

With no sensor connected:

MISO: 0x0F·0x0F·0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F

MOSI 0x36 0x3F 0x2C 0x7F 0x8D 0x2D 0x01 0x00 0x02 0x02 0x02 0x02 0x02 0x02 0x02 0x02 0x02 0x02 0x02 0x02 0x02 0x02 0x02 0x02 0x02 0x02 0x02 0x02 0x02 0x02 0x02 0x02 0x02 0x02 0x02 0x02 0x02 0x02 0x02 0x02 0x02 0x02 0x02 0x02 0x02 0x02 0x02 0x02 0x35 

After further captures it was observed that the MOSI data bytes that are all 0x02 in this capture are in generally a mixture of 0x01 and 0x02.

With the sensor connected but no current flowing:

MISO 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F

MOSI 0x36 0x3F 0x2C 0x7F 0x8D 0x2D 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x35

We see that the 0x01 and 0x02 bytes have all been replaced by 0x00.

With a 3kW kettle drawing current through the CT (five different samples of MOSI):

MISO 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F

MOSI 0x36 0x3F 0x2C 0x7F 0x8D 0x2D 0x01 0x00 0x7D 0x89 0x82 0x6B 0x4E 0x29 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x25 0x4F 0x6D 0x7C 0x88 0x84 0x6E 0x51 0x2D 0x06 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x03 0x2A 0x54 0x70 0x35

MOSI 0x36 0x3F 0x2C 0x7F 0x8D 0x2D 0x01 0x00 0x26 0x1D 0x13 0x0A 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x05 0x11 0x1D 0x22 0x27 0x26 0x1E 0x14 0x0B 0x05 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x07 0x13 0x1E 0x22 0x27 0x35

MOSI 0x36 0x3F 0x2C 0x7F 0x8D 0x2D 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x03 0x09 0x0E 0x10 0x13 0x12 0x0E 0x0B 0x07 0x05 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x03 0x08 0x0D 0x10 0x10 0x0F 0x0C 0x0A 0x07 0x05 0x01 0x00 0x00 0x00 0x35

MOSI 0x36 0x3F 0x2C 0x7F 0x8D 0x2D 0x01 0x00 0x00 0x00 0x00 0x01 0x02 0x03 0x06 0x06 0x07 0x07 0x06 0x05 0x03 0x01 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x02 0x03 0x05 0x05 0x06 0x07 0x06 0x05 0x03 0x02 0x00 0x00 0x00 0x00 0x00 0x00 0x35

MOSI 0x36 0x3F 0x2C 0x7F 0x8D 0x2D 0x01 0x00 0x06 0x07 0x07 0x07 0x06 0x04 0x02 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x01 0x02 0x04 0x06 0x06 0x07 0x06 0x07 0x05 0x03 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x01 0x02 0x02 0x05 0x35

It can be seen that the MISO data is always 0x0F, indicating that the sensor is not receiving anything. This is the case whether or not the main unit and Buddy are powered. So the sensor is transmit-only.

Plotting the MOSI data for several sets of data gave the following chart:

MOSI data for six data captures

We observe the following:

  • The first 8 bytes and the last byte are the same in all captures. The first few bytes are commands to the CC1100 chip rather than data to be sent. The last byte is also a command to the CC1100.
  • The remaining 40 bytes appear to show the positive half cycles only of a sinusoidal waveform.

So it is reasonable to conclude that once every ten seconds the ADC takes 40 readings at 1ms intervals, then these readings are set in a single burst as 8-bit data points. Negative values are sent as zero.

The three readings that peak at a value of 140 were taken with a 3kW kettle drawing current. The Buddy displayed about 2600W during this time. Assuming 240V main voltage, this corresponds to an current of 10.83A RMS or 15.3A peak. Therefore the transmitted value appears to be about 9.1 times the instantaneous current.

So it appears that this transmission will be easy to fake, using an off-the-shelf RF module based on a CC1100 or CC1101 chip.

Decoding the main unit and Buddy protocols

Armed with the above information, I set up a SDR radio to receive the signals and confirmed that I could use rtl_433 (https://github.com/merbanan/rtl_433) to capture similar data. I found that it was unable to capture the data when no current was flowing and all the samples were zero, but otherwise it could.

I then tried capturing transmissions with either or both of the iBoost main unit and the Buddy powered up. With just the main unit powered up, there were no additional transmissions. With just the sender and Buddy powered up there were additional transmissions like this:

2c8d2d0100000000000000000000000000000000000000000000000000000000000000000000000000000000000

I interpret this as a message from the Buddy to poll and/or command the main unit. When the main unit is also powered up, transmissions of the following form appear shortly after each message from the sender:

258d2d2200000001000000000100000000ffffffffffff000000000000000000000000000000a7f38

The above message was captured with zero current in the sender. With a 1400W load connected it changed to this:

258d2d2200000001000000000100000000000018190900000000000000000000000000000000f2020

With a 3kW kettle connected (Buddy reading about 2500W, reducing slowly) I saw these transmissions:

258d2d22000000010000000001000000000000b7071000000000000000000000000000000000c1f38
258d2d22000000010000000001000000000000f2371000000000000000000000000000000000d9bc4
258d2d22000000010000000001000000000000f2371000000000000000000000000000000000d9bc0
258d2d22000000010000000001000000000000cf3010000000000000000000000000000000009b020
258d2d220000000100000000010000000000009f21100000000000000000000000000000000097c82

With the current transducer reversed (so that the Buddy registered power export) it changed to this:

258d2d2200000000000000000100000000590b5de3f6ff000000000000000000000000000000cc760

So it appears that the import/export amount and heating power are is passed in a 6-byte field. A value of 0xffffffffffff means no data available. A value of 000018190900 is an amount of imported power, and a value of 590b5de3f6ff is an amount of exported power and an amount of heating.

Further tests indicated that when the amount of imported power varied, the final two bytes of this 6-byte field remained as 0900 and the leading two bytes remained at 0000. With the export current held more or less constant and the heating power ramping up, the values I saw included these:

0000d81ff7ff 0000d81ff7ff 1b00ac2bf7ff b5045842f7ff b5084563f7ff 590b5de3f6ff

With the export power increased to almost 3kW I saw these values:

5404d9ffefff 150b58c8efff 150b58c8efff

So I hypothesise the following:

  • The first two bytes represent the amount of heating power as a 16-bit little-endian unsigned integer;
  • The next four bytes represent the amount of imported or exported power as a 32-bit little-endian signed integer. With 2.5kW export indicated these values are 0xffefffd9 and 0xffefc858 which translate to -1048615 and -1062824. With 1.4kW export the values are 0xfff71fd8, 0xfff72bac, 0xfff76345 and 0xfff6e35d which translate to -581672, -578644, -564411 and -597155. With 2.5kW import they are 0x001007b7, 0x0001037f2, 0x001030cf and 0x0010219f which translate to 1050551, 1062898, 1061071 and 1057183; so approximately the negative of the export figures, as expected.
  • There are also a couple of 1-bits earlier in the message, meaning unknown.
  • When the thermostat cuts out, one or more bits must change to tell the Buddy to display “Water tank HOT”. I haven’t captured this message yet.

You can see some sample data I captured in BitBench.

I also captured the initialization commands that are sent to the CC1100 chip when the sender battery is inserted. First CS is lowered, a single byte of 0x30 is sent and CS is raised again. Then CS is lowered and there is the following 70-byte burst:

0x0D·0x21·0x0E·0x65·0x0F·0x6A·0x0B·0x08·0x0C·0x00·0x10·0x5B·0x11·0xF8·0x12·0x03·0x13·0x22·0x14·0xF8·0x0A·0x00·0x15·0x47·0x21·0xB6·0x22·0x10·0x18·0x18·0x17·0x00·0x19·0x1D·0x1A·0x1C·0x1B·0xC7·0x1C·0x00·0x1D·0xB2·0x23·0xEA·0x24·0x2A·0x25·0x00·0x26·0x1F·0x29·0x59·0x2C·0x81·0x2D·0x35·0x2E·0x09·0x00·0x0B·0x02·0x46·0x07·0x04·0x08·0x05·0x09·0x00·0x06·0xFF

Then CS is raised, lowered for this 9-byte burst, and raised again:

0x7E·0xC6·0x39·0x3A·0x3B·0x3C·0x3D·0x3E·0x3F

About 10 seconds later it sends the commands and data for the first sensor reading transmission. Another user kindly decoded these commands and published his analysis at https://github.com/merbanan/rtl_433/issues/1739#issuecomment-1265761556.

This information should be sufficient for me to construct a module to transmit fake CT readings to replace the sender and fake poll messages to replace the Buddy, and receive the readings from the iBoost main unit.

Next: Planning the Hardware


This entry was posted in Electronics and tagged . Bookmark the permalink.

1 Response to Home Energy Management System Part 2: Hacking the iBoost+ protocol

  1. Sam says:

    Really nice writeup. I have one of these, and I’m looking for a way to remote control the power usage at night. I get cheap power at night, and use the timer to top up during the day.
    However, I have some data that the iBoost does not – tomorrow’s weather. I’d like to inhibit the iBoost from charging if it is a sunny day upcoming.

    Disabling the timer, and using fake 3kW CT values could do this (at the cost of losing track of how many kW the device really has saved).

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s