The BoilerIO Software Thermostat

It’s time to step up from command-line control of the heating system I’ve been working on to having a weekly schedule and temporary override function available through a UI, making the system practical on a day-to-day basis.

As always, code described here is available on github under an MIT license.

Overview

The scheduler chooses a target heating temperature throughout the day and needs to be usable by someone non-technical to show the state of the system, provide a means to enter and edit a schedule, and provide a means to change the target temperature for a fixed period of time.

system-overview

To achieve this a new daemon and web service have been added to the architecture.  Here is a summary of the responsibilities of those components:

  • The Scheduler reads the weekly schedule from the database, and sends commands to the Boiler Controller to change the target temperature at the appropriate times.
  • The SchedulerWeb REST service provides a REST API over HTTP to get a summary of system status, set or clear the temperature target override, and add and remove entries from the weekly schedule.  This is implemented using Flask in Python, with uWSGI and nginx to provide a robust service.
  • The scheduler configuration database is a PostegreSQL database holding the schedule, a cached copy of the current temperature, and the target override configuration.  It’s the primary store for this information: the REST service updates it and the scheduler uses it to control the temperature.
  • The SchedulerWeb web frontend is an HTML5/CSS3/Javascript single-page web UI that makes use of the REST service to interface with the system from the user’s web browser.

Experience and implementation notes

Rather than trying to document the entire implementation, I will instead talk about each area and some of the challenges or points of interest, and the major decisions that were taken and why.  My day job is a lot lower in the software stack than this, so there may better approaches than what I’m presenting.

The HTML frontend

alignment
1px alignment issue (red line illustrates the problem) without left padding on input boxes

The app is designed to largely follow Google’s Material Design guidelines; I hadn’t realised when I started that there are stylesheets provided by Google that can be used so I implemented the styles I needed myself.

Since the fashionable choice of web-framework seems to change frequently I was initially resistant to using anything at all fancy, and instead use traditional jQuery directly.  In retrospect, not having some of the features of a slightly higher-level framework probably made the code worse that it ought to be and in future I might look at whether a framework provides a better frontend implementation.

Modern web development has definitely come on a long way since I last attempted it: the developer tools inside Chrome are great, though there are still cross-browser issues even when only supporting latest-generation browsers (for example implementing modal dialogs is still done manually even though there’s the dialog tag now, and I had issues with alignment and appearance of certain input fields).

The REST service

The backend service uses Flask, a neat framework for web apps in Python.

When developing your service you should be aware that werkzeug, the built-in web server, is not suitable for production due to security and scalability issues.  However, if you do use it during test you’ll find it also makes it easy to accidentally keep global state within your app, which you shouldn’t be doing because it won’t work when you’re inside a production server.  For that reason, I suggest starting to use uWSGI relatively early in your development.  It’s not difficult to use for test.

I’ll come onto a step-by-step deployment guide in an upcoming post, but I recommend that the web service be deployed using uWSGI behind nginx or Apache to get a secure and scalable deployment.  However, on Raspbian at least there are a couple of pitfalls I found with uWSGI: I used pip to install a relatively modern version.  The installation used a PREFIX of /usr/local.  For some reason, even with /usr/local/bin on my PATH, uwsgi did not work correctly unless called with its full path (despite printing a message stating the full path it had detected, which was correct).  Perhaps this is a security measure, but the failure mode here and on other issues I experienced was somewhat opaque and better error messages would have been useful.

To use uwsgi in production, it is helpful to have it start to host relevant services on system boot.  On Raspbian, this requires a systemd unit to be created (on other systems an init script, upstart job, or just adding to rc.local would be needed).  That nothing was already in place could be a result of the way I installed uwsgi, but in any case I followed the Debian package’s convention of creating a configuration file in /etc/uwsgi/apps-available and linking to it in /etc/uwsgi/apps-enabled.  The config I used was this:

[uwsgi]
chdir = /var/www/app/boilerio
socket = /var/www/app/thermostat.sock
module = schedulerweb:app
logto = /var/log/uwsgi/thermostat.log
uid = boilerio
gid = www-data
chmod-socket = 664

Note here that, for better isolation, I’m using a system user specifically for this service, and sharing a group with the web-server so I can create the socket with permisions for both to be able to access.

nginx and the URL namespace

The web service natively places its REST endpoints at root level.  As I use nginx to also serve static content – the client HTML/JS/CSS files – I decided to map the service under /api.  Files for the web client get served from /, and they make API calls assuming the api prefix .  I use a simple nginx configuration file to achieve this:

server {
    listen      80;
    server_name hub;
    charset     utf-8;
    location / {
        root /var/www/app/boilerio/static;
    }
    location /api {
        try_files $uri @boilerio;
    }
    location @boilerio {
        include uwsgi_params;
        rewrite ^/api/(.*)$ /$1 break;
        uwsgi_pass unix:/var/www/app/thermostat.sock;
    }
}

The endpoints and the data they expect are currently hand-coded in the Python web application code, which is less than ideal.  Defining a clear API where constraints on input and validation can be consistently and mechanistically verified is a better approach and an area for improvement.  Swagger seems like one good option to implement this, has integration options with Flask, and has the side benefit of a nice web UI for making REST calls to the service too.

