HVAC systems can be very elaborate. But with some research, it’s surprisingly easy to learn how to replace a thermostat with a smart, programmable Raspberry Pi thermostat. Even basic, student-focused electronic kits contain all the parts necessary to control the heat in a house.
This post will focus on the Arduino Uno which controls the three zones, as well as the Node RED code which makes it smart.
An early prototype, showing the propane in the air (safety feature) and temperature in the boiler room itself.
Replacement Thermostat Design
Every HVAC system is a bit different…
Not just electronically, but because every climate is a bit different. Here in the foothills surrounding Denver, coldis the enemy. But heating a home with propaneis expensive. It’s also slow: the radiant heat comes from hot water running through pipes in heaters near the floorboards.
The three zones of the house are labeled in the boiler room, where the hot water flows to the radiant heaters in each zone.
The first thing I noticed when examining the existing three-zone heating system was that the temperature readings were… imprecise. Each of the three old-school Honeywell wall-mounted thermostats had only the most rudimentary, mechanical temperature sensor. They were also placed centrally in the house. However, what we’re most concerned with is the temperature along the walls. This is where the pipes are, which we need to protect from freezing.
Two-Wire Thermostats
Thankfully, the electrical design of the HVAC system was very simple. When I removed the three thermostats, each of the three zones had only a red and white wire (a.k.a., a “two-wire” thermostat). In this case, each of the three control units is just a glorified on/off switch. Using a 120 VAC relay switch is all it takes to toggle the heat for each zone. When the red/white wires are connected, the motors (pictured above) open the valve to the zone so that hot water flows to the radiators.
I decided to centralize the HVAC control, using a single Arduino Uno to control each of the three zones from the boiler room itself. This eliminated the need for the three separate control units in the house by instead running the wires directly to a wall-mounted unit, behind the hot water tank.
Another reason for this design was that it allowed me to build in a closed-loop safety system in the Arduino itself. The Arduino Uno itself has no internet connection and is backed up by a battery (see below). Even without its USB connection, it is programmed to use its own temperature readings and ensure a minimum temperature of 60 F. The battery backup lasts up to 36 hours, based upon my testing.
But that’s enough theory.
On to the practice.
Installing a Smart Raspberry Pi Thermostat
At the core of the design is an Arduino Uno connected to a DHT-11 temperature/humidity sensor, a MQ-2 gas sensor, and a few relays. I cover the basics of this design in the post on building an Arduino temperature sensor that communicates via USB. The core Arduino Uno thermostat could be built for under $100 by choosing only the necessary parts (but sensor kits are more fun, as they include sensors and parts that may be used on other projects):
If you're Arduino, most people start with the Uno or Nano models. If WiFi is desired, check out the ESP32 or ESP8266.
The following are affiliate links to other parts I used in this project. I never link to a product that I have not personally used.
No products found.
I’m also not the first person to build their own thermostat. I took inspiration from this smart Arduino thermostat, as well as this retro thermostat design. As you can see, I stuffed a few more parts into my design. The gas sensor in particular was meant to detect any propane leak or fire that might happen in the boiler room. I also stuck to the closed-loop principle from above, so that the core of the system requires nothing but power to protect the house.
While it may look complicated, it’s really just a bunch of simple examples stapled together. I also used the same easing functions from the temperature sensor project in order to smooth out the readings from the sensors.
I’m including the basic schematic and source code below, though this should not be treated as a “copy-and-paste” solution. Rather, it’s a starting point you might consider adapting into your own solution:
Pin 7: DHT-11 data
Pin 8: Rotary Button
Pin 9: MQ-2
Pins 5-6, 10-13: LCD Screen
/****************************************************************
GLOBAL CONFIGURATION
****************************************************************/
#define NUM_ZONES 3
bool zone_states[NUM_ZONES];
char zone_names[NUM_ZONES][16] = { "Upstairs", "Bedroom", "Downstairs" };
#define NUM_SENSORS 7
#define LCD_COLS 16
#define LCD_ROWS 2
// Under this temperature, all modes are turned on:
#define SAFETY_TEMP 60
int lcdSensorsAt = 0;
#define SENSOR_UPDATE_MS 10000
/****************************************************************
Sensors
****************************************************************/
#include "DHT.h"
#include
#include "EasingLib.h" // Smoothing algorithms.
#include
StaticJsonDocument<256> sensor_json_doc;
class Sensor {
private:
Easing _easing;
float _value;
public:
char name[16];
char prefix[10];
char unit[3];
// Returns true if value changed.
bool setValue(float value) {
_easing.SetSetpoint(value);
float ev = _easing.GetValue();
if (abs(ev - _value) < 0.1) return false;
_value = ev;
sensor_json_doc[name] = ev;
return true;
}
float getValue() {
return _easing.GetValue();
}
Sensor(const char* sensor_name, const char* sensor_unit) {
_easing = Easing();
_value = 0.0;
strcpy(name, sensor_name);
strcpy(unit, sensor_unit);
}
};
#define PIN_DHT11 7
#define PIN_MQ2 9
#define IMPERIAL true
Sensor sensors[NUM_SENSORS] = {
#if IMPERIAL
Sensor("Temperature", "F"),
#else
Sensor("Temperature", "C"),
#endif
Sensor("Humidity", "%"),
Sensor("H2", "ppm"),
Sensor("LPG", "ppm"),
Sensor("CO", "ppm"),
Sensor("Alcohol", "ppm"),
Sensor("Propane", "ppm")
};
DHT dht(PIN_DHT11, DHT11);
MQUnifiedsensor mq2(PIN_MQ2, 2);
// Returns true if any value changed.
bool updateSensors() {
mq2.update();
bool valueChanged = false;
valueChanged = sensors[0].setValue(dht.readTemperature(IMPERIAL)) || valueChanged;
valueChanged = sensors[1].setValue(dht.readHumidity()) || valueChanged;
for (int x=2; x 1;
}
/****************************************************************
LCD
****************************************************************/
#include
// initialize the library with the numbers of the interface pins
LiquidCrystal lcd(5, 6, 10, 11, 12, 13);
void lcdPrintSensor(int sensorIdx) {
sensorIdx = sensorIdx % NUM_SENSORS;
char txt[LCD_COLS];
char val[6];
sprintf(val, "%d", (int)sensors[sensorIdx].getValue());
lcd.print(sensors[sensorIdx].name);
int numStartAt = LCD_COLS - strlen(val) - strlen(sensors[sensorIdx].unit);
for (int x=strlen(sensors[sensorIdx].name); x= NUM_SENSORS) lcdSensorsAt = 0;
lcd.display();
return true;
}
/****************************************************************
HVAC
****************************************************************/
bool isSafetyModeEngaged() {
return sensors[0].getValue() <= SAFETY_TEMP;
}
void setHeating(int zone, bool heatOn) {
bool safety = isSafetyModeEngaged();
int pin = zone + 2;
char* zone_name = zone_names[zone];
bool realHeatOn = heatOn || safety;
if (sensor_json_doc[zone_name].as() == realHeatOn)
return;
pinMode(pin, realHeatOn ? HIGH : LOW);
digitalWrite(LED_BUILTIN, safety ? HIGH : LOW);
sensor_json_doc[zone_name] = realHeatOn;
zone_states[zone] = heatOn;
}
// {"bedroom":true}
bool readSerialCommand() {
while (Serial.available() && Serial.peek() != '{')
Serial.read(); // Wait for JSON-start.
if (!Serial.available()) return false;
StaticJsonDocument<128> doc;
auto error = deserializeJson(doc, Serial);
if (error) {
Serial.println(error.c_str());
// Chomp past the entire string (reset).
while (Serial.available()) Serial.read();
return false;
}
JsonObject root = doc.as(); // get the root object
for (JsonObject::iterator it=root.begin(); it!=root.end(); ++it) {
const char* key = it->key().c_str();
int zone = -1;
for (int x=0; x= NUM_ZONES) {
Serial.println("Invalid Heater");
continue;
}
bool val = it->value().as();
setHeating(zone, val);
serializeJson(sensor_json_doc, Serial);
Serial.println("");
}
return true;
}
/****************************************************************
Main
****************************************************************/
void setup() {
Serial.begin(9600);
Serial.setTimeout(1000);
dht.begin();
pinMode(PIN_BTN, INPUT);
pinMode(PIN_MQ2, INPUT);
pinMode(PIN_DHT11, INPUT);
lcd.begin(LCD_COLS, LCD_ROWS);
lcd.clear();
updateSensors();
for (int x=0; x= SENSOR_UPDATE_MS;
bool displayChanged = readSerialCommand ();
displayChanged = updateInput() || displayChanged;
if (updatingSensors) {
displayChanged = updateSensors() || displayChanged;
// Give everything some time to start up and collect initial readings.
if (areSensorsLoaded()) {
for (int x=0; x
This code outputs data as JSON to the USB port, just like the other sensor project. The relevant configuration block is:
The data is written via USB every 10 seconds (SENSOR_UPDATE_MS). It also can be controlled via the serial port, setting the on/off state manually. For example, the JSON payload {"bedroom": true}, written via the USB port, will turn on the Bedroom zone.
With the basic code in place, I began to test the reliability…
Reliability Tests
I brought my laptop over to the boiler room and left it connected to the Arduino Uno, so that I could use the Arduino IDE in order to manually test the zones via the Serial Debugger. I ran standard resiliency tests, like cutting out the power and then reconnecting the Arduino Uno randomly.
One of the motors which controls the valves for each zone.
I watched the relays turn on and off and could hear the hot water flowing to each zone. Just to be sure everything was reliable, I continued to test manually in this way for a couple days.
At first, everything seemed fine. Slowly I began to notice that one of the rooms was never getting cold, though. I took a look at the motor for the zone (right) and noticed that it was stuck on, even when the relay indicated it should have been off. I soon discovered that the gear mechanism itself was stuck. Even after cleaning and oiling it, it proved unreliable, often failing to turn on the zone. In the end, I ended up buying a replacement motor for $25. Since then, each zone has turned on and off flawlessly.
DIY Programmable Thermostat
To make the thermostat smart, two more things were required:
Temperature readings from each zone.
A programmable controller.
For temperature readings, I used a combination of other sensors fed into Home Assistant. For example, the downstairs temperature is taken as an average of two sensors in different rooms using the following yaml configuration in Home Assistant:
If it looks complicated, that’s because I added some bells and whistles to the Raspberry Pi thermostat. There are some extra sensor variables being exported to Home Assistant, for better monitoring (below). There’s also an “override” feature, so that changes made to the target temperature on the Home Assistant side (e.g., though the Lovelace UI) temporarily take priority over the pre-programmed schedule.
Here’s the full Node RED JSON flow and home assistant package (packages/themostat.yaml). Again, these should be treated as code samples for adaptation rather than copy-and-paste solutions (your HVAC system is certainly different than mine). That said, the code is made to be reusable. If you have any troubles, I’m happy to help in the comments below (or contact me directly).
I then use the Simple Thermostat card to show each of the three zones in the Home Assistant UI:
The Home Assistant cards.
Monitoring & Optimizing
Home Assistant does have built-in history for climate controls. However, I thought I might do a little better with Grafana and Prometheus. Using the design shown there, we can track the HVAC performance over time. Some zones struggled to keep up with the heating demands:
Radiators are slow to react, and some zones required further optimization.
To improve this, we began adding electric heaters to those rooms which required it. This gave us an opportunity to offset the cost of propane with (much cheaper) electricity. The same Home Assistant switches that controlled the radiators, above, were reconfigured to also turn on the electric heaters for that zone.
It took some fiddling to get the temperature readings right, as well. Eventually we ended up with two sensors in each zone. The DHT-11 attached to the central thermostat itself is only accurate enough for a fallback sensor. Elsewhere, I used the SHT31-D sensor — which is much more accurate and precise.
Adding Accuracy
To really dial-in the HVAC system, more sensors are required.
The additional SHT31-D sensors run about $15 each, with ESP8266s costing about $4 each… so each additional temperature+humidity reading costs about $20 total. The following post breaks down the process:
In many cases, cheap ambient air temperature sensors are fine. However, it's possible to save a good deal of money on electricity and gas through the use of more accurate sensors. This post breaks down the cheapest, most accurate temperature sensor build I've found so far.
By day, I develop a piano teaching app. This site began as a place to document DIY projects. It's grown into a collection of IOT projects, technical tutorials, and how-to guides. Read more about this site...
Types of Waterproof Cases IP67 enclosures and connectors are the most common and versatile for outdoor usage. The IP67 rating means that the waterproof case can handle up to thirty minutes in partially submerged water (read about the IP ratings)...
Building a DIY Raspberry Pi security camera is much easier than it might sound thanks to open-source security camera software. We use several such cameras placed around the house, as part of our DIY CCTV security camera system. One such camera is...
No silly gimmicks. This collection of home automation ideas will actually make your home more enjoyable for you and your guests. I've personally implemented many of the ideas in this list. These all came from our DIY Home Automation project...
Dashcams (video cameras in cars) are a great security and safety feature. As with the rest of the vanlife IOT, I built my own DIY dashcam that has some unique features — like motion detection and automatic recording. On its surface, this is a post...
A DIY smart doorbell with a built-in camera, microphone, and speaker. This steampunk-themed design integrates with home assistant and our multi-room audio system to communicate with the rest of our DIY smart home. Rather than buying a Ring Doorbell...
Running a custom router gives unprecedented insight into everything happening in a network. Building your own router with a a Raspberry Pi may be a little daunting, but it's surprisingly easy and rewarding to do... and the benefits are tremendous...