# Boiler Control to Maintain a Set Temperature

I wanted to be able to control my home’s heating from a computer.  This post discusses the next phase in that project: a control layer that maintains a specified room temperature using a temperature sensor and the boiler control built in previous articles.

I published the boilerio repository on github that contains the code to do this.

Note that neither the code nor the article come with any warranty: please be careful if you’re using it as you could damage your heating system or create a safety hazard.

# Heating system overview

Our heating system is fairly typical for the UK: an S-plan system using a gas-fired “system” boiler with a pressurised hot water storage vessel.  When the thermostats call for heat, water is heated by the boiler and pumped around a series of radiators.  As noted in the previous article, we have a Danfoss RX2 receiver whose control protocol I’ve reverse-engineered, so we will use that here to control the boiler.

This article looks at the common but relatively crude control method of simply turning the boiler on/off periodically to maintain a steady temperature.  Note that the boiler also has a manually-specified target flow temperature and will modulate the burners to achieve this when it is active.

More advanced controls, possibly the subject of future articles, could adjust boiler settings in response to flow temperature and other variables to ensure the boiler is within its most efficient operating parameters.  This requires integration with the boiler’s electronic systems not explored here.

The system has three main components:

1. The thermostat transceiver.  Here we’re using simple on/off control by implementing an interface to the Danfoss RX2 receiver.  We’ll respond to MQTT messages to allow services to issue commands to the boiler and, for monitoring, publish to an MQTT topic when messages are received over RF.
2. The heating controller.  This will be a Python daemon that works towards a temperature setpoint (for now this is a command-line parameter, but in future it will be hooked up to a scheduler) by monitoring the current temperature and deciding how to control the boiler to reach the target.
3. The temperature sensor.  I’m using an EmonTH for this that measures and publishes room temperature to MQTT.  There are some software tweaks I will discuss below.

# The heating controller

There are three modes of operation in reaching and maintaining temperature: significantly below setpoint, significantly above setpoint, and near the setpoint.  The first two cases are easy: the boiler should either be on or off.  Within the target zone we can modulate the boiler on/off to produce an average heating input to the room of the desired level: this is a type of pulse-width modulation (PWM) with long pulse durations in the order of minutes.

To decide what the duty-cycle (the fraction of the time boiler is turned on for in the full cycle) should be we need to determine the required heat input for the room using a control mechanism that can ‘find’ the correct value, since it will differ based on various factors including the temperature difference to outside and outside weather, building materials and insulation type, effectiveness of the radiators, losses through pipes, etc.

After trying out a couple of approaches based around incrementally increasing or decreasing the PWM duty-cycle according to the current “error” (difference between target and actual temperature), I learned that using a PID controller is a common approach that can be effective given some tuning.  This computes a control variable (in our case the PWM duty cycle) given a process variable (the current temperature), and a setpoint (the target temperature).  The output u at time t is given by:

$u(t) = K_pe(t) + K_i\int_0^t e(\tau) d\tau - K_d\frac{de(t)}{dt}$

The PID output combines the error (difference between current value and setpoint), the total error over time (the integral component, which allows the controller to adjust to the current conditions), and the differential (to damp excessive corrections), in an amount that is application specific using the coefficients Kp, Ki, and Kd.  An initial implementation is in pid.py in the boilerio repository.  It is currently quite basic and could be further refined.

For a more detailed overview of PID controllers, the Wikipedia page is a good place to start, and then Brett Beauregard’s excellent Improving the Beginner’s PID article series and accompanying library for the Arduino provide a good explanation of some of the common issues and solutions with basic PID controllers.

• The output is limited between 0.15 and 1.  Values below 0.15 are rounded down to 0 since such a small duty cycle doesn’t give the boiler chance to do anything useful.  Different limits may be suitable for different systems.
• The integral component is limited between -1 and +1 to avoid it becoming excessively large in either direction (since it can’t influence the output beyond those limits anyway).
• Unlike many applications of PID controllers where the process variable is actively moved in both directions, we can’t actively cool the room.  Therefore, we allow a negative integral that’s larger than one might in other systems, to accommodate the proportional term being too large.

# The simulator

Choosing appropriate coefficients for the PID controller and the efficiency of test/dev cycles were both important challenges.  At this time of year there is little opportunity to do real-world testing where the temperature difference between inside and out is very high, and to do such a test is both time-consuming and potentially wasteful of energy.  Instead, I decided to write a simple simulator, sim.py, to help with the majority of the debugging and tuning.

There are various tools online for calculating heat loss in your home that take into account the building materials, insulation, windows, ventilation, etc.  To estimate heat loss through conduction they look at loss through each building element $Q=UA(T_i-T_O)$; there is then the heat loss through ventilation to add in.