The scheduler

After modifying the maintaintemp script to listen for new target temperatures over MQTT rather than having a static target passed on the command-line, the scheduler is able to periodically update the target temperature.  The current, simple, implementation polls the database once per minute, or when a trigger message is received, to load the currently-active schedule and target override.  It then selects a target based on these inputs and sends a message to the boiler controller to update the target temperature.  At startup, and when the controller for a zone restarts, the target request is sent immediately to avoid having to wait a whole polling interval.

This is a likely area for innovation in future: enhancing how the target is chosen using additional inputs or policies, enabling features like pre-heating to reach an upcoming set-point, altering the target based on presence information, and intelligently dealing with installations with multiple zones or independent controls within a zone.

The database

A PostgreSQL database is used to store the schedule and configuration.  This might seem like overkill, but when developing a web app where multiple processes need read/write access to the data it seems sensible to use a tool that is designed for that kind of environment, even if the scale it is being deployed at is relatively tiny, for a few reasons:

  • It avoids designing scalability out now.  If we used another approach that was “simpler” but couldn’t be scaled if necessary it would be a potentially large undertaking to fix.
  • You get a lot of correctness for free.  If, for example, the schedule were stored in a plain text file (say, as JSON), then it is definitely possible to make everything atomic.  But the hassle of getting it exactly right does not seem worthwhile when the database can deal with it all more efficiently from the beginning.

Another approach would be to use a document store like MongoDB.  There are pros and cons to either way (this strongly-worded post is an interesting read concerning problems faced in a practical application), but I decided to go with something I was familiar and confident with.  While having a fixed schema seems to be considered by some to be “overhead” or to slow down development, it can also reduce problems by helping to identify programming or design errors earlier in the cycle and certainly did not seem to make development difficult.

What about AWS/PaaS/Serverless?

A somewhat different approach could be to use Amazon or Azure web services.  Amazon have several relevant offerings here (and I believe Microsoft have alternatives too):

  • AWS IoT: This is a service that maintains a ‘shadow device state’.  User agents can post new ‘desired state’ and devices can post the actual state of the device.  AWS publishes messages to indicate various conditions including when the target and current state are divergent so that the device can change its physical state to match the desired state.
  • AWS Lamda and API gateway: This provides a potentially simple way to implement the scheduler REST API without having to host the web service component yourself, potentially reducing the maintenance burden.  You can easily provide authenticated access regardless of where you are connecting from.  Zappa is a tool that lets you easily run a Flask applications within Lambda, so could be used to allow the BoilerIO code base to be used without modification.
  • S3 could host the static files, such as the CSS, JS, HTML, etc. for the client app.
  • AWS DynamoDB or Relational DB services.  The latter could be a drop-in replacement as it has PostgreSQL support, whereas changes to the app (relatively minor/contained within one module) would be required for DynamoDB although that option does have more attractive pricing

The first issue to consider with this approach is what the connectivity requirements are and what is the user’s expectation of behaviour when their Internet connection is unavailable.  In this case, the minimum requirements seem to be that (i) the schedule should continue to run without interruption regardless of Internet downtime, and (ii) the user should be able to supply at least an override even if the schedule is not editable.  Both are doable, the first trivially since the scheduler can (and should) be run locally to the installation.

The second issue is vendor lock-in.  These services are proprietary, and there’s no way to run local versions of them either for testing or deployments where using an online service is suitable.

In the end I decided to stick with a regular web service for now, which leaves the option open for either hosting it off-site, in a remote VM for example, or having a connector module that enables AWS IoT or similar to provide off-network access without hosting a public-facing service locally.

Next steps

This blog post covers part of one of the “next steps” I identified in the previous post.  Upcoming areas for further work are better documentation including a setup guide, and looking at additional features such as multiple zone support and pre-heating.

Advertisements

Danfoss Wireless Thermostat Hacking – Part One

tp7000rf

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.

More posts in this series

Setup

rx2

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.

Signal acquisition

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.

ask
Trying to decode the signal as amplitude-moduated

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).

nooelec
Nooelec SDR receiver

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.

gqrx

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:

  1. Choose the FM (Stereo) demodulation option
  2. Ensure the correct centre frequency, 433.9MHz, is chosen
  3. Press the Rec button in the bottom-right.
  4. Trigger the transmission.
  5. 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:

audacity

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 001 and 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:

  • 0xAA at 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.
  • 0xDD and 0x46 are 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.
  • 0xC5 and 0x88 (together 0x88C5, also seen written in ink on the PCB of this particular thermostat) are the thermostat ID.  This is different for the other thermostats.
  • 0xCC is the instruction.  This is 0xCC for ‘off’, 0x77 for ‘learn’, and 0x33 for ‘on’.
  • The rest of the transmission is a repeat of the original message, and looks different at first glance in hex because there was a 0 bit between the two transmissions so the second one is offset by one bit.  (You can see this for yourself by running xxd with the -b option 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.