Super Thermostat

I have two heat pumps to heat and cool my house.  The house layout is such that there is no path from one part of the house to another for a big old air duct and two of them offer redundancy in case of failure.  I have really nice programmable thermostats on the systems that can shut them off during the peak period but there are still a few things that would be nice to have.

1. Ability for each of them to know when the other one is on to prevent both of them from coming on at the same time, or restrict each thermostat to a specific period of time so they don't overlap each other. This should  lower my peak demand.

2. Be able to adjust both of them from the same spot so you don't have to run back and forth.

3. Be able to tell if either of them are on and what they are doing when wondering what is eating up the power.

4. Keep visitors from playing with the temperature when they think they're getting too hot or cold.

Sounds like a job for a couple of Web enabled thermostats doesn't it? Enter another Arduino, this time a thermostat.

This collection of strange things wired together are the components of the thermostat.  Upper left is the Arduino stack with the Arduino topped by an ethernet shield and a prototype board holding a power supply and control relays.  Upper right is  a Sparkfun serial LCD display.  Lower left is the button keyboard.  Lower right is a set of LCDs to illustrate the fan, compressor and heat pump switchover.  Also on the lower right is the actual temperature sensor sticking down.

Schematic


Basic Schematic of the relay and power supply


WEB CONNECTION


Instead of the WiShield that I have used on the other projects, I decided to use a wired ethernet device since I have relatively easy access to ethernet ports at both thermostat locations and I can save a few bucks.  I ordered two Arduinio brand ethernet shields to serve this purpose.  However a little testing turned up three serious problems.  First, the darned things have a huge connector on one end directly over the USB connector on the Arduino itself.  This connector sticks up high enough to prevent putting another board on top because it will short out anything above it.  Second, the ethernet board won't connect properly when only power is applied.   It works fine if you apply power then press the reset, but for unattended operation this will lock up the control system.  Third, the board doesn't initialize to the network every time it is reset.  The symptoms are that the Rx led goes solid on and the link light blinks rapidly.  A search of the web turned up a couple of solutions for the power on reset problem, but the other two didn't seem to bother anyone else.

First, solving the physical size problem: This picture shows the ethernet board attached to the arduino.

Notice how the two plugs butt up against each other and it slightly raises the ethernet board.  However, it doesn't short anything out since the ethernet board has a couple of plastic parts that hold it away.  These are visible below.  
I decided this wasn't a bad enough problem to bother with.  However, notice how high the ethernet plug is in the two pictures above.  That means any prototype board I put on will have a real problem with shorting on the plug casing.  This was solved by adding an extra set of header pins to separate a prototype board from the metal plug.  The combination is shown below.
Here is an end on view that clearly shows the separation of the boards so I don't short out anything.

I could have put the prototype board in the middle, but I needed to constantly mess with it while developing the circuitry and having it in the middle would have been too big a hassle.  The extra header for spacing, although not an elegant solution, got the job done nicely. 

The second problem of the board not linking properly when powered on required some modifications to the board.  I cut the reset lines on the ethernet board and isolated the arduino reset pin so it could reset the ethernet board.  Then I wired the ethernet reset to arduino analog pin 1.  Programming the analog pin as a digital output with pull up enabled allowed me to reset the board during the setup routine.  This also lets me reset the ethernet board separately from the rest of the systems if I need to. 

The third problem of the link not initializing properly was tougher to figure out, but easier to fix.  When I finally realized it wasn't something in my programming or usage of the board, I connected a wire from the Rx led to digital pin 8 of the Arduino so I could sample the Rx signal.  With some experimentation I was able to come up with a way of telling that the link was invalid and then used the work above to reset the board and try again.  I've watched the board reset as many as three times before it got a solid connection with the network.
The combination of modifications allows the board to be reset by the arduino sketch each time it fails to get a connection.  I added additional code to the sketch to fully reset the arduino if the connection can't come up for some reason.  This is actually required because the entire thermostat implementation relies on a good time standard to keep usage to the minimum during peak periods.  It also gives me an accurate clock at each thermostat location.  Unfortunately, this took the ethernet board pin usage from 4 up to 6 and pretty much made me approach the limit for the Arduino.  Hope I don't think of something else I 'need'.

This is a good time to mention that the Arduino Ethernet board also uses analog pins 1 and 2 for no good reason.  Originally they were control pins for the SD card portion, but apparently that didn't work out and they didn't ship the board with the SD card capability.  However, they left two 10K resistors attached to the pins to mess up using the board.  I removed both of these resistors to free up the pins for other uses.  All things considered, this board was an annoying experience since the list of changes necessary to make it work unattended was so long, but it works well once modified and was relatively easy to program.  I don't have any experience with the newest version of this board, but one should be careful of pin usage problems and the link problem that gave me fits.

DISPLAY

The Sparkfun serial enabled LCD I show in the pictures is very nice to use but a real pain to put into an enclosure. Take a look at the picture below
Notice the real estate to the left of the display?  This area of the board makes the display hard to mount and eats up space on whatever it mounts to that can't be used for anything else.  I've ordered a different display that is the same white on black, but isn't serial enabled.  To connect it I also have a serial converter on order that will piggyback and should reduce the mounting footprint to something reasonable.  I'll edit this when the stuff comes in.

Update Dec 12, 2010: I got the new display stuff in.  I'm now using a LCD117 from Modern Device.  This little piggyback serial board is easy to assemble and can be configured for almost any LCD board that uses HD44780 controller chip.  This means I don't have to worry about 3V supplies or anything, just hook it up and go.  I also found this serial board much easier to use than the SparkFun device, but both of them are quite nice.  The LCD117 allows custom characters though and that can be a real plus.
I bought the LCD from a vendor on ebay.  I found a white on black display using the right controller chip and mated it to the serial board. This turned out to be really easy and gave me a reasonably easy to mount combination.



TEMPERATURE

I got an LM335A sensor and hooked it up.  It worked first try but was really sensitive and dealing with degrees Kelvin introduced weird problems.  I tried several things and finally gave up and ordered a TMP36 to try.  This has a nice range and the output voltage is high enough to deal with.  The only problem is that it works in degrees C and I need F.  It's easy to convert but there are some rounding problems since each degree C is 9/5 of a degree F.  I still haven't worked this out to my satisfaction but I haven't given up yet.  There was a small amount of sensitivity problems that I solved by reading the sensor 1000 times a second and then averaging the result as well as taking the seconds and keeping a rolling average over 10 seconds to stabilize it further.  These little things can respond to a gentle breeze and one doesn't want a compressor turning on and off with every little draft.

Update: Dec 12, 2010:  I fixed my problems with this sensor.  It seems the relays turning on and off were dropping the reference voltage a tiny bit that translated into a degree or two.  I moved the analog pins reference to the 3.3 volt supply on the Arduino and changed the calculations accordingly and it settled right down.  My smoothing will now hold it constant to a tenth of a degree while the relays act and then recover correctly in a second or two.  My brief testing so far shows that this will work great when I hook it up.


POWER

