I wanted to control my central heating system using a Raspberry Pi and Arduino micro-controllers to provide better control, flexibility, and a fun home automation project.
We originally chose wireless thermostats when we replaced the heating system in our home, but their user interface is not great and they are fiddly to use. “Smart” thermostats were starting to come onto the market showing a glimpse of what could be done.
Having made some useful progress in my overall goal, I am documenting it here for the benefit of others. My requirements were simple:
- Easy to change the heating profile for a day, e.g. if we decided to light a fire and didn’t need heat from the central heating system;
- The boiler should be used efficiently to reduce costs;
- Changes should be minimally invasive to the existing setup (e.g. no major rewiring/plumbing).
This post talks about how I was able to control the boiler whilst being minimally invasive by using the existing thermostat receiver and reverse-engineering its protocol, thus avoiding any electrical modifications.
The system being ‘hacked’ is a Danfoss RX2 wireless receiver, with two TP7000 RF thermostats. It’s plumbed to create a two-zone heating system, one zone for each floor of the house.
Options for controlling the boiler
Initially I planned to put my own relay into the system with a wireless module attached that I could control it with. I chickened out of this approach mostly because I didn’t want my dodgy soldering interacting with always-on mains-voltage equipment. This led me to the idea of a Z-Wave based relay. Fibaro make a product (the FGS-222) that’s quite appropriate for this use case: it is a dual-relay unit (since my home has two heating zones) and has switched and permanent inputs so you can have the existing control system continue to operate, or override it with your own. The problem here was that Z-Wave devices require a gateway (such as Domoticz) to get them working, which seemed a bit overkill, but I think in general this is a reasonable route to go down.
However, my goal is to be minimally invasive: by using the existing control mechanism (the Danfoss RX2 wireless receiver), no changes are needed to the boiler electrical circuits. Of course, that is easier said that done since it requires emulation of the protocol used by the wireless thermostats. In this post I talk about receiving and decoding the protocol; subsequent posts will talk about emulating it and taking over control of the system.
My starting point was to try to capture the signal being sent by the Danfoss wireless thermostats to the receiver unit, in order that I could at least replicate it bit-for-bit. Ideally though, I’d also like to understand the contents of the payload of the messages being transmitted, and be able to capture them programatically in order to track when the existing system is calling for heat.
Having taken apart an RX1 receiver (a single-channel version of the RX2) that was given to me some time back, and photographed the circuit board in anticipation of this project, I can see it uses an Infineon TDA5210 chip for RF. The datasheet indicates that this is a receiver only, which tells us that the protocol is one-way and could either be amplitude- or frequency-modulated. Having looked at the circuit, I mistakenly thought that the signal was amplitude modulated and tried to use a basic RF receiver a friend gave me to receive the signal by having an Arduino dump its output over serial in variously increasingly complicated ways. I quickly became frustrated on seeing a long “high” followed by silence as the gain circuit ramped back up to just amplifying noise in the receiver. I initially thought I was missing the transmission, but was actually seeing it all along albeit unable to decode it because it was actually frequency modulated (and therefore seen by the ASK receiver as the long ‘high’ pulse).
Unable to make progress I wondered if I was mistaken about the modulation, didn’t know much about the RF69 yet that we’ll use later, and needed to find a way of figuring out what was going on. Software-defined radio seemed to provide the answer: enter the Nooelec USB software-defined radio receiver. Note that this isn’t necessarily the best hardware to buy, but it was available quickly in the UK and seemed to be good enough. The RTL-SDR blog sell a modified version of units like these that are optimised for use with SDR apps, but as they are shipped from China the shipping time can be quite long.
As a Mac/Linux user the software options for SDR are a bit limited. The flagship option seems to be SDR# but this is only available on Windows. You apparently can get it to work on macOS using Mono, but instead I decided to opt for gqrx using X11 installed via MacPorts. Once installed, you can turn on the waterfall view and then try to trigger the signal. From previous experimentation with the ASK decoders, I was pretty sure that just pressing a button (temperature up/down) would result in an RF transmission even if the boiler state wasn’t being changed, which is handy because it meant I could avoid cycling the boiler on/off without disconnecting it from the mains.
On centring the receiver at 433.9MHz (chosen from looking at the TDA5210 datasheet) and triggering a transmission, it’s very clear that the signal is frequency modulated (the horizontal axis shows the frequency domain, the vertical axis shows time, and the colour show signal strength). The waterfall display isn’t detailed enough to be able to see the signal content, but by experimenting with demodulation options in the software I found that the signal came out cleanly demodulated using the “FM (Stereo)” option:
- Choose the FM (Stereo) demodulation option
- Ensure the correct centre frequency, 433.9MHz, is chosen
- Press the Rec button in the bottom-right.
- Trigger the transmission.
- Press the Rec button again to stop the recording.
The signal is saved as a .wav file, which then takes us into similar territory documented by others of examining and trying to replay the signal ourselves. You can use Audacity to view the waveform you saved from gqrx:
As a starting point, I captured the same signal multiple times with the target temperature being different (i.e. different set temperatures all of which result in no heating demand, and the room temperature not having been updated) and found each capture produced a signal that looked identical. I then compared that with one where the boiler should be on and at that point it was looking good: the signal was pretty much the same apart from in one section where a couple of 0s become 1s and vice-versa.
Decoding the signal
Looking at the signal it seems like there is a clear pattern of
011 occurring; these likely correspond with 0s and 1s in the decoded signal. Python has a handy library,
wave, that you can use to easily read the values from a
.wav file, so I used this first to dump the file to get an idea of how many frames the longer pulses lasted for. I then used simple temporal and amplitude thresholding (detecting when a high or low has been seen for more than a fixed number of frames in the wave file) to find the encoded values: if we see two
0s together in the wire protocol we emit a
0, and if we see two
1s together a
1 is emitted.
This is the program that I used:
We can use
xxd to dump this as hex so we can inspect it. This is the decoded data for the ‘upstairs off’ signal:
andy@beta:~/rf$ python decode_danfoss.py -d up_off.wav | xxd 00000000: aadd 46c5 88cc 556e a362 c466 ..F...Un.b.f
Looking at this combined with other captured signals makes it pretty clear what’s going on:
0xAAat the start is the preamble. It is somewhat interesting that they transmit this as encoded data, I’m not sure if that is common practise. The preamble is used by the receiver to set the gain correctly.
0x46are both part of a “sync word”, and are consistent across all messages from all thermostats. This indicates to the receiver that the signal is of interest to them.
0x88C5, also seen written in ink on the PCB of this particular thermostat) are the thermostat ID. This is different for the other thermostats.
0xCCis the instruction. This is
0x77for ‘learn’, and
- The rest of the transmission is a repeat of the original message, and looks different at first glance in hex because there was a
0bit between the two transmissions so the second one is offset by one bit. (You can see this for yourself by running
-boption to dump the output as binary instead of hex.)
In part two
In the next part, we will use an RF69 module alongside an Arduino-compatible microprocessor to send messages to the receiver to turn on/off the boiler, as well as receive messages from the existing thermostats programmatically to observe their behaviour.
Continue to Part Two.