Arduino Powered 3-zone Thermostat
by hbomb9000 in Circuits > Arduino
61895 Views, 313 Favorites, 0 Comments
Arduino Powered 3-zone Thermostat
I was looking at all those swoopy-zoomy internet connected and controllable thermostats. "Self," I told myself, "we should make one of those." He didn't seem all that excited about that - I mean, what do we know about hardware, firmware, electronics or HVAC? Well, now's as good as any to figure it out.
So without further ado, here's my Instructable on my endeavors building a functioning 3-zone thermostat. (internet connectedness coming later)
Procure Components
This build requires lots of different things I didn't have, and as such I had to move forward by using the internet! I researched (a LOT of research) all the different components people are using and have success within the realm of what I was trying to do. That meant I needed a bunch (or at least some) of the following:
- Arduino Uno - I know there's a way to build your own, but I'm trying to keep things simple - at least initially. Besides, I need to be able to program the ATMega chip, right?
- Temperature Sensors - DS18B20 Temperature Sensors was what I decided on. There was another one that also could handle humidity, but again we're erring on the side of simpler.
- 16x2 Character LCD display - Hitachi compatible.
- LCD Keypad Arduino shield
- miscellaneous wire leads, connectors, and resistors.
Once all my parts arrived, I started looking at hooking things together on a small, local scale.
Start Programming the Arduino
I am not new to programming, so moving into the programming space of the Arduino was not some imposing task. I went through a handful of tutorials (turning on LED's and such) to get the syntax and some of the conventions down, and I was off.
The first thing I did was document what I wanted to do IN THE CODE. After that, I ran a thumbnail sketch of the program I wanted to develop - that is, I set up the inputs and outputs, sketched out some functions which I could use to open and close the dampers, turn the furnace on or off, those kinds of things. Great, I had my initial program sketched out. Of course it wasn't ready, wouldn't compile, anything like that.
During this process, I was getting pretty frustrated with the poor development environment of the Arduino IDE. A quick search later, I had found an add on for SublimeText called 'Stino'. Stino did everything I wanted it to and was a much more mature dev environment. I switched over immediately.
Pull the Program Together
The first step in making the program actually work was to get the temp sensors working. They are at the heart of the thermostat, and without them, the furnace doesn't know when to turn on or off.
Time to introduce a couple of libraries.
The temperature sensor is pretty cool in that it uses a single wire to send data back to the Arduino. What's more, each sensor has an address, a unique number which identifies the sensor. This is really exciting because it means you can have multiple sensors sending their inputs into the same pin on the Arduino (a big deal when you start to look at what it takes to plug in the 16x2 LCD screen. Pin space is at a premium!).
In order to do this, we'll call on a couple of libraries that already exist - no need to reinvent the wheel. The two libraries are OneWire and DallasTemperature. Both are freely available and will come in useful as we move forward. Being unfamiliar with either one of these libraries, I again referred to the interwebs and found a pretty interesting tutorial on just that thing. All I had to do was modify it to fit my needs.
In going through the hacktronics tutorial, it was mentioned that the DS18B20 Temp sensors are addressable - that is, that they are represented by a specific address. This is what allows for multiple sensors to ride into the Arduino on the same pin. So as we go forward, we have to find those temperature sensor addresses. Hacktronics offers another tutorial which outlines how to find those addresses, as well.
OneWire library
DallasTemperature library
Code so far: (a lot of stuff is commented out because there are still a lot of suppositions and unknowns, but I THINK this is how the code will eventually play out. We'll see)
/* This is the application that I will be using to control the furnace. The concept is this: I have 3 zones I will be monitoring. Each zone will have at least one zone damper which will be controlled by the arduino application. Each zone will have a single thermometer which will report back to the controller. Depending on the thermometer reading and the thermostat setting, the dampers will open or shut and the central furnace will be turned on. Once an acceptable temperature has been reached in all zones, the furnace will turn off. In addition to general heating cycles, the system will be programmable. At this time, however, the programming cycle will be handled here, not via the thermostat. IMPORTANT TEST CASES: (to be added as I think of them/come across them * Need to ensure that the furnace is always off if all 3 dampers are closed. */ #include //This is a library needed for the LCD display #include //This is a library needed for the thermometer readings #include //This is a temperature library #include // Connections: // rs (LCD pin 4) to Arduino pin 12 // rw (LCD pin 5) to Arduino pin 11 // enable (LCD pin 6) to Arduino pin 10 // LCD pin 15 to Arduino pin 13 // LCD pins d4, d5, d6, d7 to Arduino pins 5, 4, 3, 2 LiquidCrystal lcd(12, 11, 10, 5, 4, 3, 2); const int backlight = 13; //controls backlight #define THERM_BUS 6 // all temperature readings come in via this single pin. OneWire oneWire(THERM_BUS); DallasTemperature sensors(&oneWire); DeviceAddress zone1Therm = {0x28, 0x48, 0x39, 0x11, 0x04, 0x00, 0x00, 0x94}; // START PROGRAMMING // Times are 6 AM, 8 AM, 3PM, and 9PM int* PROGRAMMED_VALUES[7]; int SUNDAY_VALUES[4] = {67, 67, 67, 60}; int MONDAY_VALUES[4] = {67, 67, 67, 60}; int TUESDAY_VALUES[4] = {67, 67, 67, 60}; int WEDNESDAY_VALUES[4] = {67, 67, 67, 60}; int THURSDAY_VALUES[4] = {67, 67, 67, 60}; int FRIDAY_VALUES[4] = {67, 67, 67, 60}; int SATURDAY_VALUES[4] = {67, 67, 67, 60}; //END PROGRAMMING float zone1Temp = 0; float zone2Temp = 0; float zone3Temp = 0; boolean isOverrideHeld = false; boolean isOverridden = false; boolean furnaceState = false; //with furnaceState, false means it's currently off, true means its currently on. int overrideValue = 0; const int furnacePin = 1; void setup() { Serial.begin(9600); // This connects the arduino to the RPi sensors.begin(); sensors.setResolution(zone1Therm, 10); //SETUP OUTPUTS pinMode(furnacePin, OUTPUT); //SETTING UP THE WEEKLY PROGRAM PROGRAMMED_VALUES[0] = SUNDAY_VALUES; PROGRAMMED_VALUES[1] = MONDAY_VALUES; PROGRAMMED_VALUES[2] = TUESDAY_VALUES; PROGRAMMED_VALUES[3] = WEDNESDAY_VALUES; PROGRAMMED_VALUES[4] = THURSDAY_VALUES; PROGRAMMED_VALUES[5] = FRIDAY_VALUES; PROGRAMMED_VALUES[6] = SATURDAY_VALUES; } // this runs the continual loop void loop() { delay(1000); log("000", "Getting Temperature"); sensors.requestTemperatures(); zone1Temp = getTemperature(zone1Therm); log("001", (String)(int)zone1Temp); // if(c1 || c2 || c3) { // if(!furnaceState) { // startFurnace(); // } // TODO - DETERMINE THE CORRECT PINS FOR THE DAMPERS. WILL THE DAMPERS BE DIGITAL OR ANALOG? // if(c1) { // openDamper(1); // } // if(c2) { // openDamper(2); // } // if(c3) { // openDamper(3); // } // } // else { // if(furnaceState){ // stopFurnace(); // } // } // if(!c1) { // closeDamper(damper1Pin); // } // if(!c2) { // closeDamper(damper2Pin); // } // if(!c3) { // closeDamper(damper3Pin); // } } float getTemperature(DeviceAddress deviceAddress) { float tempC = sensors.getTempC(deviceAddress); //Serial.println(tempC); if(tempC == -127){ Serial.println("ERROR getting temperature."); } return DallasTemperature::toFahrenheit(tempC); } //convenience function. Handles all the boilerplate for writing to the LCDScreen void toScreen(String line1Value, String line2Value) { lcd.begin(16,2); lcd.clear(); lcd.setCursor(0,0); lcd.print(line1Value); lcd.setCursor(0,1); lcd.print(line2Value); } int getTime() { return -1; } void openDamper(int damper) { // if(damper == damper1Pin || damper == damper2Pin || damper == damper3Pin) { // digitalWrite(damper, HIGH); // } else { // log("Problem opening damper. Damper not found. Was expecting 1, 2, or 3. Got " + damper); // } } void closeDamper(int damper) { // if(damper == damper1Pin || damper == damper2Pin || damper == damper3Pin) { // digitalWrite(damper, LOW); // } else { // log("Problem closing damper. Damper not found. Was expecting 1, 2, or 3. Got " + damper); // } } //function for logging messages to the console. //Eventually plan to write to logfile on RPi void log(String code, String message) { Serial.println(code + ": " + message); } void recordData(String type, String object, String value) { // TODO - output to RPi database } void startFurnace(){ //Send appropriate signal to the furnace to start up if(furnaceState != 0) { digitalWrite(furnacePin, HIGH); } } void stopFurnace(){ //Send appropriate signal to the furnace to stop if(furnaceState == 1){ digitalWrite(furnacePin, LOW); } }
First Steps in Getting Things Working
So now I have my breadboard, a temperature sensor, an Arduino, and have the program set up so I can test out and see if my temperature sensor is reading correctly. Now to wire everything up and see what happens!
Hooray! We have data! Everything's comin' up Millhouse!
Okay, so first major technical hurdle overcome - we are now receiving data from the temperature sensor. Next, up, lets add the second and third sensors in line with the first, make some slight changes to the code to handle the two new sensors, and see what happens.
... DeviceAddress zone1Therm = {0x28, 0x48, 0x39, 0x11, 0x04, 0x00, 0x00, 0x94}; DeviceAddress zone2Therm = {0x28, 0xF2, 0x1F, 0x11, 0x04, 0x00, 0x00, 0x38}; DeviceAddress zone3Therm = {0x28, 0xBA, 0xF3, 0x10, 0x04, 0x00, 0x00, 0x98}; ... void loop() { ... zone1Temp = getTemperature(zone1Therm); zone2Temp = getTemperature(zone2Therm); zone3Temp = getTemperature(zone3Therm); log("001", (String)(int)zone1Temp); log("001", (String)(int)zone2Temp); log("001", (String)(int)zone3Temp); ...}
Bingo!
Make Those Readings DO Something.
All right. I now have an Arduino. It's hooked up via jumper wires to three temperature sensors, all sitting cozily on my desk. I've programmed in a time and day sensitive schedule. Let's make it work.
Except...
The Arduino doesn't really have a clock in it - at least not a clock that remembers anything This would be fine EXCEPT we need to have this information in order to really make this work correctly. Back to the internet I traipse, looking for something which will give my Arduino a mind for time. Seconds later (thanks, Internet!), I hit pay-dirt. The DS1307 RT Clock was exactly what I was looking for. Click to order and it's on its way.
Install the Clock
Installing the clock is the next step. After waiting anxiously for its arrival, it finally came! A quick trip out to the shop for a little soldering (adding on the single row of pins needed to plug this baby into the breadboard), and I was ready to rock!
Now, I had never really done much on-board soldering before. This was 'uncharted' territory. Sure, I got the basic premise - get the pin/component you're trying to solder hot and the solder just flows onto, around, and through everything, making a nice solid connection. But I didn't have much practical application of stated principle. Time to change that.
I had a spare 16x2 LCD screen lying around, so I soldered a set of pins onto that, first. At first it was slow going... my soldering iron was not hot enough. After cranking that baby to about 9 (on the 11 scale), things started getting groovy. I soldered all the pins together in short order and was back in the house, and wiring the new clock into the circuit.
(For a detailed tutorial on how the clock is wired up, check this adafruit tutorial out.)
Add in the LCD
So with our clock and our temperature sensors working, let's get some output moving to the LCD controller.
The first part of this is determining what I want it to say. Space is somewhat limited since I only have a total of 32 characters across 2 lines, so the first question I had to answer was how do I display everything I need to display within those 32 characters.
Here are my final LCD readouts:
Zone1: ()
Mode:
Zone2: ()
Furnace:
Zone3: ()
Open: <1,2,3/NONE>
where T1 is temperature for zone 1, TT is target temperature, MODE is the current mode it's running (AWAKE, AWAY, HOME, SLEEP, OVERRIDE, or HOLD.
Open refers to which of the zones are currently open and receiving heat (if the furnace is running).
This was actually pretty straightforward. I have some figuring to do as far as a custom symbol is concerned, but it's pretty far down on my priority list.
<tt><tt><tt> /* This is the application that I will be using to control the furnace. The concept is this: I have 3 zones I will be monitoring. Each zone will have at least one zone damper which will be controlled by the arduino application. Each zone will have a single thermometer which will report back to the controller. Depending on the thermometer reading and the thermostat setting, the dampers will open or shut and the central furnace will be turned on. Once an acceptable temperature has been reached in all zones, the furnace will turn off. In addition to general heating cycles, the system will be programmable. At this time, however, the programming cycle will be handled here, not via the thermostat. IMPORTANT TEST CASES: (to be added as I think of them/come across them * Need to ensure that the furnace is always off if all 3 dampers are closed. * dampers should be open unless they specifically need to be closed. */ #include <LiquidCrystal.h> //This is a library needed for the LCD display #include <OneWire.h> //This is a library needed for the thermometer readings #include <DallasTemperature.h> //This is a temperature library #include <Wire.h> #include "RTClib.h" // Connections: // rs (LCD pin 4) to Arduino pin 12 // rw (LCD pin 5) to Arduino pin 11 // enable (LCD pin 6) to Arduino pin 10 // LCD pin 15 to Arduino pin 13 // LCD pins d4, d5, d6, d7 to Arduino pins 5, 4, 3, 2 LiquidCrystal lcd(12, 11, 10, 5, 4, 3, 2); byte deg[8] = { B01000, B10100, B01000, B00111, B00100, B00110, B00100, B00100 }; const int backlight = 13; //controls backlight #define THERM_BUS 6 // all temperature readings come in via this single pin. OneWire oneWire(THERM_BUS); DallasTemperature sensors(&oneWire); DeviceAddress zone1Therm = { 0x28, 0x48, 0x39, 0x11, 0x04, 0x00, 0x00, 0x94}; DeviceAddress zone2Therm = { 0x28, 0xF2, 0x1F, 0x11, 0x04, 0x00, 0x00, 0x38}; DeviceAddress zone3Therm = { 0x28, 0xBA, 0xF3, 0x10, 0x04, 0x00, 0x00, 0x98}; RTC_DS1307 RTC; // START PROGRAMMING // Times are 6 AM, 8 AM, 3PM, and 9PM int* PROGRAMMED_VALUES[7]; int SUNDAY_VALUES[4] = { 67, 67, 67, 60}; int MONDAY_VALUES[4] = { 67, 67, 67, 60}; int TUESDAY_VALUES[4] = { 67, 67, 67, 60}; int WEDNESDAY_VALUES[4] = { 67, 67, 67, 60}; int THURSDAY_VALUES[4] = { 67, 67, 67, 60}; int FRIDAY_VALUES[4] = { 67, 67, 90, 75}; int SATURDAY_VALUES[4] = { 67, 67, 67, 60}; //END PROGRAMMING float zone1Temp = 0; float zone2Temp = 0; float zone3Temp = 0; boolean z1Check = 0; boolean z2Check = 0; boolean z3Check = 0; boolean isOverrideHeld = false; boolean isOverridden = false; boolean furnaceState = false; //with furnaceState, false means it's currently off, true means its currently on. int overrideValue = 0; const int furnacePin = 1; int zoneCounter = 0; void setup() { Serial.begin(57600); // This connects the arduino to the computer //TEMPERATURE SENSOR SETUP sensors.begin(); sensors.setResolution(zone1Therm, 10); lcd.createChar(0, deg); // RTC SETUP. Wire.begin(); RTC.begin(); if(!RTC.isrunning()){ log("ERROR", "RTC is NOT running!"); //RTC.adjust(DateTime(__DATE__, __TIME__)); //THIS ONLY NEEDS TO BE UNCOMMENTED IF YOU ARE SETTING UP YOUR RTC FOR THE FIRST TIME! ONCE IT IS SET, THIS MUST BE DISABLED! } //SETUP OUTPUTS pinMode(furnacePin, OUTPUT); pinMode(backlight, OUTPUT); digitalWrite(backlight, HIGH); //SETTING UP THE WEEKLY PROGRAM PROGRAMMED_VALUES[0] = SUNDAY_VALUES; PROGRAMMED_VALUES[1] = MONDAY_VALUES; PROGRAMMED_VALUES[2] = TUESDAY_VALUES; PROGRAMMED_VALUES[3] = WEDNESDAY_VALUES; PROGRAMMED_VALUES[4] = THURSDAY_VALUES; PROGRAMMED_VALUES[5] = FRIDAY_VALUES; PROGRAMMED_VALUES[6] = SATURDAY_VALUES; } // this runs the continual loop void loop() { delay(3000); //DELAY CURRENTLY SET TO 10 SECONDS. WILL ADJUST AS NEEDED //DATE/TIME LOGIC log("TIME", "Loop initiated at " + getDateAndTime()); //TEMPERATURE LOGIC sensors.requestTemperatures(); zone1Temp = getTemperature(zone1Therm); zone2Temp = getTemperature(zone2Therm); zone3Temp = getTemperature(zone3Therm); logTemperatureData(); getScreenOutput(); z1Check = checkZoneTemp(zone1Temp); z2Check = checkZoneTemp(zone2Temp); z3Check = checkZoneTemp(zone3Temp); if(z1Check || z2Check || z3Check){ startFurnace(); } else { stopFurnace(); } if(z1Check && furnaceState) { closeDamper(1); } else { openDamper(1); } if(z2Check && furnaceState) { closeDamper(2); } else { openDamper(2); } if(z3Check && furnaceState) { closeDamper(3); } else { openDamper(3); } } boolean checkZoneTemp(int temperature) { int temp = getTargetTemperature(); if(temperature > temp){ return false; } return true; } void logTemperatureData() { log("Zone1Temp", (String)(int)zone1Temp); log("Zone2Temp", (String)(int)zone2Temp); log("Zone3Temp", (String)(int)zone3Temp); } void getScreenOutput() { zoneCounter++; if(zoneCounter == 1){ String val = "Zone1: " + (String)(int)zone1Temp; val += " (" + (String)getTargetTemperature(); val += (String)")"; toScreen(val, "Mode: " + getModeString()); } else if (zoneCounter == 2){ String val = "Zone2: " + (String)(int)zone2Temp; val += " (" + (String)getTargetTemperature(); val += (String)")"; toScreen(val, "Furnace: " + getFurnaceState()); } else { zoneCounter = 0; String val = "Zone3: " + (String)(int)zone3Temp; val += " (" + (String)getTargetTemperature(); val += (String)")"; toScreen(val, "Open: " + getActiveZonesForScreen()); } } int getTargetTemperature(){ int mode = getMode(); DateTime now = RTC.now(); int date = now.dayOfWeek(); int* values = { 0 }; log("info", (String)date); values = PROGRAMMED_VALUES[date]; log("info", "getting target temp: " + (String)values[mode]); return values[mode]; } String getActiveZonesForScreen(){ String retVal = ""; if(z1Check && !furnaceState){ retVal += "1,"; } if(z2Check && !furnaceState) { retVal += "2,"; } if(z3Check && !furnaceState){ retVal += "3"; } if(retVal.length() == 0) { retVal += "None"; } return retVal; } String getFurnaceState(){ if(furnaceState){ return "On"; } else { return "Off"; } } String getModeString() { int mode = getMode(); switch (mode) { case 0: return "Wake"; case 1: return "Away"; case 2: return "Home"; case 3: return "Sleep"; default: return "N/A"; } } int getMode() { DateTime now = RTC.now(); int hour = now.hour(); if(hour < 6 || hour >= 21) { return 3; } if(hour >= 6 && hour < 8){ return 0; } if(hour >=8 && hour < 15){ return 1; } if(hour >= 15 && hour < 21){ return 2; } } float getTemperature(DeviceAddress deviceAddress) { float tempC = sensors.getTempC(deviceAddress); //Serial.println(tempC); if(tempC == -127){ Serial.println("ERROR getting temperature."); } return DallasTemperature::toFahrenheit(tempC); } //convenience function. Handles all the boilerplate for writing to the LCDScreen void toScreen(String line1Value, String line2Value) { lcd.begin(16,2); lcd.clear(); lcd.setCursor(0,0); lcd.print(line1Value); lcd.setCursor(0,1); lcd.print(line2Value); } String getDateAndTime() { DateTime now = RTC.now(); return String(now.year()) + "/" + String(now.month()) + "/" + String(now.day()) + " " + String(now.hour()) + ":" + String(now.minute()) + ":" + String(now.second()); } void openDamper(int damper) { // if(damper == damper1Pin || damper == damper2Pin || damper == damper3Pin) { // digitalWrite(damper, HIGH); // } else { // log("Problem opening damper. Damper not found. Was expecting 1, 2, or 3. Got " + damper); // } } void closeDamper(int damper) { // if(damper == damper1Pin || damper == damper2Pin || damper == damper3Pin) { // digitalWrite(damper, LOW); // } else { // log("Problem closing damper. Damper not found. Was expecting 1, 2, or 3. Got " + damper); // } } //function for logging messages to the console. //Eventually plan to write to logfile on RPi void log(String code, String message) { Serial.println(code + ": " + message); } void recordData(String type, String object, String value) { // TODO - output to RPi database } void startFurnace(){ //Send appropriate signal to the furnace to start up if(!furnaceState) { furnaceState = !furnaceState; log("INFO", "STARTING FURNACE"); digitalWrite(furnacePin, HIGH); } } void stopFurnace(){ //Send appropriate signal to the furnace to stop if(furnaceState){ furnaceState = !furnaceState; log("INFO", "STOPPING FURNACE"); digitalWrite(furnacePin, LOW); } } </tt></tt></tt>
Adding in Analog Buttons
All right. Since I'm pretty lean on my available pins on the arduino (I have 7,8, and 9 digital pins available, currently, and I still need to connect the furnace and the dampers), I'm going to look for a way to use the available analog inputs (I have A0 through A3 available, since A4 and A5 are connected to the RTC) to accept button input. Pretty quckly I find something which is very promising - a tutorial on how to accept input from multiple buttons via a single analog input. Pay dirt. This is perfect, and all it requires is some additional resistors. No problem whatever. Let's get to wiring.
(See this tutorial for a from the horses mouth tutorial on how it works)
The wiring for this is super simple. Run one wire into the analog pin of your choice (my program reads from pin A0), and connect that to one side of the button array. The other side of the button array is connected to ground. From there, the buttons are simply chained together via a resistor network. An additional wire is added from each button to the analog input pin, so every button has a direct path to the analog pin (see schematic).
Taking the pertinent pieces of the code from the tutorial and modifying it for our own needs, we get the following:
/* This is the application that I will be using to control the furnace. The concept is this: I have 3 zones I will be monitoring. Each zone will have at least one zone damper which will be controlled by the arduino application. Each zone will have a single thermometer which will report back to the controller. Depending on the thermometer reading and the thermostat setting, the dampers will open or shut and the central furnace will be turned on. Once an acceptable temperature has been reached in all zones, the furnace will turn off. In addition to general heating cycles, the system will be programmable. At this time, however, the programming cycle will be handled here, not via the thermostat. IMPORTANT TEST CASES: (to be added as I think of them/come across them * Need to ensure that the furnace is always off if all 3 dampers are closed. * dampers should be open unless they specifically need to be closed. * if system is set to "hold", all zones are set to open and the programming does not run. * if system is overridden but not held, the override stops once the next programmed interval comes up */ #include //This is a library needed for the LCD display #include //This is a library needed for the thermometer readings #include //This is a temperature library #include #include "RTClib.h" // Connections: // rs (LCD pin 4) to Arduino pin 12 // rw (LCD pin 5) to Arduino pin 11 // enable (LCD pin 6) to Arduino pin 10 // LCD pin 15 to Arduino pin 13 // LCD pins d4, d5, d6, d7 to Arduino pins 5, 4, 3, 2 LiquidCrystal lcd(12, 11, 10, 5, 4, 3, 2); byte deg[8] = { B01000, B10100, B01000, B00111, B00100, B00110, B00100, B00100 }; const int backlight = 13; //controls backlight #define THERM_BUS 6 // all temperature readings come in via this single pin. OneWire oneWire(THERM_BUS); DallasTemperature sensors(&oneWire); DeviceAddress zone1Therm = { 0x28, 0x48, 0x39, 0x11, 0x04, 0x00, 0x00, 0x94}; DeviceAddress zone2Therm = { 0x28, 0xF2, 0x1F, 0x11, 0x04, 0x00, 0x00, 0x38}; DeviceAddress zone3Therm = { 0x28, 0xBA, 0xF3, 0x10, 0x04, 0x00, 0x00, 0x98}; RTC_DS1307 RTC; // START PROGRAMMING // Times are 6 AM, 8 AM, 3PM, and 9PM int* PROGRAMMED_VALUES[7]; int SUNDAY_VALUES[4] = { 67, 67, 67, 60}; int MONDAY_VALUES[4] = { 67, 67, 67, 60}; int TUESDAY_VALUES[4] = { 67, 67, 67, 60}; int WEDNESDAY_VALUES[4] = { 67, 67, 67, 60}; int THURSDAY_VALUES[4] = { 67, 67, 67, 60}; int FRIDAY_VALUES[4] = { 67, 67, 90, 75}; int SATURDAY_VALUES[4] = { 67, 67, 67, 60}; //END PROGRAMMING float zone1Temp = 0; float zone2Temp = 0; float zone3Temp = 0; boolean z1Check = 0; boolean z2Check = 0; boolean z3Check = 0; boolean isOverrideHeld = false; boolean isOverridden = false; boolean furnaceState = false; //with furnaceState, false means it's currently off, true means its currently on. String thermState = "run"; int overrideValue = 0; const int furnacePin = 1; int zoneCounter = 0; int a = 0; void setup() { Serial.begin(57600); // This connects the arduino to the computer //TEMPERATURE SENSOR SETUP sensors.begin(); sensors.setResolution(zone1Therm, 10); lcd.createChar(0, deg); // RTC SETUP. Wire.begin(); RTC.begin(); if(!RTC.isrunning()){ log("ERROR", "RTC is NOT running!"); //RTC.adjust(DateTime(__DATE__, __TIME__)); //THIS ONLY NEEDS TO BE UNCOMMENTED IF YOU ARE SETTING UP YOUR RTC FOR THE FIRST TIME! ONCE IT IS SET, THIS MUST BE DISABLED! } //SETUP OUTPUTS pinMode(furnacePin, OUTPUT); pinMode(backlight, OUTPUT); digitalWrite(backlight, HIGH); //SETUP INPUTS pinMode(A0, INPUT_PULLUP); //SETTING UP THE WEEKLY PROGRAM PROGRAMMED_VALUES[0] = SUNDAY_VALUES; PROGRAMMED_VALUES[1] = MONDAY_VALUES; PROGRAMMED_VALUES[2] = TUESDAY_VALUES; PROGRAMMED_VALUES[3] = WEDNESDAY_VALUES; PROGRAMMED_VALUES[4] = THURSDAY_VALUES; PROGRAMMED_VALUES[5] = FRIDAY_VALUES; PROGRAMMED_VALUES[6] = SATURDAY_VALUES; } // this runs the continual loop void loop() { delay(3000); //DELAY CURRENTLY SET TO 10 SECONDS. WILL ADJUST AS NEEDED //DATE/TIME LOGIC log("TIME", "Loop initiated at " + getDateAndTime()); //TEMPERATURE LOGIC sensors.requestTemperatures(); zone1Temp = getTemperature(zone1Therm); zone2Temp = getTemperature(zone2Therm); zone3Temp = getTemperature(zone3Therm); logTemperatureData(); getScreenOutput(); z1Check = checkZoneTemp(zone1Temp); z2Check = checkZoneTemp(zone2Temp); z3Check = checkZoneTemp(zone3Temp); if(z1Check || z2Check || z3Check){ startFurnace(); } else { stopFurnace(); } if(z1Check && furnaceState) { closeDamper(1); } else { openDamper(1); } if(z2Check && furnaceState) { closeDamper(2); } else { openDamper(2); } if(z3Check && furnaceState) { closeDamper(3); } else { openDamper(3); } a = analogRead(0); //THESE NUMBERS CHANGE DEPENDING ON YOUR RESISTORS. if(a < 20) { //This means temperature up. isOverridden = true; } if(a < 30 && a > 20) { //This means temperature down. isOverridden = true; } if(a < 45 && a > 30){ //This is a toggle for hold/run if(thermState == "hold") { thermState = "run"; isOverridden = false; } else { thermState = "hold"; isOverridden = true; } isOverridden = true; } if(a < 60 && a > 45){ //DO I NEED A FOURTH BUTTON? } } boolean checkZoneTemp(int temperature) { int temp = getTargetTemperature(); if(temperature > temp){ return false; } return true; } void logTemperatureData() { log("Zone1Temp", (String)(int)zone1Temp); log("Zone2Temp", (String)(int)zone2Temp); log("Zone3Temp", (String)(int)zone3Temp); } void getScreenOutput() { zoneCounter++; if(zoneCounter == 1){ String val = "Zone1: " + (String)(int)zone1Temp; val += " (" + (String)getTargetTemperature(); val += (String)")"; toScreen(val, "Mode: " + getModeString()); } else if (zoneCounter == 2){ String val = "Zone2: " + (String)(int)zone2Temp; val += " (" + (String)getTargetTemperature(); val += (String)")"; toScreen(val, "Furnace: " + getFurnaceState()); } else { zoneCounter = 0; String val = "Zone3: " + (String)(int)zone3Temp; val += " (" + (String)getTargetTemperature(); val += (String)")"; toScreen(val, "Open: " + getActiveZonesForScreen()); } } int getTargetTemperature(){ int mode = getMode(); DateTime now = RTC.now(); int date = now.dayOfWeek(); int* values = { 0 }; log("info", (String)date); values = PROGRAMMED_VALUES[date]; log("info", "getting target temp: " + (String)values[mode]); return values[mode]; } String getActiveZonesForScreen(){ String retVal = ""; if(z1Check && !furnaceState){ retVal += "1,"; } if(z2Check && !furnaceState) { retVal += "2,"; } if(z3Check && !furnaceState){ retVal += "3"; } if(retVal.length() == 0) { retVal += "None"; } return retVal; } String getFurnaceState(){ if(furnaceState){ return "On"; } else { return "Off"; } } String getModeString() { int mode = getMode(); switch (mode) { case 0: return "Wake"; case 1: return "Away"; case 2: return "Home"; case 3: return "Sleep"; default: return "N/A"; } } int getMode() { DateTime now = RTC.now(); int hour = now.hour(); if(hour < 6 || hour >= 21) { return 3; } if(hour >= 6 && hour < 8){ return 0; } if(hour >=8 && hour < 15){ return 1; } if(hour >= 15 && hour < 21){ return 2; } } float getTemperature(DeviceAddress deviceAddress) { float tempC = sensors.getTempC(deviceAddress); //Serial.println(tempC); if(tempC == -127){ Serial.println("ERROR getting temperature."); } return DallasTemperature::toFahrenheit(tempC); } //convenience function. Handles all the boilerplate for writing to the LCDScreen void toScreen(String line1Value, String line2Value) { lcd.begin(16,2); lcd.clear(); lcd.setCursor(0,0); lcd.print(line1Value); lcd.setCursor(0,1); lcd.print(line2Value); } String getDateAndTime() { DateTime now = RTC.now(); return String(now.year()) + "/" + String(now.month()) + "/" + String(now.day()) + " " + String(now.hour()) + ":" + String(now.minute()) + ":" + String(now.second()); } void openDamper(int damper) { // if(damper == damper1Pin || damper == damper2Pin || damper == damper3Pin) { // digitalWrite(damper, HIGH); // } else { // log("Problem opening damper. Damper not found. Was expecting 1, 2, or 3. Got " + damper); // } } void closeDamper(int damper) { // if(damper == damper1Pin || damper == damper2Pin || damper == damper3Pin) { // digitalWrite(damper, LOW); // } else { // log("Problem closing damper. Damper not found. Was expecting 1, 2, or 3. Got " + damper); // } } //function for logging messages to the console. //Eventually plan to write to logfile on RPi void log(String code, String message) { Serial.println(code + ": " + message); } void recordData(String type, String object, String value) { // TODO - output to RPi database } void startFurnace(){ //Send appropriate signal to the furnace to start up if(!furnaceState) { furnaceState = !furnaceState; log("INFO", "STARTING FURNACE"); digitalWrite(furnacePin, HIGH); } } void stopFurnace(){ //Send appropriate signal to the furnace to stop if(furnaceState){ furnaceState = !furnaceState; log("INFO", "STOPPING FURNACE"); digitalWrite(furnacePin, LOW); } }
Make the Buttons DO Something.
Each button has to be assigned a function or purpose. In our case, the first and second buttons are for increasing or decreasing the buttons respectively, and the third button is for toggling between run or hold mode. I've included a fourth button, but I don't know if its needed. For now I'll leave it in, but we'll see.
The Final Code:
For the purposes of this Instructable, this is the final code
/* /* This is the application that I will be using to control the furnace. The concept is this: I have 3 zones I will be monitoring. Each zone will have at least one zone damper which will be controlled by the arduino application. Each zone will have a single thermometer which will report back to the controller. Depending on the thermometer reading and the thermostat setting, the dampers will open or shut and the central furnace will be turned on. Once an acceptable temperature has been reached in all zones, the furnace will turn off. In addition to general heating cycles, the system will be programmable. At this time, however, the programming cycle will be handled here, not via the thermostat. IMPORTANT TEST CASES: (to be added as I think of them/come across them * Need to ensure that the furnace is always off if all 3 dampers are closed. * dampers should be open unless they specifically need to be closed. * if system is set to "hold", all zones are set to open and the programming does not run. * if system is overridden but not held, the override stops once the next programmed interval comes up * can only override to explicit upper and lower bounds. */ #include <LiquidCrystal.h> //This is a library needed for the LCD display #include <OneWire.h> //This is a library needed for the thermometer readings #include <DallasTemperature.h> //This is a temperature library #include <Wire.h> #include "RTClib.h" // Connections: // rs (LCD pin 4) to Arduino pin 12 // rw (LCD pin 5) to Arduino pin 11 // enable (LCD pin 6) to Arduino pin 10 // LCD pin 15 to Arduino pin 13 // LCD pins d4, d5, d6, d7 to Arduino pins 5, 4, 3, 2 LiquidCrystal lcd(12, 11, 10, 5, 4, 3, 2); byte deg[8] = { B01000, B10100, B01000, B00111, B00100, B00110, B00100, B00100 }; // const int backlight = 13; //controls backlight #define THERM_BUS 6 // all temperature readings come in via this single pin. OneWire oneWire(THERM_BUS); DallasTemperature sensors(&oneWire); DeviceAddress zone1Therm = { 0x28, 0x48, 0x39, 0x11, 0x04, 0x00, 0x00, 0x94}; DeviceAddress zone2Therm = { 0x28, 0xF2, 0x1F, 0x11, 0x04, 0x00, 0x00, 0x38}; DeviceAddress zone3Therm = { 0x28, 0xBA, 0xF3, 0x10, 0x04, 0x00, 0x00, 0x98}; RTC_DS1307 RTC; // START PROGRAMMING // Times are 6 AM, 8 AM, 3PM, and 9PM int* PROGRAMMED_VALUES[7]; int SUNDAY_VALUES[4] = { 67, 67, 67, 60}; int MONDAY_VALUES[4] = { 67, 67, 67, 60}; int TUESDAY_VALUES[4] = { 67, 67, 67, 60}; int WEDNESDAY_VALUES[4] = { 67, 67, 67, 60}; int THURSDAY_VALUES[4] = { 67, 67, 67, 60}; int FRIDAY_VALUES[4] = { 67, 67, 90, 75}; int SATURDAY_VALUES[4] = { 67, 67, 67, 60}; //END PROGRAMMING float zone1Temp = 0; float zone2Temp = 0; float zone3Temp = 0; boolean z1Check = 0; boolean z2Check = 0; boolean z3Check = 0; boolean isOverridden = false; boolean furnaceState = false; //with furnaceState, false means it's currently off, true means its currently on. String thermState = "run"; int overrideValue = 0; int currentMode = 0; const int furnacePin = 7; int zoneCounter = 0; int a = 0; int loopCounter = 0; int z1Pin = 8; int z2Pin = 9; int z3Pin = 13; //scavenging backlight pin for z3 void setup() { Serial.begin(57600); // This connects the arduino to the computer //TEMPERATURE SENSOR SETUP sensors.begin(); sensors.setResolution(zone1Therm, 10); lcd.createChar(0, deg); // RTC SETUP. Wire.begin(); RTC.begin(); if(!RTC.isrunning()){ log("ERROR", "RTC is NOT running!"); //RTC.adjust(DateTime(__DATE__, __TIME__)); //THIS ONLY NEEDS TO BE UNCOMMENTED IF YOU ARE SETTING UP YOUR RTC FOR THE FIRST TIME! ONCE IT IS SET, THIS MUST BE DISABLED! } //SETUP OUTPUTS pinMode(furnacePin, OUTPUT); pinMode(z1Pin, OUTPUT); pinMode(z2Pin, OUTPUT); pinMode(z3Pin, OUTPUT); // eventually fit backlight control back into arduino. For now we'll just set it to constant. // digitalWrite(backlight, HIGH); //SETUP INPUTS pinMode(A0, INPUT_PULLUP); //SETTING UP THE WEEKLY PROGRAM PROGRAMMED_VALUES[0] = SUNDAY_VALUES; PROGRAMMED_VALUES[1] = MONDAY_VALUES; PROGRAMMED_VALUES[2] = TUESDAY_VALUES; PROGRAMMED_VALUES[3] = WEDNESDAY_VALUES; PROGRAMMED_VALUES[4] = THURSDAY_VALUES; PROGRAMMED_VALUES[5] = FRIDAY_VALUES; PROGRAMMED_VALUES[6] = SATURDAY_VALUES; } // this runs the continual loop void loop() { delay(100); //DELAY CURRENTLY SET TO .1 SECONDS. WILL ADJUST AS NEEDED //DATE/TIME LOGIC // if(loopCounter % 2 == 0){ // log("TIME", "Loop initiated at " + getDateAndTime()); // } //TEMPERATURE LOGIC // logTemperatureData(); if(loopCounter % 30 == 0) { log("info", (String)loopCounter); //changes output every 3 seconds outputToScreen(); } // The following allows us to only check furnace state every 60 seconds while reading the button changes in effective realtime if(loopCounter % 60 == 0) { log("info", (String)loopCounter); sensors.requestTemperatures(); zone1Temp = getTemperature(zone1Therm); zone2Temp = getTemperature(zone2Therm); zone3Temp = getTemperature(zone3Therm); z1Check = checkZoneTemp(zone1Temp); z2Check = checkZoneTemp(zone2Temp); z3Check = checkZoneTemp(zone3Temp); loopCounter = 0; if(z1Check || z2Check || z3Check){ startFurnace(); } else { stopFurnace(); } if(z1Check && furnaceState) { closeDamper(z1Pin); } else { openDamper(z1Pin); } if(z2Check && furnaceState) { closeDamper(z2Pin); } else { openDamper(z2Pin); } if(z3Check && furnaceState) { closeDamper(z3Pin); } else { openDamper(z3Pin); } } a = analogRead(0); //THESE NUMBERS CHANGE DEPENDING ON YOUR RESISTORS. int targetTemp = getProgrammedTargetTemperature(); if(a < 20) { //This means temperature up. isOverridden = true; if(overrideValue != targetTemp && overrideValue != 0){ overrideValue++; } else { overrideValue = targetTemp + 1; } if(overrideValue > 80){ overrideValue = 80; } log("info", "temperature up to " + (String)overrideValue); loopCounter = -1; //THIS RESETS THE LOOP COUNTER, MEANING EVERYTHING WILL BE RUN AGAIN } if(a < 30 && a > 20) { //This means temperature down. isOverridden = true; if(overrideValue != targetTemp && overrideValue > 0){ overrideValue--; } else { overrideValue = targetTemp - 1; } if(overrideValue < 50) { overrideValue = 50; } log("info", "temperature down to " + (String)overrideValue); loopCounter = -1; //THIS RESETS THE LOOP COUNTER, MEANING EVERYTHING WILL BE RUN AGAIN } if(a < 45 && a > 30){ //This is a toggle for hold/run if(thermState == "hold") { thermState = "run"; isOverridden = false; log("info", "setting thermState to run"); } else { thermState = "hold"; isOverridden = true; log("info", "setting thermState to hold"); } isOverridden = true; loopCounter = -1; //THIS RESETS THE LOOP COUNTER, MEANING EVERYTHING WILL BE RUN AGAIN } if(a < 60 && a > 45){ //DO I NEED A FOURTH BUTTON? } loopCounter++; } boolean checkZoneTemp(int temperature) { int temp = getTargetTemperature(); if(temperature > temp){ return false; } return true; } void logTemperatureData() { log("Zone1Temp", (String)(int)zone1Temp); log("Zone2Temp", (String)(int)zone2Temp); log("Zone3Temp", (String)(int)zone3Temp); } void outputToScreen() { zoneCounter++; if(zoneCounter == 1){ String val = "Zone1: " + (String)(int)zone1Temp; val += " (" + (String)getTargetTemperature(); val += (String)")"; toScreen(val, "Mode: " + getModeString()); } else if (zoneCounter == 2){ String val = "Zone2: " + (String)(int)zone2Temp; val += " (" + (String)getTargetTemperature(); val += (String)")"; toScreen(val, "Furnace: " + getFurnaceState()); } else { zoneCounter = 0; String val = "Zone3: " + (String)(int)zone3Temp; val += " (" + (String)getTargetTemperature(); val += (String)")"; toScreen(val, "Open: " + getActiveZonesForScreen()); } } void checkMode() { int mode = getMode(); if(mode != currentMode) { currentMode = mode; if(isOverridden) { if(thermState == "run") { //This logic simply overturns the override value and resumes the program if the thermstate is not held. Otherwise, the overrideValue is used. isOverridden = false; } } } } int getTargetTemperature(){ checkMode(); if(isOverridden) { return overrideValue; } else { return getProgrammedTargetTemperature(); } } int getProgrammedTargetTemperature() { int mode = getMode(); DateTime now = RTC.now(); int date = now.dayOfWeek(); int* values = { 0 }; values = PROGRAMMED_VALUES[date]; // log("info", "getting programmed target temp: " + (String)values[mode]); return values[mode]; } String getActiveZonesForScreen(){ String retVal = ""; if(z1Check && !furnaceState){ retVal += "1,"; } if(z2Check && !furnaceState) { retVal += "2,"; } if(z3Check && !furnaceState){ retVal += "3"; } if(retVal.length() == 0) { retVal += "None"; } return retVal; } String getFurnaceState(){ if(furnaceState){ return "On"; } else { return "Off"; } } String getModeString() { if(isOverridden) { if(thermState == "run") { return "Override"; } else { return "HOLD"; } } int mode = getMode(); switch (mode) { case 0: return "Wake"; case 1: return "Away"; case 2: return "Home"; case 3: return "Sleep"; case -1: return "Error"; default: return "N/A"; } } int getMode() { DateTime now = RTC.now(); int hour = now.hour(); if(hour < 6 || hour >= 21) { return 3; } if(hour >= 6 && hour < 8){ return 0; } if(hour >=8 && hour < 15){ return 1; } if(hour >= 15 && hour < 21){ return 2; } return -1; } float getTemperature(DeviceAddress deviceAddress) { float tempC = sensors.getTempC(deviceAddress); //Serial.println(tempC); if(tempC == -127){ Serial.println("ERROR getting temperature."); } return DallasTemperature::toFahrenheit(tempC); } //convenience function. Handles all the boilerplate for writing to the LCDScreen void toScreen(String line1Value, String line2Value) { lcd.begin(16,2); lcd.clear(); lcd.setCursor(0,0); lcd.print(line1Value); lcd.setCursor(0,1); lcd.print(line2Value); } String getDateAndTime() { DateTime now = RTC.now(); return String(now.year()) + "/" + String(now.month()) + "/" + String(now.day()) + " " + String(now.hour()) + ":" + String(now.minute()) + ":" + String(now.second()); } void openDamper(int damper) { if(damper == z1Pin || damper == z2Pin || damper == z3Pin) { digitalWrite(damper, HIGH); // log("info", "Opening damper" + damper); } else { log("error", "Problem opening damper. Damper not found. Was expecting 1, 2, or 3. Got " + damper); } } void closeDamper(int damper) { if(damper == z1Pin || damper == z2Pin || damper == z3Pin) { digitalWrite(damper, LOW); // log("info", "Closing damper" + damper); } else { log("error", "Problem closing damper. Damper not found. Was expecting 1, 2, or 3. Got " + damper); } } //function for logging messages to the console. //Eventually plan to write to logfile on RPi void log(String code, String message) { Serial.println(code + ": " + message); } void recordData(String type, String object, String value) { // TODO - output to RPi database } void startFurnace(){ //Send appropriate signal to the furnace to start up if(!furnaceState) { furnaceState = !furnaceState; log("INFO", "STARTING FURNACE"); digitalWrite(furnacePin, HIGH); } } void stopFurnace(){ //Send appropriate signal to the furnace to stop if(furnaceState){ furnaceState = !furnaceState; log("INFO", "STOPPING FURNACE"); digitalWrite(furnacePin, LOW); } }
Next Steps
This has mostly been a theoretical build. I fully intend on installing it as my actual thermostat, but I need to do some further soldering and other assorted setups in order to have a really nice wall-adorning thermostat (my kids would have a ball pulling wires out of the electronics on the wall, but it's not something I want to continually be putting back together).
My next steps will be to fashion a housing for the main thermostat as well as the two remote temperature sensors. These housings will sit on the wall in their respective zones. I've decided I will be using hardwood maple for the housings, so they should look fairly sharp once attached to the wall. I have some ideas for the design of them, but nothing set in stone, yet.
Additionally, I will want to reclaim my Arduino board and replace this circuit with a hackduino or something similar. No use having a fast prototyping board in the final product of a project.
Beyond that, some simple soldering and affixing the components to permanent breadboards will be required, as again these breadboards are not designed for permanent use.
I have to either procure or build out some zone dampers. Once these have been procured and installed, the zone aspect of this thermostat will be fully realized.
And finally, I will have to install a Raspberry Pi or equivalent tiny web server into my main thermostat body. From the webserver, I'll be able to monitor my thermostat data in real time as well as eventually turning it into a 'smart' thermostat.
I hope you enjoyed this Instructable. I haven't really found exactly this thing, and while a Nest is cool and swoopy zoomy, this can already do things the Nest cannot and will probably never be able to do.
Thanks!