Almost all heating and cooling systems work off 24VAC.  That means I have a built in power supply if I rectify, filter and regulate it.  I found this cool device the SWADJ HV that is a DC - DC switching regulator that will handle 60V on its input.  That means only a little heat to dissipate and a really small part count.  I use a bridge rectifier to feed it and an electrolytic to filter and have a nice 5V power supply that runs my relays, and the various boards.  Don't use a normal 5V regulator here, the 24VAC filtered and rectified is around 37VDC and will blow out a lot of regulators (don't ask how I know, I'll lie about blowing them up).


Update 3/3/2012: I found this device, it has a lower maximum voltage and a slightly larger form factor, but it's a lot cheaper. http://www.gravitech.us/35v1aswvore.html   I haven't tried this device yet, so I can't tell you how good or bad it is.


CONTROL RELAYS

I used the Omron G5V-1; it's a tiny 5V relay that can handle the current necessary to close the heat pump contactors.  Remember, the heat pump control voltages are only 24VAC and they just need a few milliamps to work.  These little guys fit the .1 inch spacing on the prototype and breadboards so they can be nicely mounted and have a coil current of 30ma so they can hook directly to the Arduino.


FINAL FORM FACTOR

Jan 10, 2011: Decided on using a PacTec cover for a duplex outlet.  Below is the bezel for the LCD display and the ragged hole I cut to hold it.

 This bezel is GREAT.  No more filing and worrying about exactly fitting the LCD display.  The depth of the bezel matches the plastic on the faceplate and I can mount the LCD display directly behind it.  The bezels are available from DigiKey, I used PRD250LPW-ND that requires a couple of adhesive strips to hold it on.  Below is how it looked when I got the bezel and LCD display mounted.


The hardest part so far is the keypad for controlling temp and mode local to the thermostat.  I think I have it finally:


I'm still working on the power supply, but I have both locations prepped with the heat pump control lines and ethernet connection.  Just a couple more parts to receive and I'll be testing it live.

14 Jan 2010:  Got the parts in and everything worked just fine.  I have one of them installed and the other one is ready.  It works great.  Here's a picture of it in actual operation:


OPERATION

The basics of a thermostat are available all over the web and I stole rampantly from everywhere I could find, but there wasn't much on web controls.  Additionally, I started to run out of memory on the little Arduino so I'm just using very basic commands.  The web server on the Arduino will report the settings if you interrogate  it and it can respond to a few simple commands.  

URL/cooling will change the mode to cooling; 
URL/heating will make it into a heater; 
URL/off will turn it off; 
URL/fan=[on] [auto][recirc] controls the fan; 
URL/temp=nn will set the temperature. 
URL/status  will return a comma separated string that shows the various items
URL/fudge=nn  allows the temperature correction to be set 
URL/peakon  turns on peak period special handling
URL/peakoff  disables peak period special handling
URL/save  forces a save to eeprom of the settings
URL/reset  reboots the arduino (just in case)

 I put in a table of peak demand times and simply don't allow the compressor to even turn on during peak periods.  As I said, I'm down to about 200 bytes of data memory on the Arduino so I can't shoehorn too much more in.  My plan is to put more sophisticated control into yet another machine that will report and control the various devices in the house.

17 Jan 2011: Well, a bug had to creep in, even with all the testing I did.  I ran out of memory.  Above, notice that I was down to around 200 bytes?  Well, this wasn't enough so it started failing at random times in random ways.  I discovered that I could use PSTR("some text") to handle literals and away I went.  I modified almost all the sprintf calls and a lot of other stuff to use program memory instead of the RAM and got back enough memory to prevent failures like this.  I want people who actually read this to notice how long this project has taken.  These things don't come easy.

13 Feb, 2011:  Yet another bug, I forgot the save the fan setting into the eeprom.  When I updated them this time I installed a USB cable on the boards and left it inside the wall.  This way I don't have to remove the thermostat to update it, just pull the cord out of the wall and plug it in.  Nice, this will make life a lot easier as I update them over time.  I may look for a USB plug to put on the face of the device to make this even easier.  Another thing, I may pull the protoboard off the top and add some transistors to increase the current capability of the Arduino pins.  I'm only running them at 30ma but the rating is for 40 and I may be causing an early failure.  Got time to think about this though (there not much room on the board).

14 Feb, 2011: Yet another stinking bug.  Seems  "if (strstr_P,(Dbuf2,PSTR("GET /reset")) != 0){" will compile just fine but always tests true.  If you look closely there is a misplaced comma in there that took me all day to find.  I went ahead and modified the thermostat code to use the house clock and both of them seem to be working fine.  At least until the next bug....

8 July, 2011:  This isn't actually a bug.  I found out that the heat pump switchover valve is meant to operate differently than my original research indicated.  When you set the thermostat to heat, the valve moves to heat and stays there; same with cooling.  I had the switchover valve turning off each time I cycled the compressor and this was causing noise and strange operation.  I modified the code slightly to leave the valve in the last used position and the problems went away.

SKETCH
// Web enabled thermostat code.  Of course it's open source.
// pin usage:
// removed the 10K pullups from the Arduino ethernet board
// to free up analog pins 0 and 1  (not needed)
// Analog 0 temperature sensor input
// Analog 1 arduino ethernet board reset
// Analog 2
// Analog 3 pushbutton
// Analog 4 pushbutton
// Analog 5 pushbutton
// Analog 6 pushbutton
// Digital 0 Serial input
// Digital 1 Serial output
// Digital 2 Arduino ethernet board
// Digital 3
// Digital 4 Serial out to LCD display
// Digital 5 Fan control
// Digital 6 Compressor control
// Digital 7 Heat pump switchover control
// Digital 8 Sense for the ethernet card reset
// Digital 9
// Digital 10 Arduino ethernet board
// Digital 11 Arduino ethernet board
// Digital 12 Arduino ethernet board
// Digital 13 Arduino ethernet board
//
#include <avr/interrupt.h>
#include <avr/io.h>
#include <SPI.h>
#include <Ethernet.h>
#include <SoftwareSerial.h>
#include <Time.h>
#include <EEPROM.h>
#include <MemoryFree.h>

#define north  // comment this out for South Thermostat
#ifdef north
#define nameString "North"
#else
#define nameString "South"
#endif

#define tzOffset -7
time_t tNow;
// Enter a MAC address and IP address for controller below.
// The IP address will be dependent on your local network:
#ifdef north
byte ip[] = { 192,168,0,202 };
byte mac[] = {  0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };
#else
byte ip[] = { 192,168,0,203 };
byte mac[] = {  0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xEE };
#endif
byte gateway_ip[] = {192,168,0,1}; // router or gateway IP address
byte subnet_mask[] = {255,255,255,0}; // subnet mask for the local network
//byte NISaddress[] = { 128,138,188,172 }; // NIS  (not used anymore)
byte SatClockaddress[] = {192,168,0,204};  //House satellite clock

boolean Clockconnected = false;

// Initialize the Ethernet client library
// with the IP address and port of the server
// that you want to connect to (port 80 is default for HTTP):
//Client NISserver(NISaddress, 13);
Client SatClock(SatClockaddress, 80);
Server Control(80);
#define aref_voltage 3.3
#define TempPin 0
#define FanPin 5
#define CompressorPin 6
#define CoolControlPin 7
#define rxSense 8
#define tempupPin A3
#define tempdownPin A2
#define fanmodePin A4
#define compressormodePin A5

