HVAC controller log / Introduction

Hey! I was partly inspired to do this project after seeing your site earlier this week. It's great to have your feedback.

Nothing super surprising to me in your list but let me go over it for the viewers at home;

  • I got the big relay board because it was cheap and I know I'll use it for other projects in the future. For this small project I plan on moving to solid state relays once I've got it stabilized a bit.

  • Power: My thermostat is directly above a 110 outlet so I can easily power it from that, or run some low voltage wire through the wall to the existing hole where the HVAC cables run. (I'm not sure how the power source from the HVAC works, if it's always on, etc. It's probably enough to run the bits I'm adding. Investigation needed)

  • It will look good. Don't worry. :smiley: I take a lot of pride in making these sorts of things look like artwork.

  • Light and Humidity; Probably not in use for this project. They were a few dollars and I knew I'd have fun with them. There are many days in the spring/fall where the temperature in the house is fine, but it's just a bit muggy and I want to run the AC for 5 minutes to dry it out a little bit. (Thought; maybe I just add a button that runs the AC for 10 minutes... something like that)

  • Display. I want to show what modes are in use; AC, HEAT, FAN. I want to show actual temp, and the set/target temp. As far as user input, I'll default to 70f and just have buttons for up/down. I usually never go outside of 68-72 unless I'm gone for long periods of time.

  • I'm thinking some small toggle switches to select modes.

I would really like to make it controlled by a simple web interface. Not sure what kind of effort that will be.

I don't want to monopolize the thread, so I'll keep it short. Build something pretty simple at first, then use it a couple of weeks. When you get some experience, then go completely nuts and make it really cool. A really neat way to show what mode it's in is with a colored led display: red letters for heat, green for only the fans, blue for A/C, and of course, white for idle. If I hadn't taken mine to the web for control, that's exactly what I'd do. Adafruit has that kind of display and remember, serial displays are really easy, parallel ones will drive you nuts with connections. Graphic displays can wait until you understand web controls, do graphics on something you can see like a tablet or phone.

Have fun.

@LVLAaron
Disclaimer:
Be sure you have a very good understanding of your HVAC system sequence of operation before using this as a controller. There are some very important safeties that must be used. If this is a gas furnace, there are rollout switches and induced draft fan safeties that are in place to keep you from dying in a fire or from CO poisoning. If it's all electric, you need limit switches so your heaters turn off in the event of a blower failure.

Project/fun
It sounds like a very interesting project. Draw up a clear objective for your project. What are the features you want? Start small, and work your way up. Once you're confident that the basics are working properly and safely, start adding some bells and whistles. Have fun, be careful.

flyboy:
@LVLAaron
Disclaimer:
Be sure you have a very good understanding of your HVAC system sequence of operation before using this as a controller. There are some very important safeties that must be used. If this is a gas furnace, there are rollout switches and induced draft fan safeties that are in place to keep you from dying in a fire or from CO poisoning. If it's all electric, you need limit switches so your heaters turn off in the event of a blower failure.

Safety is important, but are these things relevant to a thermostat replacement? On my HVAC at least, such things are encapsulated in its own control boards: the thermostat asks the HVAC to provide heat or cooling - it's up to the HVAC to 'decide' whether it's safe to comply.

I'm just replacing the thermostat on the wall, not getting into the HVAC unit itself.

For example, the only leads for heat are "HEAT" and "+24v" - Whatever is inside the HVAC unit does the rest of the exhaust fan spinup/cooldown, etc.

If you have a heat pump, you'll have a wire for heat/cool, a wire for fan, a wire for compressor, 24vac, and common. Sometimes they don't carry the 24vac to the thermostat, you'll have to check that. Just prowl around the web, find a thermostat schematic, and you're ready to go.

It's a little different if you have a furnace/AC, but the same idea. Find a schematic for the thermostat and steal the ideas.

There's tons and tons of information out there.

I am already familiar with the wiring for my unit. When my original thermostat gave out on me I just used some jumper wires to get the AC going. 8)

First real update! Some action shots and some code.

Action shot #1 - Successfully reading sensor data and displaying it on the LCD.

Action shot #2 - Added code to determine if ambient temperature is above or below a static "target" temp. The LED's have been added to indicate which side of the target the air is at. Also wired in the relay board. Relay 1 and 2 act in concert with the red and yellow leds.

Here's my sketch;

// include the libraries for gizmos
#include <LiquidCrystal.h>
#include <dht11.h>

dht11 DHT11; // Figure out what this line does
LiquidCrystal lcd(12, 11, 5, 4, 3, 2); // initialize the library with the numbers of the interface pins

