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).



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();
}

0 comments:

Post a Comment