#define ON 1
#define OFF 0
#define CHECK 5
#define COOLING 55 //mode settings
#define HEATING 56
#define FanRECIRC 57 // three modes for the fan, Recirc, auto and on.
#define FanON 58     // there is no off to protect the compressor
#define FanAUTO 59

int Cvalue, Fvalue;
char Dbuf[50];  //general purpose buffer for strings
char Dbuf2[50];
char Dbuf3[100];  // this is only for sprintf stuff
int thisHour, thisDay;

struct config_t
{
  long magic_number;
  int tempSetting;
  int modeSetting; //heat or cool
  int fanMode; // recirc, on, auto
  int Hysteresis; //kick in at one degree beyond setting and out at one degree past it
  float TempFudge;
  int tempHigh;
  int tempLow;
} configuration, tempconfig;

template <class T> int EEPROM_writeAnything(int ee, const T& value)
{
    const byte* p = (const byte*)(const void*)&value;
    int i;
    for (i = 0; i < sizeof(value); i++)
 EEPROM.write(ee++, *p++);
    return i;
}

template <class T> int EEPROM_readAnything(int ee, T& value)
{
    byte* p = (byte*)(void*)&value;
    int i;
    for (i = 0; i < sizeof(value); i++)
 *p++ = EEPROM.read(ee++);
    return i;
}
int isPeak;
int peakOverride = false;
#define PEAKSTART 0
#define PEAKEND 1
// entries are: peak start hour, peak end hour
const byte peakArray [7][2] = { {99,0},   //Sunday no peak period
                          {12,19},    //Monday
                          {12,19},    //Tuesday
                          {12,19},    //Wednesday
                          {12,19},    //Thursday
                          {12,19},    //Friday
                          {99,0} }; //Saturday no peak period