// These constants won't change:
const int ACledPin = 8;      // Air Conditioner LED (RED)
const int HTledPin = 9;      // Heat LED (YELLOW)
const int threshold = 90;    // an arbitrary threshold level that's in the range of the analog input

const int RELAY_01 = 6;    // RELAY 1 PIN 9
const int RELAY_02 = 10;   // RELAY 2 PIN 10

void setup() {
  DHT11.attach(7);       // Attach DHT11 sensor to pin 7
  lcd.begin(16, 2);      // set up the LCD's number of columns and rows:
  Serial.begin(9600);    // Serial setup

  pinMode(ACledPin, OUTPUT);  // initialize pin 8 as output
  pinMode(HTledPin, OUTPUT);  // initialize pin 9 as output
  pinMode(RELAY_01, OUTPUT);  // initialize pin 6 as output
  pinMode(RELAY_02, OUTPUT);  // initialize pin 10 as output

  // Leaves the relays in default "off" (no noise/click)
  digitalWrite(RELAY_01, HIGH);
  digitalWrite(RELAY_02, HIGH);

}

void loop() {
  int chk = DHT11.read();   // Check status of the sensor
  lcd.setCursor(0, 0);      // Set cursor to top left


////////////// if then ////////////////
  float analogValue = (DHT11.fahrenheit()); // trying to set up the temp reading as an int variable

  // if the analog value is high enough, turn on the LED:
  if (analogValue > threshold) {
    digitalWrite(ACledPin, HIGH);
    digitalWrite(HTledPin, LOW);
    digitalWrite(RELAY_01, HIGH);
    digitalWrite(RELAY_02, LOW);
  }
  else {
    digitalWrite(ACledPin, LOW);
    digitalWrite(HTledPin, HIGH);
    digitalWrite(RELAY_01, LOW);
    digitalWrite(RELAY_02, HIGH);
  }

  // print the analog value:
  Serial.println(analogValue);
  delay(1);        // delay in between reads for stability
  ////////////// if then ////////////////

  lcd.setCursor(0, 1);
  lcd.print("Temp(F): ");
  lcd.print(DHT11.fahrenheit(), 2);

  delay(2000);

}

As always I am open to all constructive criticisms.

Next steps for me;

  • User input for target temperature instead of being hard coded. Display this on the LCD.

  • Work on temperature "swings". In HVAC speak, if you are in AC mode and have your temp set at 72, the swing is usually 1 to 3 degrees. Let's assume our swing is 1 in this case. This means that when the ambient room temp reaches 73 the AC turns on, and runs until it cools the air to 71 degrees. 1 degree of "swing" in either direction of the target temp.

  • Further down the road, overrun protection. It's not good to have the compressor turn off and then turn it back on in quick succession. Need something in the code that says if the unit has been running in the last 10 minutes, to not run until 10 minutes has passed.

That's it for today. More parts coming tomorrow.

Some clearer names might be nice. ACledPin is ok, in context, so is HTledPin but you wouldn't have to think about it if it were HeatLedPin. As for Relay_01 and Relay_02 - really! Shouldn't analogValue be Termperature or TemperatureF?

These things don't really matter, especially in such a small sketch but they make it slightly harder to work with and spot bugs. As the sketch gets bigger, that will become more important.

delay(1) doesn't serve any useful purpose now - that delay(2000) is doing its job.

Finally, why do you use DHT11.fahrenheit() to get the temperature for printing to the LCD when you already have it in analogValue?

Some clearer names might be nice

Absolutely. It's on my list of to-do's. I'll do a replace-all a little later on.

delay(1) doesn't serve any useful purpose now - that delay(2000) is doing its job.

I was hoping that I could have two different things going on; a 1 second delay for the serial output and a 2 second delay for the screen refresh. Not possible?

Finally, why do you use DHT11.fahrenheit() to get the temperature for printing to the LCD when you already have it in analogValue?

Good catch. I can tidy that up.

was hoping that I could have two different things going on; a 1 second delay for the serial output and a 2 second delay for the screen refresh. Not possible?

It's possible, but not using delay. You'll need to use millis instead - the blink without delay example in the IDE shows you how to manage it. You'll need to use millis (or a RTC) when you get to the point of ensuring the compressor doesn't cycle too often. Also, use of delay will make your code unresponsive to the user when trying to change the set point.

Nobody ever listens to me when I tell them that the way to handle delays and timers is with the Time and TimeAlarm libraries. You can set a timer to fire every two seconds and update the display. This will run without any delay() calls and leave the processor free to do something else, like check for user input. Doing it for a second is a bit iffy since that's its smallest granularity, but most often, we don't need a second, we need a delay of some kind and we just choose a second. Yes, you can have multiple timers and alarms.

