Reverse engineering a wireless thermostat

We moved the boiler in our house a few months ago, and replaced the wired, rotary thermostat in the process, with a 7 day digital wireless one. Or rather, the builders did, and I didn't really pay attention.

The timers are pretty simple - a morning schedule, an early evening schedule and a late evening schedule - but we're often in and out at different times, and the UI is so terrible and the buttons so horrible to press, it's preferable to just sit in the cold and shiver.

Anyway, I wanted to see if I could control the heating from something smarter. I've not quite done that yet, but I've managed to reverse engineer the signalling, and I thought it might be useful to write down how I did it.

We've got a Tower RFWRT thermostat - it's pretty cheap. There's two parts: the heating control unit, connected to the boiler, and a wireless thermostat you can place anywhere within range. These communicate over 433MHz, an unlicensed band which is pretty common for household devices.

The thermostat sends one of two command: turn on or turn off. It sends the current command on a 30s cycle, and when the state is changed. If the on command isn't received for a certain period (the thermostat has gone out of range, run out of battery, etc.) the heating control unit will switch the boiler off as a safety measure.

If we can find out what these signals are we can send them ourselves, and eventually replace the wireless thermostat with something smarter. (Or, y'know, you can just buy a Nest or something. You can stop reading here.)

There's a few ways to receive 433MHz signals, but the easiest for me was to use an RTL-SDR USB digital TV receiver. These are about £10, and let you listen across a wide range of frequencies (not just TV/radio), up to about 1700MHz.

I used CubicSDR, a Mac SDR application, and pressed the temperature controls up and down watching for the on/off commands. Sure enough, I spotted a short blip whenever the desired temperature is set.

CubicSDR

Using Audio Hijack (Soundflower works too), I recorded both commands to an audio file, trimmed the silence, and put the two segments side by side to compare.

Waveform

It looks like the signal is repeated twice, probably to reduce the likelihood of interference.

You can see a preamble at the beginning - a long high and long low, four times - probably designed to let the receiver calibrate its automatic gain control. These receivers crank up the volume (and the noise) during silence, and then adjust it when they hear a signal. The preamble gives the gain control a chance to respond so the beginning of the data isn't lost in the noise.

After that we're into the command data. I spent a long time staring at this, initially thinking it was Manchester encoded, before realising I needed to switch Audacity into ‘Waveform (dB)’ mode to see what's really going on. This is actually pulse width modulation, with a 0 sent as a short high and a long low, and a 1 sent as a long high and a short low.

Waveform

I measured each short section as 250μs long, and each long section as 500μs, and each preamble section as 1000μs.

The off command is just a run of 25 zeros, while the on command sends a few 1s too. It doesn't really matter what it means — I just need to send the same thing.

I took a cheap 433MHz transmitter and soldered a coiled 17cm antenna on. I attached this to the digital out of an Ardiuno, and wrote a little script to perform the transmission.

#define transmitPin 4 // Digital Pin 4

// on
bool data[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0 };
// off
// bool data[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };

int shortDelay = 250;
int longDelay = 500;
int preambleDelay = 1000;

void setup() {
Serial.begin(9600);
pinMode(transmitPin, OUTPUT);
delay(3000);
}

void loop() {
sendPreamble();
sendData();
sendPreamble();
sendData();
delay(30000);
}

void sendPreamble() {
for (int i = 0; i < 4; i++) {
digitalWrite(transmitPin, HIGH);
delayMicroseconds(preambleDelay);
digitalWrite(transmitPin, LOW);
delayMicroseconds(preambleDelay);
}
}

void sendData() {
for (int i = 0; i < sizeof(data); i++) {
bool b = data[i];
if (b == 1) {
digitalWrite(transmitPin, HIGH);
delayMicroseconds(longDelay);
digitalWrite(transmitPin, LOW);
delayMicroseconds(shortDelay);
} else {
digitalWrite(transmitPin, HIGH);
delayMicroseconds(shortDelay);
digitalWrite(transmitPin, LOW);
delayMicroseconds(longDelay);
}
}
}

A little click, the boiler goes on, and we're in business. To start with, I'd like to schedule the timing from a shared family calendar, so now I need to make a thermostat control loop, and drive the whole thing from something web connected, like a Raspberry Pi.