//
// heat pump routines
//
int Fan(int mode){
  if (mode == ON && digitalRead(FanPin) == LOW){
    digitalWrite(FanPin, HIGH);
//    Serial.println("Fan On");
  }
  if (mode == OFF && digitalRead(FanPin) == HIGH){
    digitalWrite(FanPin, LOW);
//    Serial.println("Fan Off");
  }
  return(digitalRead(FanPin));
}
int Compressor(int mode){
  if (mode == ON && digitalRead(CompressorPin) == LOW){
    digitalWrite(CompressorPin, HIGH);
//    Serial.println("Compressor On");
  }
  else if (mode == OFF && digitalRead(CompressorPin) == HIGH){
    digitalWrite(CompressorPin, LOW);
//    Serial.println("Compressor Off");
  }
  return(digitalRead(CompressorPin));
}
int Cool(int mode){
  if (mode == ON && digitalRead(CoolControlPin) == LOW){
    digitalWrite(CoolControlPin, HIGH);
//    Serial.println("Cool On");
  }
  else if (mode == OFF && digitalRead(CoolControlPin) == HIGH){
    digitalWrite(CoolControlPin, LOW);
//    Serial.println("Heat On");
  }
  return(digitalRead(CoolControlPin));
}
int recirc = 0;
void HandleHeatpump(){
  /* Recirculation (evens the room temperatures)
   if it's 5 minutes after the hour, and the fan is not already running,
   turn it on for 10 minutes.  Do the same thing at 30 minutes after the hour.
   on the second thermostat it'll start at 16 and 41 past the hour for 10 minutes.
   05 - 15 North one on
   16 - 26 South one on
   30 - 40 North one on
   41 - 51 South one on
   I have to check to be sure the system isn't already on cooling or heating.
   Additionally, the two systems have to be controlled to be
   sure both systems aren't recirculating at the same time (Controlling Peak Demand)
   The reason for checking to see if the fan is already on is because the system
   may be in a heating or cooling cycle.
   */
  if (configuration.fanMode ==  FanRECIRC){
#ifdef north
    if ((minute() >= 5) && (minute() <15) && (Fan(CHECK) == OFF)){
#else
    if ((minute() >= 16) && (minute() <26) && (Fan(CHECK) == OFF)){
#endif
      Fan(ON);
      recirc = 1;
    }
    // never turn the fan off if the compressor is on
#ifdef north
    if ((minute() >= 15) && (minute() < 30)&& (Compressor(CHECK) == OFF) && (Fan(CHECK) != OFF)){
#else
    if ((minute() >= 26) && (minute() < 41)&& (Compressor(CHECK) == OFF) && (Fan(CHECK) != OFF)){
#endif
      Fan(OFF);
      recirc = 0;
    }
#ifdef north
    if ((minute() >= 30) && (minute() < 40) && (Fan(CHECK) == OFF)){
#else
    if ((minute() >= 41) && (minute() < 51) && (Fan(CHECK) == OFF)){
#endif
      Fan(ON);
      recirc = 1;
    }
    // minutes work in groups of 60 so I don't have to check the high
#ifdef north
    if ((minute() >= 40) && (Compressor(CHECK) == OFF) && (Fan(CHECK) != OFF)){
#else
    if ((minute() >= 51) && (Compressor(CHECK) == OFF) && (Fan(CHECK) != OFF)){
#endif
      Fan(OFF);
      recirc = 0;
    }
  }
  else if (configuration.fanMode == FanAUTO) {
    // if the fan is running, and not the compressor, turn it off
    if(Fan(CHECK) == ON && Compressor(CHECK) == OFF)
      Fan(OFF);
  }
  else if (configuration.fanMode == FanON)
    Fan(ON);
  else
    Serial.println("Fan control error");
  
  if (isPeak && peakOverride == false){
    Compressor(OFF); //No compressor use during peak billing periods.
    Cool(OFF);
    return;
  }

  if (configuration.modeSetting == COOLING){
    if (Fvalue > configuration.tempSetting && Compressor(CHECK) == OFF){
      Fan(ON);
      Cool(ON);
      Compressor(ON);
      strcpy_P(Dbuf3,PSTR("%2d:%02d Cooling started at %dF for %dF"));
      sprintf(Dbuf,Dbuf3,hour(),minute(),Fvalue, configuration.tempSetting);
      Serial.println(Dbuf);
    }
    else if (Fvalue <= (configuration.tempSetting - configuration.Hysteresis) && Compressor(CHECK)==ON){
       Compressor(OFF);
       Cool(OFF);
       if (recirc == 0)
         Fan(OFF);
       strcpy_P(Dbuf3,PSTR("%2d:%02d Cooling stopped at %dF for %dF"));
       sprintf(Dbuf,Dbuf3,hour(),minute(),Fvalue, configuration.tempSetting);
       Serial.println(Dbuf);
     }
  }
  else if (configuration.modeSetting == HEATING){
    if (Fvalue < configuration.tempSetting && Compressor(CHECK) == OFF){
      Fan(ON);
      Cool(OFF);
      Compressor(ON);
      strcpy_P(Dbuf3,PSTR("%2d:%02d Heating started at %dF for %dF"));
      sprintf(Dbuf,Dbuf3,hour(),minute(),Fvalue, configuration.tempSetting);
      Serial.println(Dbuf);
    }
    else if (Fvalue >= (configuration.tempSetting + configuration.Hysteresis) && Compressor(CHECK)==ON){
       Compressor(OFF);
       Cool(OFF);  // just to save wear on contactor
       if (recirc == 0)
         Fan(OFF);
       strcpy_P(Dbuf3,PSTR("%2d:%02d Heating stopped at %dF for %dF"));
       sprintf(Dbuf,Dbuf3,hour(),minute(),Fvalue, configuration.tempSetting);
       Serial.println(Dbuf);
     }
  }
  else if (configuration.modeSetting == OFF){
    Compressor(OFF);
    Cool(OFF);
    if (recirc == 0)
      Fan(OFF);
  }
  else
    Serial.println("Compressor mode error");
}

#define InterruptOff  TIMSK2 &= ~(1<<TOIE2)
#define InterruptOn  TIMSK2 |= (1<<TOIE2)
volatile long TempReadCount = 0;
volatile long Temperature = 0;
volatile int int_counter = 0;
volatile int seconds = 0;
int oldSeconds = 0;
unsigned int tcnt2; //used for timer value

// Aruino runs at 16 Mhz, so we have 1000 Overflows per second...
// this little routine will get hit 1000 times a second.
ISR(TIMER2_OVF_vect) {
  long junk;
  int i;

  int_counter++;
  if (int_counter == 1000) {
    seconds+=1;
    int_counter = 0;
  }
//  junk = 0;  // oversampling according to Atmel avr121 app note
//  for(i=0; i<16; i++)
//    junk += analogRead(TempPin);  //Oversampling
//  Temperature += (junk >> 4);
  Temperature += analogRead(TempPin);
  TempReadCount ++;
  TCNT2 = tcnt2;  //reset the timer for next time
}

// Timer setup code borrowed from Sebastian Wallin
// http://popdevelop.com/2010/04/mastering-timer-interrupts-on-the-arduino/
// This was the best explanation I found on the web.

void setupTimer()
{
  //Timer2 Settings:  Timer Prescaler /1024
  // First disable the timer overflow interrupt while we're configuring
  TIMSK2 &= ~(1<<TOIE2);
  // Configure timer2 in normal mode (pure counting, no PWM etc.)
  TCCR2A &= ~((1<<WGM21) | (1<<WGM20));
  // Select clock source: internal I/O clock
  ASSR &= ~(1<<AS2);
  // Disable Compare Match A interrupt enable (only want overflow)
  TIMSK2 &= ~(1<<OCIE2A);

  // Now configure the prescaler to CPU clock divided by 128
  TCCR2B |= (1<<CS22)  | (1<<CS20); // Set bits
  TCCR2B &= ~(1<<CS21);             // Clear bit

  /* We need to calculate a proper value to load the timer counter.
   * The following loads the value 131 into the Timer 2 counter register
   * The math behind this is:
   * (CPU frequency) / (prescaler value) = 125000 Hz = 8us.
   * (desired period) / 8us = 125.
   * MAX(uint8) - 125 = 131;
   */
  /* Save value globally for later reload in ISR */
  tcnt2 = 131;

  /* Finally load end enable the timer */
  TCNT2 = tcnt2;
  TIMSK2 |= (1<<TOIE2);
  sei();
}
#define LCDrxPin 3
#define LCDtxPin 4
SoftwareSerial mySerial = SoftwareSerial(LCDrxPin, LCDtxPin);
//
//  LCD display routines
//


void setupLCD()
{
  pinMode(LCDtxPin, OUTPUT);
  mySerial.begin(9600);
  mySerial.print("?Bff");   // Intensity to max
  delay(100);
  mySerial.print("?c0");    // No Cursor
//  mySerial.print("?G216");  // 2x16 display (only have to do this once)
  mySerial.print("?f");     //clear command.
  delay(100);

}
void LCDline1()
{
  mySerial.print("?y0");   //Row 0
  mySerial.print("?x00");    //Column 0
}
void LCDline2()
{
  mySerial.print("?y1");   //Row 1
  mySerial.print("?x00");    //Column 0
}

#define numReadings 10 // these items used for smoothing temp readings
float readings[numReadings];
int index = 0;
float total = 0.0;

void HandleTemp(){ // lots of smoothing in here.  also, uses floats to keep it from jumping
  float T;         // by whole degrees.

  InterruptOff; //so it doesn't change while getting it.
  T = Temperature / TempReadCount;  //get the average of the readings taken during interrupts
  Temperature = 0;                                //and start over for the next second
  TempReadCount = 0;
  InterruptOn;

  total -= readings[index];  // rolling average over 'numReadings'
  readings[index] = T;
  total += T;
  index++;
  if (index >= numReadings)
    index = 0;
  T = total / (float)numReadings;  //average these readings also (smooth it out even more)
  T = T * 3.3 / 1024;
  T = (T - 0.5) * 100.0 + configuration.TempFudge;
//  Serial.print("temp=");
//  Serial.print(T,DEC);
//  Serial.print("C ");
//  Serial.print((T * 9.0)/5.0 + 32.0);
//  Serial.println("F");
  Cvalue = round(T);
  Fvalue = round((T * 9.0)/5.0 + 32.0);
}
int lastF;
char Status[8];

void HandleDisplay(){
//  if ((second() % 60) == 0){  // used in debugging the code
//   sprintf(Dbuf, "Temperature=%dC %dF ",Cvalue,Fvalue);
//   Serial.print(Dbuf);
//   if (isPeak)
//      Serial.println("Peak");
//    else
//      Serial.println("Off Peak");
//  }
  strcpy_P(Status,PSTR("BUG"));
  if ((Compressor(CHECK) == OFF) && (Fan(CHECK) == OFF))
    strcpy_P(Status,PSTR("Idle"));
  if ((Compressor(CHECK) == OFF) && (Fan(CHECK) == ON))
    strcpy_P(Status, PSTR("Recirc"));
  if ((Compressor(CHECK) == ON) && (Cool(CHECK) == OFF))
    strcpy_P(Status, PSTR("Heating"));
  if ((Compressor(CHECK) == ON) && (Cool(CHECK) == ON))
    strcpy_P(Status, PSTR("Cooling"));

//  if (lastF != Fvalue){
//    sprintf(Dbuf,"%d:%2d %d", hour(),minute(),Fvalue - lastF);
//    Serial.println(Dbuf);
//  }
  lastF = Fvalue;
  
  strcpy_P(Dbuf3,PSTR("%3dF %7s  %s"));
  sprintf(Dbuf, Dbuf3,Fvalue,Status, isPeak?"Pk":"  ");

  strcpy_P(Dbuf3,PSTR("%3s %2d:%02d%c %c%c %2d"));
  sprintf(Dbuf2, Dbuf3,dayShortStr(weekday()),hourFormat12(),minute(),(isAM()?'A':'P'),
           (configuration.modeSetting==COOLING?'C':(configuration.modeSetting==HEATING?'H':'O')),
           configuration.fanMode == FanRECIRC?'R':(configuration.fanMode==FanON?'O':'A'),
           configuration.tempSetting);

  InterruptOff; //LCD display goes nuts if interrupted.
  LCDline1();
  mySerial.print(Dbuf);
  LCDline2();
  mySerial.print(Dbuf2);
  InterruptOn;
}

boolean trySetTime(){
  char* ptr;
  int cnt, myyear, mymonth, myday, myhour, myminute, mysecond;
  int waitcnt = 0;

  ptr = Dbuf;
  while(1){
    if (!Clockconnected){
//      Serial.println("Connecting");
      if (SatClock.connect()) {
        Clockconnected = true;
        SatClock.println("GET /time_t HTTP/1.0");
      }
      else{
        Serial.println("failed");
      }
    }
    if (SatClock.available() > 0){
       while (SatClock.available()) {
         *ptr++ = SatClock.read();
       }
       SatClock.flush(); //suck out any extra chars
       SatClock.stop();
       while (SatClock.status() != 0){
         delay(5);
       }
       Clockconnected = false;
    
       *ptr='\0';
//       Serial.println(Dbuf); //debug only
       ptr = Dbuf + 44; //time value starts 44 bytes in
       tNow = 0;
       while ( isdigit(*ptr)){
         tNow = tNow * 10 +(*ptr++ - '0');
       }
       setTime(tNow);
       tNow = now();
       strcpy_P(Dbuf3,PSTR("%02d/%02d/%4d %02d:%02d:%02d "));
       sprintf(Dbuf,Dbuf3,month(tNow),day(tNow),year(tNow),hour(tNow),minute(tNow),second(tNow));
       Serial.print(Dbuf);
       strcpy_P(Dbuf,PSTR("time sync complete"));
       Serial.println(Dbuf);
       return(true);
    }

    if (!Clockconnected){
      SatClock.flush();
      SatClock.stop();
      while (SatClock.status() != 0){
        delay(5);
      }
    }
    if (waitcnt++ < 10){
//      Serial.println("waiting");  // for debugging
      mySerial.print('*');
      delay (1000);
    }
    else{
      SatClock.flush();
      SatClock.stop();
      while (SatClock.status() != 0){
        delay(5);
      }
      return(false);
    }
  }
}
void(* resetFunc) (void) = 0; //declare reset function @ address 0

time_t SetTime(){
  if(trySetTime() == false)
    resetFunc();  //call reset if you can't get the comm to work
  return(now());
}

void eepromadjust(){
  EEPROM_readAnything(0, tempconfig); // get saved settings
  if(configuration.tempSetting != tempconfig.tempSetting ||
    configuration.modeSetting != tempconfig.modeSetting ||
    configuration.fanMode != tempconfig.fanMode ||
    configuration.Hysteresis != tempconfig.Hysteresis ||
    configuration.TempFudge != tempconfig.TempFudge ||
    configuration.tempHigh != tempconfig.tempHigh ||
    configuration.tempLow != tempconfig.tempLow){
      strcpy_P(Dbuf, PSTR("configuration updated"));
      Serial.println(Dbuf);
      EEPROM_writeAnything(0, configuration);
    }
}
void ethernetReset(){
  bool rxState;
  int cnt = 10, result;

  pinMode(rxSense, INPUT);  //for stabilizing the ethernet board
  pinMode(A1,INPUT);
  while(1){
    digitalWrite(A1, HIGH);
    pinMode(A1, OUTPUT);
    digitalWrite(A1, LOW);  // ethernet board reset
    delay(100);
    digitalWrite(A1, HIGH);
    delay(2000);
  // now, after the reset, check the rx pin for constant on
    cnt = 10;
    result = 0;
    while (cnt-- != 0){  // simply count the number of times the light is on in the loop
      result += digitalRead(rxSense);
      delay(50);
    }
    Serial.println(result,DEC);
    if (result >=6)      // experimentation gave me this number YMMV
      return;
    delay(50);
  }
}

void setup() {
  Serial.begin(57600);
  strcpy_P(Dbuf, PSTR("Heat Pump Controls Initialization"));
  Serial.println(Dbuf);

  analogReference(EXTERNAL);
  pinMode(FanPin, OUTPUT);
  pinMode(CompressorPin, OUTPUT);
  pinMode(CoolControlPin, OUTPUT);
  Fan(OFF);
  Compressor(OFF);
  Cool(OFF);
  // pushbutton control pins
  pinMode(tempupPin, OUTPUT);
  digitalWrite(tempupPin, HIGH);
  pinMode(tempdownPin, OUTPUT);
  digitalWrite(tempdownPin, HIGH);
  pinMode(fanmodePin, OUTPUT);
  digitalWrite(fanmodePin, HIGH);
  pinMode(compressormodePin, OUTPUT);
  digitalWrite(compressormodePin, HIGH);
  // get saved configuration
  EEPROM_readAnything(0, configuration); // get saved settings
  if (configuration.magic_number != 1234){ //this will set it up for very first use
    configuration.magic_number = 1234;
    configuration.tempSetting = 78;
    configuration.modeSetting = COOLING;
    configuration.fanMode = FanRECIRC;
    configuration.Hysteresis = 1;
    configuration.TempFudge = 1.0;
    configuration.tempHigh = 60;
    configuration.tempLow = 60;
    EEPROM_writeAnything(0, configuration);
  }
  strcpy_P(Dbuf3,PSTR("Set for %s at %d"));
  sprintf(Dbuf,Dbuf3,configuration.modeSetting==COOLING?"Cooling":
     (configuration.modeSetting==HEATING?"Heating":"Off"),
     configuration.tempSetting);
  Serial.println(Dbuf);
  strcpy_P(Dbuf3,PSTR("Hysteresis=%d and Temp correction="));
  sprintf(Dbuf,Dbuf3,configuration.Hysteresis);
  Serial.print(Dbuf);
  Serial.println(configuration.TempFudge);
  strcpy_P(Dbuf, PSTR("Initializing LCD Display"));
  Serial.println(Dbuf);
  setupLCD();
  strcpy_P(Dbuf, PSTR("Temp Init..."));
  mySerial.print(Dbuf);    //let them know it's alive
  // start the Ethernet connection:
  // first, reset the board and make sure it intialized correctly
  ethernetReset();
  mySerial.print('*'); // just to let the person know it's alive
  // now give it an address
  Ethernet.begin(mac, ip, gateway_ip, subnet_mask);
  delay(1000);  // wait a bit for the card to stabilize
  strcpy_P(Dbuf, PSTR("time request"));
  Serial.println(Dbuf);
  setSyncInterval(30 * 60);  //update time every 30 minutes
  setSyncProvider(SetTime); //this will immediately go get the time
  // the SetTime routine will reset the board and start over if it can't
  // sync the time
  Control.begin(); // start the web control server

  strcpy_P(Dbuf, PSTR("starting timerinterrupt"));
  Serial.println(Dbuf);
  setupTimer();

}

int firsttime = true;
void loop() {
  int index = 0;

  if(firsttime == true){  // keep running out of memory!!
    Serial.print("Mem=");
    Serial.println(freeMemory());
    firsttime = false;
  }
  if(int_counter % 250 == 0){
    if (digitalRead(tempupPin) == LOW){
      Serial.println("Temp up");
      configuration.tempSetting++;
      HandleDisplay();
    }
    if (digitalRead(tempdownPin) == LOW){
      Serial.println("Temp down");
      configuration.tempSetting--;
      HandleDisplay();
    }
    if (digitalRead(fanmodePin) == LOW){
      Serial.println("Fan button");
      if (configuration.fanMode == FanRECIRC)
        configuration.fanMode = FanAUTO;
      else if (configuration.fanMode == FanAUTO)
        configuration.fanMode = FanON;
      else if (configuration.fanMode == FanON)
        configuration.fanMode = FanRECIRC;
      HandleDisplay();
   }
    if (digitalRead(compressormodePin) == LOW){
      Serial.println("Compressor button");
      if (configuration.modeSetting == COOLING){
        configuration.modeSetting = OFF;
      }
      else if (configuration.modeSetting == OFF){
        configuration.modeSetting = HEATING;
      }
      else if (configuration.modeSetting == HEATING){
        configuration.modeSetting = OFF;
        HandleHeatpump();
        configuration.modeSetting = COOLING;
      }
      HandleDisplay();
   }
  }
  
  if (oldSeconds != seconds) {
    if (seconds > 10000)  // integers go to 32K and roll negative, so fix it.
      seconds = 31;       // set it to 31 to prevent temperature redoing average (below)
    oldSeconds = seconds;
    if(second() % 60 == 0 && minute() % 5 == 0)  //check to see if the configuration needs saving
      eepromadjust();                            //every five minutes or so
    
    thisHour = hour();
    thisDay = weekday()-1;
    if (thisHour < (int)peakArray[thisDay][PEAKSTART] || thisHour >= (int)peakArray[thisDay][PEAKEND])
        isPeak = false;
    else
        isPeak = true;
  
    HandleTemp();
    if (thisHour == 0 && peakOverride == true) //just in case it was left overridden
      peakOverride = false;                    //put it back for tomorrow
    if (seconds > 30) // to allow temperature to average out when first started
      HandleHeatpump();
    HandleDisplay();  
  
  }
  //web server control code
   Client ControlClient = Control.available();
   if (ControlClient) {
    // an http request ends with a blank line
    while (ControlClient.connected()) {
      if (ControlClient.available()) {
        char c = ControlClient.read();
        // if you've gotten to the end of the line (received a newline
        // character) you can send a reply
        if (c != '\n' && c!= '\r') {
          Dbuf2[index++] = c;
          if (index >= sizeof(Dbuf)) // max size of buffer
            index = sizeof(Dbuf) - 1;
          continue;
        }
        Dbuf2[index] = 0;
        Serial.println(Dbuf2);
        // standard http response header
        strcpy_P(Dbuf, PSTR("HTTP/1.1 200 OK"));
        ControlClient.println(Dbuf);
        strcpy_P(Dbuf, PSTR("Content-Type: text/html"));
        ControlClient.println(Dbuf);
        ControlClient.println();
        if (strstr_P(Dbuf2, PSTR("GET / ")) != 0){ // root level request, send status  
          // standard http response header
          strcpy_P(Dbuf, PSTR("<html><body><h1>"));
          ControlClient.print(Dbuf);
          ControlClient.print(nameString);
          strcpy_P(Dbuf, PSTR(" Thermostat<br>"));
          ControlClient.print(Dbuf);
          // output the time
          tNow = now();
          strcpy_P(Dbuf3,PSTR("%02d/%02d/%4d %02d:%02d:%02d <br>"));
          sprintf(Dbuf,Dbuf3,month(tNow),day(tNow),year(tNow),hour(tNow),minute(tNow),second(tNow));
          ControlClient.print(Dbuf);
          strcpy_P(Dbuf3,PSTR("Current Temp: %d <br>"));
          sprintf(Dbuf,Dbuf3,Fvalue);
          ControlClient.print(Dbuf);
        
          strcpy_P(Dbuf3, PSTR("Current Status: %s <br><br>"));
          sprintf(Dbuf, Dbuf3, Status);
          ControlClient.print(Dbuf);
        
          strcpy_P(Dbuf3,PSTR("Settings:<br>Temp: %d<br>Mode: %s<br>Fan Mode: %s<br>Peak Handling: %s<br>Hysteresis: %d<br>"));
          sprintf(Dbuf,Dbuf3,
              configuration.tempSetting, configuration.modeSetting == COOLING?"Cooling":
              (configuration.modeSetting==HEATING?"Heating":"Off"),
              configuration.fanMode == FanRECIRC?"Recirc":(configuration.fanMode==FanON?"On":"Auto"),
              peakOverride == true ? "OFF" : "ON",
              configuration.Hysteresis);
          ControlClient.print(Dbuf);
          strcpy_P(Dbuf3,PSTR("Temp Correction: %d.%01d<br>"));
          sprintf(Dbuf,Dbuf3,
              (int)configuration.TempFudge,(int)(configuration.TempFudge * 10) % 10);
          ControlClient.print(Dbuf);
          strcpy_P(Dbuf, PSTR("<br></h1></body></head></html>"));
          ControlClient.println(Dbuf);
          break;
        }
        else if (strstr_P(Dbuf2,PSTR("GET /status")) != 0){ //for other machines
          strcpy_P(Dbuf3,PSTR("%d,%s,%d,%s,%s"));
          sprintf(Dbuf,Dbuf3,Fvalue,Status,configuration.tempSetting,
          configuration.modeSetting == COOLING?"Cooling":(configuration.modeSetting==HEATING?"Heating":"Off"),
          configuration.fanMode == FanRECIRC?"Recirc":(configuration.fanMode==FanON?"On":"Auto")
          );
          ControlClient.println(Dbuf);
          break;
        }
        else if (strstr_P(Dbuf2,PSTR("GET /cool")) != 0){
          configuration.modeSetting = OFF;
          HandleHeatpump();
          configuration.modeSetting = COOLING;
          strcpy_P(Dbuf,PSTR("Web, Mode = Cooling"));
          ControlClient.println(Dbuf);
          break;
        }
        else if (strstr_P(Dbuf2,PSTR("GET /heat")) != 0){
          configuration.modeSetting = OFF;
          HandleHeatpump();
          configuration.modeSetting = HEATING;
          strcpy_P(Dbuf, PSTR("Web, Mode = Heating"));
          ControlClient.println(Dbuf);
          break;
        }
        else if (strstr_P(Dbuf2,PSTR("GET /off")) != 0){
          configuration.modeSetting = OFF;
          HandleHeatpump();
          strcpy_P(Dbuf, PSTR("Web, Mode = Off"));
          ControlClient.println(Dbuf);
          break;
        }
        else if (strstr_P(Dbuf2,PSTR("GET /fan=recirc")) != 0){
          configuration.fanMode = FanRECIRC;
          strcpy_P(Dbuf, PSTR("Web, Fan = Recirc"));
          ControlClient.println(Dbuf);
          break;
        }
        else if (strstr_P(Dbuf2, PSTR("GET /fan=on")) != 0){
          configuration.fanMode = FanON;
          strcpy_P(Dbuf, PSTR("Web, Fan = On"));
          ControlClient.println(Dbuf);
          break;
        }
        else if (strstr_P(Dbuf2, PSTR("GET /fan=auto")) != 0){
          configuration.fanMode = FanAUTO;
          strcpy_P(Dbuf, PSTR("Web, Fan = Auto"));
          ControlClient.println(Dbuf);
          break;
        }
        else if (strstr_P(Dbuf2, PSTR("GET /peakoff")) != 0){
          peakOverride = true;
          strcpy_P(Dbuf, PSTR("Web, Peak handling OFF"));
          ControlClient.println(Dbuf);
          break;
        }
        else if (strstr_P(Dbuf2, PSTR("GET /peakon")) != 0){
          peakOverride = false;
          strcpy_P(Dbuf, PSTR("Web, Peak handling ON"));
          ControlClient.println(Dbuf);
          break;
        }      
        else if (strstr_P(Dbuf2,PSTR("GET /temp=")) != 0){
          int tmpI;
          char* tmpS = strtok(Dbuf2,"="); //skip over the first part
          if ((tmpS = strtok(NULL,"=")) != NULL)
            tmpI = atoi(tmpS);
          if (tmpI > 40 && tmpI < 99)
            configuration.tempSetting = tmpI;
          else
            tmpS = NULL;
          if (tmpS == NULL){
            strcpy_P(Dbuf,PSTR("I\'m sorry Dave"));
            ControlClient.println(Dbuf);
          }
          else{
            strcpy_P(Dbuf, PSTR("Web, temp = "));
            Serial.print(Dbuf);
            Serial.println(configuration.tempSetting);
            strcpy_P(Dbuf, PSTR("Temp = "));
            ControlClient.print(Dbuf);
            ControlClient.println(configuration.tempSetting);
          }
          break;
        }
        else if (strstr_P(Dbuf2,PSTR("GET /reset")) != 0){ // remote reset, just for the heck of it
          resetFunc();
          break;  //Won't ever get here
        }
        else if (strstr_P(Dbuf2,PSTR("GET /save")) != 0){ // forced save to eeprom
          strcpy_P(Dbuf, PSTR("Saving "));
          ControlClient.println(Dbuf);
          eepromadjust();
          break;
        }
        else if (strstr_P(Dbuf2,PSTR("GET /fudge=")) != 0){
          float tmpD;
          char* tmpS = strtok(Dbuf2,"="); //skip over the first part
          if ((tmpS = strtok(NULL,"=")) != NULL){
            tmpD = atof(tmpS);
          }
          if (tmpD >= -5.0 && tmpD <= 5.0)
            configuration.TempFudge = tmpD ;
          else
            tmpS = NULL;
          if (tmpS == NULL){
            strcpy_P(Dbuf,PSTR("I\'m sorry Dave"));
            ControlClient.println(Dbuf);
          }
          else{
            strcpy_P(Dbuf,PSTR("Web, Fudge = "));
            Serial.print(Dbuf);
            Serial.println(configuration.TempFudge);
            strcpy_P(Dbuf,PSTR("Fudge = "));
            ControlClient.print(Dbuf);
            ControlClient.println(configuration.TempFudge);
          }
          break;
        }
        else{
          strcpy_P(Dbuf,PSTR("command error"));
          ControlClient.println(Dbuf);
          break;
        }
      }
     }
    }
    // give the web browser time to receive the data
    delay(1);
    // close the connection:
    ControlClient.stop();
}

20 comments:

  1. thanks for the lead on the swadjhv. i'm working on my own wifi thermostat and that is a key component.

    ReplyDelete
  2. Before you actually order it, take a look at http://www.gravitech.us/35v1aswvore.html. I haven't actually tried this one, but it is a lot cheaper.

    ReplyDelete
  3. Hey this is great stuff on here!
    I am new to the arduino and I too am setting up a HVAC controller. I am having very similar problems with the tmp36, large spikes in temperature when a relay is energized. I would love to implement your idea of taking 1000 reading per second then averaging again, did you use the oversampling for this? or is it just a large average? I understand the basics of oversampling, decimation and bit shift but I am finding it very difficult to translate into code. Any help would be awesome!

    Thanks,
    Chris

    ReplyDelete
  4. I just used a regular old average. I add the reading to a variable and keep count of the number of reads in another one. Then, I just divide the value by the number of reads. I also used the 3V supply to avoid the spikes that caused me so darn much trouble. To even it out even more, I took a 10 second rolling average to slow it down even more.

    The code for both of these is in the sketch above, feel free to steal it and use anything you want. The caveat is that if you come up with a better way, let me know. I may want to use it.

    ReplyDelete
  5. Thanks for sharing your design, Dave.

    Will the Sparkfun Arduino R3 work for this project (i.e. will it have enough memory)?

    https://www.sparkfun.com/products/11224

    I need to build an HVAC controller that has two temperature sensors to control an AC/Heater that currently has two thermostats controlling a single compressor. It appears your design might be a great starting point.

    Depending upon which thermostat has control a baffle shifts the compressor between the upstairs part of the house and the downstairs part. It is a crappy design and my AC is constantly freezing up causing huge spikes in my power bill. I would like to be able to lock out the upstairs thermostat during the day and enable it at night while disabling the downstairs unit.

    ReplyDelete
  6. The R3 Arduino should work fine for you. Notice above that I ran my devices out of memory with some stupid code and found a solution. You want to look at using PSTR() and a couple of other tricks to take care of wasting memory in these little devices. Also look at how I print how much memory is available; you'll need to keep a couple of hundred bytes free to allow for temporary variables and stack usage. But, with these minor warnings, it should do a good job for you.

    If I were to do it over again, I would think about a couple of things differently. For one, there are some really great relay boards for use with the Arduino. There are possibilities for the power supply that I didn't consider. I can code better now so it might have been a little less confusing...that kind of thing.

    Let me know how the project turn out, it sounds like you need to customize the thermostat for reasons similar to mine.

    ReplyDelete
  7. Thanks Dave. I am a rank beginner at this but I am a Linux admin and write shell scripts, so I am at least familiar with programming. I know a little c++ but am about to get a baptism by fire. I can read someone else's code, and if it is commented, I can make sense out of it.

    I saw a four relay board at Sparkfun that I was thinking might be a good match. What are your thoughts on a power supply? I planned to purchase the 9v wallwart that Sparkfun offers, but perhaps I should rethink that.

    I will definitely keep you posted. Where in Az do you live? I used to live in Mesa but Tucson, Flagstaff, & Prescott were my favorites. Currently living in Austin, Tx but trying to move back to NM, SW Colorado, or Az some day!

    ReplyDelete
  8. Take a look on ebay for 4 relay boards. I have one I got there that works great for a small price. I recommend a 2 amp wall wart for an iPad (note, iPad, they take a lot of power). These little devices are available for as little as 3-4 dollars and pack a lot of power. I use several of them for projects and have never had one fail. When you're running relays, the usual USB cable may not do the job because a lot of them won't carry over 500ma. So, look for a cable that can carry more current than that. There are cables that have a USB on one end and the coaxial plug on the other end that work well for long term installation.

    When you get it installed there will be changes you want to make over time. Make sure you think about reprogramming it while it's in the wall. I have a cable stuffed inside the wall that I pull out to program the thermostats. I walk over with a laptop and plug it in to make changes. Naturally, I learned this the hard way.

    I'm in New River, AZ, about half way between Phoenix and Black Canyon City, 10 miles or so North of Phoenix in the foothills at the end of 3/4 miles of dirt road that washes out every time we get a heavy rain.

    ReplyDelete
  9. I've been thinking of building an Arduino Thermostat for a couple years now, but the biggest thing stopping me is how to power the device. I live in an old apartment, and the thermostat is located on a wall that has no ready access to an outlet. I really like your idea of powering the Arduino using the 24VAC that powers the heating system itself. However, it is unclear to me in your schematic whether this design will work in my case. I live in the Northeast, and my old apartment only has heat (no AC). There are exactly two wires running into the thermostat. I already checked, they have 24VAC between them.
    Do you know if I can use your design to grab power from the same lines that the relay is controlling to turn on and off the heat? I would also like to have a backup battery in case of power outages.

    ReplyDelete
  10. My design won't work in your case. What you probably have is two wires that close a relay to turn on the heater. I had one of those in a house a while back and there just wasn't any way to get power to it. However, these days, things are a little different and there are lots of wires that can be repurposed.

    What I would do in your case is look around the thermostat and see if there is a power outlet within a reasonable distance, sometimes there is one on the opposite wall. Failing that, check and see if you can pull another wire. Sometimes they don't staple the thermostat wire to the studs and you can actually hook on and pull another wire from the heater. Also, check and see if there are more wires further back on the cable. Sometimes they only run two of them out the wall and leave the rest inside. Check at the heater end, there may be an indication of some extra wires there.

    If all that fails, you can use the two wires to supply power to the thermostat and then send the relay signal back to the heater with an XBee at the thermostat and another hooked to a relay at the heater. It's not hard to use an XBee to control a relay and you don't have to worry about wires.

    Where there's a will, usually we find a way.

    ReplyDelete
    Replies
    1. Thanks for your reply Dave. My apartment is in a very old New England house, so I would be surprised if it has anything but the two wires, but it isn't a bad idea to check. I would be worried about attempting to run new wires, as this is an apartment, and I've run into trouble trying to mess with the wiring in these old apartments (old insulation is fragile). As for a nearby outlet, the apartment itself doesn't have many outlets (again, old apartment), but worse the thermostat is positioned on a "column" formed by three doorways, isolating that section of wall from any outlets. The best I could do would be to run a power cable around one doorway, into another room, over another doorway, and about 5 feet down the wall. Less than ideal.

      I like the idea of hijacking the existing wires and using them as power wires, then just controlling a relay on the other end, I'll have to consider that.

      The "Nest Learning Thermostat" claims it is compatible with my wiring, and from what I can find online it uses the 24VAC for power (which from further research is called a "power stealing thermostat"), so it seems it is possible but one site I found said this technique is unreliable (but they hadn't specifically tested the Nest). My guess is that this type of thermostat has some kind of internal battery which is charged using a minimal amount of current from the 24VAC wires (not enough to trigger the heating system to turn on). When the relay is closed to turn the heat on, the system runs off the internal battery. This is just a guess, as I've failed to find anywhere online that says specifically how (and under what circumstances) a "power stealing" thermostat works.

      Oh well, guess I've got more research to do.

      Delete
  11. I can see how the power stealing thermostats could give trouble. The only article I found on this was (probably intentionally) vague and kept stating that running a common wire was the best way to do things. However, if you were to use a latching relay so that power is not required to keep it closed, and turn it off with the thermostat code, you could reduce the power consumption way, way down. My problem has always been that I want a nice display that I can see to tell me what is going on and those usually use more power than stealing it would provide.

    XBees are pretty easy to learn about and use, but there is the investment of two of them and some method of programming them. After I started using one of them for a simple job and got used to messing with them, and the initial investment of a USB adapter to program them, I went nuts. I have them hooked to battery chargers, pump controls, controllers, garage door openers, and soon, light switches.

    Hint, I pick them up (Series 2, wire antenna) for 17 bucks from Digikey. Most places charge a heck of a lot more than that. Of course, you need a power supply for both ends and a bunch of other stuff, but that's part of the fun. I found out that the LM317 voltage regulator will work as long as the voltage difference from input to output is less than 40V; that means it is a possibility for using the 24VAC from the heater. That would make the power supply a lot easier than the method I used. The filtered and rectified voltage from the 24VAC runs around 37VDC so it means the 317 could handle it. Haven't tried it yet though.

    Good luck and have fun.

    ReplyDelete
  12. Dave, your blog here has inspired me to start building my own power monitor and super thermostat. One comment on how I think you can improve yours. I'm starting off using a DHT22 temperature-humidity sensor (https://www.adafruit.com/products/385). It's really nice as it completely eliminates the need to do temperature averaging in the software like you are. It is naturally a slow moving sensor that is really stable.

    ReplyDelete
  13. That's a really good idea. I made these thermostats a couple of years back and they have served me well day after day with no trouble whatsoever. At some point I'm going to revisit them and there's literally a ton of things I have in mind for them. I'm actually thinking about completely removing them from the wall except for the temperature sensor, and using a web interface and a cheap table to control things

    As time goes by the various tablets are getting cheaper and it will soon be perfectly reasonable to have a tablet that controls the entire house, and that makes your idea of a slow moving temperature sensor even more compelling.

    ReplyDelete
  14. To make power more manageable, though it takes up a little space, I bought a AC/DC adapter from Amazon. It was $9.99. Specs:
    Input: AC 16V-28V
    Output: DC 12V 1.5A
    Model: 24T1812

    12V to power Arduino, then drop down to 5V to power relay module.and XBee (via 3.3v regulated breakout board)

    Currently buillding one as well.
    TFT color touchscreen
    Arduino mega2560
    DHT22 (for accuracy of it's temp sensor as well as having a humidity reading)
    XBee S2 so it will interface with my Zigbee door locks as well my LAN for Android control
    A Real Time Clock

    ReplyDelete
    Replies
    1. Integrating with the door locks is going to give you trouble. Which ones do you have?

      Delete
  15. Great write up and thanks for including your sketch!

    ReplyDelete
  16. Thanks for the great code and comments, Dave! And all your descriptions of snafus, bugs, hassles etc. I am going to start with a heat-only, no-web, 1-zone, Celsius setup for simplicity, but it's neat to realize there's room for more complexity!

    ReplyDelete
  17. like a job for a couple of Web enabled thermostats doesn't it? ... ethermostatwifi.blogspot.com

    ReplyDelete
    Replies
    1. Actually, No.

      I thought long and hard about a web enabled thermostat and decided specifically not to go that way. When you put things on the web, they are subject to getting hacked by whoever out there wants to play with them. I'd rather not have my house the victim of some bored loser that is hacking out of his mom's basement.

      I only want one device visible to the web and that one heavily protected and fully under my control, not some corporate manager that decides to get a little lax on web security to save costs and increase his bonus this year.

      Delete