I use this set of libraries extensively to handle changing displays, where I show something for a couple of seconds and something else for a couple of seconds. I also use it to have things report on a schedule; whether they report hourly, daily, every 10 seconds, etc. I have it turn things on at 8 and off at 10. It just works. If you don't need the actual time, just set it to whatever comes to mind, it will keep time and establish timers and alarms from there. I sync my stuff up off a GPS chip, but you certainly don't have to be that extreme.

A very tiny bit hard to understand at first, but once you use it, it turns out to be incredibly easy. You can turn a simple arduino into a multiprocessor pretty easily (albeit, a slow one). But, like I said, no one listens to me when I talk about it.

For temperature readings, I use a moving average of several readings. Temperature sensors can react too quickly to breezes and such for use in a home thermostat, so I slow them down a bunch by sampling a lot and averaging. I don't usually put delays in the read because I want the processor available for other things delay() locks everything out except interrupts.

Here in the desert I use a hysteresis of -2,+1 in the summer and -1,+2 in the winter. That was after trying about a jillion different things that kept the compressor on too long or cycled it too rapidly. This is something you'll have to experiment with, so make it a value you can change on the fly. Of all the weird things I ran into, the proper hysteresis curve was the most annoying.

Well I'm certainly listening! I am impressed with your setup. I will look at those libraries, and if memory serves, you have all of your code posted somewhere?

Yes, I have lots of code posted for various things, and my usage of TimeAlarm is in there. It may be hard to find where I used it, so here's a rewrite of 'blink' using time alarm. The big difference is that loop() really doesn't do anything, the processor is available for whatever you want to do. The caution is to remove all delay() calls and convert them to Alarm.delay() calls which does the same thing, but doesn't lock up the processor, as well as allow the time to tick by so things can happen.

/*
Just an example of how one timer can set off another in an endless
loop.
 */

#include <Time.h>
#include <TimeAlarms.h>

// When the timer fires, it will come here automatically
void lightOn(){
  digitalWrite(13, HIGH);
  Alarm.timerOnce(1, lightOff);  // in 5 seconds, turn the light off 
}

void lightOff(){
  digitalWrite(13, LOW);
  Alarm.timerOnce(1, lightOn);  // in 5 seconds turn the light on
}

void setup()
{
  Serial.begin(9600);    
  Serial.println("Just for fun");
  pinMode(13, OUTPUT);  // we're going to use the on board LED  
  setTime(0,0,0,1,1,13); // This is only to make the routines work
                         // however, if you want something based on
                         // time of day, you'll have to figure out
                         // a way of setting the time correctly
                         // and keeping it current. 
  Alarm.delay(0);         // You'll have to replace all the delay()
                         // calls with one of these.  I used zero here
                         // because we don't actually need any delay.
  lightOn();             // This is a priming call to get it all started.
}

void  loop()
{  
  // the loop really doesn't have to have anything in it for
  // the timers and alarms to work except the Alarm.delay()
  // this lets the alarm code run to see if anything needs to 
  // be done
  Alarm.delay(0);  // we don't actually need to spend any time here.
}

And, naturally, I still have the wrong time in the comments ... sigh.

Sunday update. I have a crude smoothing function for the temperature reading. I say crude because when the unit powers on the first 4 temp readings are low because the array is full of zeros.

In the next few days I'd like to figure out a way around this, and then start on "run protection". I dont want to turn the AC off, and then turn it back on unless 10 minutes has passed. I feel like this will be a tricky.

// include the libraries for gizmos
#include <LiquidCrystal.h>
#include <dht11.h>

dht11 DHT11; // Figure out what this line does
LiquidCrystal lcd(12, 11, 5, 4, 3, 2); // initialize the library with the numbers of the interface pins

// These constants won't change:
const int ACledPin = 8;      // Air Conditioner LED (RED)
const int HTledPin = 9;      // Heat LED (YELLOW)
const int threshold = 75;    // an arbitrary threshold level that's in the range of the analog input

const int RELAY_01 = 6;    // RELAY 1 PIN 9
const int RELAY_02 = 10;   // RELAY 2 PIN 10

// START ADDITIONS ////////////////////////////////////////////////////////////////////////////////////////////////
const int numReadings = 5;
float readings[numReadings];      // the readings from the analog input
int index = 0;                    // the index of the current reading
float total = 0;                  // the running total