We use an extremely simple model that is sufficient to achieve the goals described above. Firstly we combine the U and A terms in the heat loss formula and assume an average across all building elements.  We assume that in each time increment, some fraction of the heat will be lost to the outside and some heat will be gained through transfer from the radiators, each with different efficiencies and therefore coefficients, without considering ventilation separately.  The radiator temperature itself is assumed to increase and decrease linearly over a ramp-up and ramp-down time when heating demand is indicated or ceases.

We inject a fake Boiler class that updates the model parameters rather than actually sending commands to the real system, allowing the model to interact with the controller.  The code is careful to only get the current time in one place and pass it as a parameter, to make mocking the passage of time easier.

To find a reasonable value for the heat-loss coefficient, I grabbed some real data from my temperature logs and used scipy to do a curve fit.  Then, keeping that value constant I did a similar exercise to determine the coefficient for heat transfer from the radiator in the room.  These values are obviously very rough; different time periods produced different results as the conditions at the time weren’t known (doors opened/closed, etc.).

# Interfacing the boiler to MQTT

The boilerio repository includes a daemon, boiler_to_mqtt.py, that will interface with a serial port using the protocol implemented in the previous two articles.  This, like the other tools, uses a config file to specify the location of the MQTT broker and the topic names to use.

RF messages sent and received are published to the topic specified by info_basetopic in the config file.  The published payload contains a JSON message with keys “direction” which is ISSUE or RECV, and “cmd”, which is the command issued or received (ON, OFF, or LEARN).  An example payload might be:

{"thermostat": "0x1234", "cmd": "ON", "direction": "RECV"}

Clients can issue commands to the boiler by publishing to the topic specified as the heating_demand_topic in the configuration file.  The script expects a JSON payload consisting of an object containing two values: the command (“O” for On, “X” for Off, and “L” for Learn), and the thermostat ID as an integer.  A sample payload might be:

{"command": "X", "thermostat": 23123}

See the README.md for more information on using the boiler_to_mqtt.py script if you are using a Danfoss receiver.  Alternatively, you can still use the temperature management code but replace this script with something that can control whatever receiver you are using.

# Temperature input

There are several options for temperature input: originally I had put together my own temperature ‘transmitter’ using an AVR microprocessor, a Dallas Instruments DS18B20 and an XBee radio (Sparkfun have a guide on the XBee).  If you go down that route be sure to get the right XBee hardware since v1 and v2 are not compatible.  I also had issues with an Arduino shield I bought, though breadboarding with Sparkfun’s XBee breakout worked fine.

I have since switched to using the excellent emonTH v2 from OpenEnergyMonitor.  These have a simpler RF69 radio, which is all that’s needed (and handily is the same one that supports interfacing with the Danfoss receiver), come pre-assembled, have a lower-power sensor that can also record humidity, and are battery powered.  The hardware design and software are open-source.

I did choose to make some modifications to the emonTH and emonhub software. For the emonTH I increased the resolution of the temperature readings, which required a number of updates across the stack:

• Support for setting the resolution in the library for the SI7021 sensor;
• Setting the SI7021 resolution during emonTH startup and reporting hundredths rather than tenths of a degree over RF, which required reprogramming the emonTH using a USB-to-UART adapter;
• Modifying the emonhub configuration to accommodate the change of packet format.

Increasing the resolution of the SI7021 sensor readings will also increase the time taken to acquire those readings, and therefore the overall power consumption, so expect batteries to run out quicker.  That being said, the OEM project estimate years of battery life from the default configuration so even at a quarter of that it would still be acceptable to me since I’m using rechargeable batteries anyway.

I also modified the format emonhub uses to post data to MQTT rather than using the pre-existing options of either a single message with a series of values whose order is significant in determining their meaning (the “rx” format), or one message per reading (e.g. to topics like emonth/temperature, emonth/humidity) where the grouping of the messages cannot be reconstructed.  My branch of emonhub posts a single MQTT message that has a JSON payload with the group of readings (temperature, humidity, battery voltage, etc.) that were taken simultaneously.  This is not strictly necessary but was helpful for other projects.

The modified emonhub, emonTH, and SI7021 code are available from github.

# Real-world testing

I have used this code to control the real boiler a number of times, mostly with overnight tests.  With the weather getting warmer, I’ve not been able to get a feel for how it works when it’s really cold outside but, in the situations I’ve used it so far, it seems to have worked well.  Generally it maintains the temperature to within ±0.2ºC of the setpoint, which I consider to be a success.

# Next steps

The upcoming good weather will surely slow progress but there is plenty that can still be done: three possible areas to investigate next are:

1. Power measurement.  It would be useful to read gas usage automatically to better understand how efficiently gas is being used and what effect change have on this.
2. Scheduling.  This isn’t really usable as control requires ssh and command-line knowledge.
3. More advanced integration with the boiler.  Monitoring and setting parameters such as target, supply, and return water temperatures and burner on/off.

Hope you found this interesting and/or useful!