Putting an ESP32 between my thermostat and the boiler
Our boiler and its thermostat talk to each other using OpenTherm, an open protocol for heating controls. Rather than just switching the boiler on and off, the thermostat can ask for a particular flow temperature (how hot the water going round the radiators should be), and the boiler reports back its own temperatures, pressure and flame state. It’s a surprisingly chatty relationship, and until now Home Assistant hasn’t been invited.
I wanted in, but without ripping out the thermostat. I was after two things. The first was more flexibility over the heating schedule than the thermostat gives me. The second was some very basic weather compensation, which means sending cooler water round the radiators when it’s mild outside. A condensing boiler is at its most efficient when the water coming back from the radiators is cool enough for the steam in its exhaust to condense, recovering heat that would otherwise go up the flue. So the idea is to keep the boiler in condensing mode unless it’s really cold outside.
The way to do that is a gateway: a box that sits in the middle of the two wires, pretending to be a boiler to the thermostat and a thermostat to the boiler. Most of the time it just passes messages along. But because every message goes through it, it can read everything and, when it wants to, rewrite what gets said.
How OpenTherm talks
The thermostat is the master. About once a second it sends the boiler a 32-bit frame: a message type, a data ID saying what it’s about, and 16 bits of data. The boiler always answers. The IDs that matter most here are:
- 0, status: a byte of flags from the thermostat (central heating enabled, hot water enabled and so on), and a byte back from the boiler (flame on, heating active, fault);
- 1, control setpoint: the flow temperature the thermostat wants;
- 16 and 24: the room setpoint and the room temperature, which the thermostat reports because it has the thermometer.
Electrically it’s a bit odd. The thermostat signals by changing how much current it draws, and the boiler answers by changing the voltage. So a gateway needs two different interfaces, one facing each way.
Starting from someone else’s gateway
I didn’t start from nothing. wichers/esphome-opentherm is a port of Ihor Melnyk’s OpenTherm library to ESPHome that already works as a gateway. It relays messages and publishes the boiler’s readings to Home Assistant. I cloned it in April 2022.
For the hardware I used a pair of Igor Ybema’s shields for Wemos/Lolin boards, from Tindie. A gateway needs both halves: the master shield plays the thermostat to the boiler, and the slave shield plays the boiler to the real thermostat.
Getting the hardware to behave took most of 2022. The gateway itself lived on a Lolin S2 mini the whole time, from May onwards. In August I also set up a Wemos D1 mini on the bench, away from the boiler and thermostat, to try to recreate the problems on their own. There I ran jpraus’s self-test, which loops the thermostat and boiler sides into each other and checks each can hear the other, and it proved the original hardware was faulty, so it had to be replaced. I also tried Ihor Melnyk’s demo sketch, which cuts out the thermostat and talks to the boiler directly. In October I moved the S2 mini to ESPHome’s ESP-IDF framework, which brought problems of its own, and in January 2023 it went back to the Arduino framework.
Along the way, the boiler told me a few things about itself. It has a maximum capacity of 22 kW, it can’t modulate below 28% of that, and it’ll accept a maximum flow setpoint anywhere between 20 and 75 °C.
What I added
My changes come to about 290 lines on top of the original, and fall into two halves.
The first half is more to look at. Home Assistant now also gets the boiler pressure, the modulation level, the flow setpoint actually being used, the hot-water setpoint and the room temperature, all with proper units and device classes, so they graph nicely. Most sensors now only publish when their value changes, rather than on every message. The room temperature goes through a three-sample median filter, because every so often the thermostat’s room temperature glitches to 0 °C. A heating controller that believes it’s freezing indoors would be very keen indeed.
The second half is taking over, and it’s where the interesting logic lives.
Who’s in charge
The gateway shows up in Home Assistant as a climate entity with two modes:
- Auto: the thermostat is in charge. The gateway passes everything through untouched, and Home Assistant’s target temperature just follows the thermostat’s own setpoint.
- Heat: the gateway is in charge. It uses the thermostat’s room temperature, but ignores what the thermostat wants and makes its own decisions.
In Heat mode, it rewrites two messages on their way to the boiler. In the status message it sets the central-heating-enable bit itself, using a simple hysteresis rule: heat on once the room falls to 0.2 °C below the target, off once it rises to 0.2 °C above it, and no change in between. That gap stops the boiler cycling on and off every time the temperature wobbles. In the control-setpoint message it replaces the thermostat’s flow temperature with its own: 55 °C by default, or whatever I set with a “CH water override” number in Home Assistant.
Then comes the sneaky bit. The boiler’s reply to the status message includes a copy of the flags it was sent. If the thermostat saw my flags coming back instead of its own, it might get confused. So, on the way back, the gateway swaps the thermostat’s original flags back in. The upstream code’s comments already described doing this, but nothing actually did it yet. The thermostat carries on in blissful ignorance, sure that it’s still running the show.
A few more guard rails:
- Targets below 5 °C get clamped to 5 °C.
- The Away preset drops the target to 5 °C: frost mode, which keeps the pipes from freezing and not much else.
- The climate entity remembers its mode across reboots, and starts in Auto if it has nothing to remember.
Have a play. The logic below is the gateway’s; the house and the “pretend thermostat” are made up:
Safe mode
When a homemade box sits between the thermostat and the boiler, it’s only prudent to have a fallback. So there’s a safe mode switch. While it’s on, the gateway is just a length of wire: nothing gets rewritten, whatever mode it’s in. And it switches itself on automatically:
- at boot, so a freshly flashed gateway starts out harmless;
- whenever the connection to Home Assistant drops.
It turns off again when Home Assistant reconnects. So if Home Assistant falls over, the thermostat quietly takes back control and the house stays warm. The board’s LED shows when safe mode is on.
Where it stands
The latest firmware went onto the S2 mini on 26 January 2023, built with ESPHome 2022.10.2, and it’s running our heating. The automatic control works well.
OpenTherm is designed so the boiler can modulate down gently to hold the setpoint, rather than cycling on and off. Our boiler is pretty noisy, though, so we’d rather it ran for as short a time as possible to hold the temperature. The simple on-off rule suits us better than the boiler’s own idea of good manners.
The part I like best is Away. It’s hooked up to the alarm controls, so turning off the heating schedule isn’t a separate job when we go out. And when we’re heading home, we can turn the heating back on remotely, from the Home Assistant app or through Android Auto in the car.