float get_smoothed_temp() {
    total = total - readings[index]; // remove oldest reading
    readings[index] = DHT11.fahrenheit();
    total += readings[index];
    index = (index + 1) % numReadings;
    return (total / numReadings);
}
// END ADDITIONS ////////////////////////////////////////////////////////////////////////////////////////////////

void setup() {
  DHT11.attach(7);       // Attach DHT11 sensor to pin 7
  lcd.begin(16, 2);      // set up the LCD's number of columns and rows:
  Serial.begin(9600);    // Serial setup
  for (int thisReading = 0; thisReading < numReadings; thisReading++) readings[thisReading] = 0; // initialize all the readings to 0:

  

  pinMode(ACledPin, OUTPUT);  // initialize pin 8 as output
  pinMode(HTledPin, OUTPUT);  // initialize pin 9 as output
  pinMode(RELAY_01, OUTPUT);  // initialize pin 6 as output
  pinMode(RELAY_02, OUTPUT);  // initialize pin 10 as output

  // Leaves the relays in default "off" (no noise/click)
  digitalWrite(RELAY_01, HIGH);
  digitalWrite(RELAY_02, HIGH);

}

void loop() {

  float analogValue = (get_smoothed_temp()); // trying to set up the temp reading as an int variable
////////////// if then ////////////////
// if the analog value is high enough, turn on the LED:
  if (analogValue > threshold) {
    digitalWrite(ACledPin, HIGH);
    digitalWrite(HTledPin, LOW);
    digitalWrite(RELAY_01, HIGH);
    digitalWrite(RELAY_02, LOW);
  }
  else {
    digitalWrite(ACledPin, LOW);
    digitalWrite(HTledPin, HIGH);
    digitalWrite(RELAY_01, LOW);
    digitalWrite(RELAY_02, HIGH);
  }


Serial.println(analogValue);  // print the analog value:

// Set up the LCD Stuff
  lcd.setCursor(0, 0);      // Set cursor to top left
  lcd.print("Real F:");
  lcd.setCursor(0, 1);
  lcd.print("AVG  F:");
  lcd.print(analogValue, 1);


  delay(1000);

}

In your loop only turn the compressor on when a flag is set. Then when the compressor shuts off set the flag to false and set a timer to fire in 10 minutes. When the timer callback happens, set the flag to true. This will force a 10 minute wait between compressor cycles.

#include <Time.h>
#include <TimeAlarms.h>

boolean compressorAllowed;

To set the timer when you turn off the compressor, it would look something like this: 

Alarm.timerOnce(10 * 60, allowCompressor); // let the compressor back on in 10 minutes

The call back would look like this:

void allowCompressor(){
    compressorAllowed = true; This is your flag
}

Then, somwhere in your loop:

If (compressorAllowed){
    ....

Nothing to it, but you'd have to give up that delay call. If it were me, I'd make my loop() look something like this"

void loop(){
    alarm.Delay(0);
}

And in setup, I'd set up timers to handle everything:

alarm.timerRepeat(1, goCheckTheTemperatureAndDoStuff);  // This is the every second go check for something to do
alarm.timerRepeat(dowFriday,5,30,30,goBuyFlowersForThatGoodLookingChick); // This is to keep the people you're ignoring happy

But, that's just me.

I say crude because when the unit powers on the first 4 temp readings are low because the array is full of zeros.

Instead of setting the array elements to zero in setup, call get_smoothed_temp in the loop.

The smoothing algorithm is showing zero because it's filled with zeros to begin with and you haven't taken enough sample to change it yet. My algorithm starts at a negative number and step up to room temperature over a few seconds. If you want it to show something else while it stabilized, just fill it with that value. Say, fill it with 70, then it will show 70 and change a bit as it stabilizes.

Me, I like the numbers changing until it reaches room temp; it tells me it's alive.

Quick update. I've put my code aside for now and forked something I found on github. It was far from complete but I have most of it figured out now. Things are working well. I took some time off to get one of the Adafruit i2c RGB LCD's with buttons, etc. Pretty nice little kit.

Full sketch; HVAC_Thermostat.ino · GitHub
(Too big to post here)

The milestone for me is that I now understand how to use millis()

  • Next step is to sort out the way the relays function. Right now if the AC is ON the relay is what I will refer to as "off" which means the Arduino pin is set to HIGH. If something happened to the Arduino and it died unexpectedly, all of the relays would be "on" and I would essentially be turning on the HEAT/AC/FAN all at the same time. I'll need to work on flip/flopping the code to work around that.

  • Following that I want to work on the LCD menus. As it stands, if you press select to adjust your temperature thresholds there is no way to get back to the first/opening screen that just displays the temperature.