Multizone Thermostat Project

This past summer my wife and moved into a new house with a split HVAC system. Hot water rads for heating and a traditional A/C system for cooling. A few days after we moved in we replaced the existing thermostat with a Nest. It's a great thermostat but at $250 it's quite expensive.

This winter we have had trouble getting the heat to be even through out the house. To cold in one too, turn the heat up, then too hot in another. The solution is to split the hot water rads into multiple zones and place a thermostat in each of these zones. However I can justify spending $250 per zone on a Nest.

A little research led me to the Arduino. I actually have an education in software engineering but never took a job in the field. I've been out of practice for about 10 years now and never programmed much in C, just the usual 'Hello Word', but figured I could give it a shot.

Project Goals

  1. Have a separate thermostat in each zone instead of just a temperature sensor. A thermostat in each zone will allow much better control allowing each zone to run on its on schedule.

  2. Touch screen control. Having just a small touch screen will allow the easiest 'integration' in the rooms where the thermostats will be placed.

  3. Calculate the time it will take to reach the current set point. This is necessary for the schedule to be followed correctly and have the room up to heat by the requested time. If the schedule calls for the temperature to be 22c at 8 am then the system will need to be turned on before 8 am.

  4. Have a 'head' unit that can monitor the status of each zone controller and control the zone valves that will actually control the HAVC system.

At this point I have purchased an Arduino Uno, Adafruit TouchSheild v2, Dallas 18B20 temp sensor and the other necessary parts to get the first part, a zone controller, of the project up and running.

The basis of the code is in place for the zone controller. At this point it does almost everything I would expect of it however I few functions still need to be added. Since the program is running on a Uno I am quickly approaching the limits of its memory but with some code clean up it can be done.

Still to do

  1. Add XBee into the system. The plan is send data back and forth with the 'head' unit. Time information and outdoor temp will be sent from the 'head' unit to each zone controller and the zone controller will send system status/requests to the 'head' unit.

  2. Add minute-by-minute control. This will allow the Project Goal #3 to work properly. Currently the program only runs on an hour-by-hour basis.

  3. Add cooling functionality. Since I have a split HVAC system it will be a challenge but it will be done one day.

Here is the hardware as it stands now; a little clean up needs to be done to the screen. Ignore the red on the screen as it's just being used to aid in the layout of the screen.

My biggest issue right now is trying to get the touch screen working properly. I am having some trouble getting it to accept a 'single' touch. That is, every time I touch the screen at least 2 touches are registered and it drives the set point up/down very quickly.

I will post the code below over a few posts to beat the 9500 char limit.

Any questions, comments, concerns, or help….I am all ears.

Steve

Slow down the touch screen sampling - once a touch is recognized, wait at least 100mS say before another touch will be recognized as valid.

Definitions/Constants/setup()/loop()

/*  ----------------------------------------------------------------------------------------
 *  Arduino In Room Thermostat for Multi-zone Hydronic Boiler System
 *  Copyright (c) Steve Croswell <scroswell@gmail.com>
 *  ----------------------------------------------------------------------------------------
 */

#include <DallasTemperature.h>  
#include <OneWire.h>  
#include <Time.h> 
//#include <SoftwareSerial.h>
//#include <XBee.h>
#include <EEPROM.h> 
#include "EEPROMAnything.h"
#include <Adafruit_GFX.h>
#include <Adafruit_STMPE610.h>
#include <Wire.h>
#include <SPI.h>
#include <Adafruit_ILI9341.h>


/*  -------------------------------
 * constants
 *  -------------------------------
 */

#define MODE_OFF          0    // system off, no heating, no cooling
#define MODE_COOL         1    // thermostat in cool mode
#define MODE_HEAT         2    // thermostat in heat mode

#define MIN_TEMP         14     //MAKE THIS SETTABLE IN A MENU
#define MAX_TEMP         30     //MAKE THIS SETTABLE IN A MENU
#define TEMP_ADJUST      -0.00  //temp sensor picks up some heat from the enclosure.  ADJUST

#define TOTAL_SCHEDULES  7      //  7 Days of the week.
#define SCHEDULES_DAY    4      //  4 schedules per day.

//timers
#define MS_1MIN          60000  //1 minute
#define MS_15SEC         15000  //15 seconds

//touchscreen pressure
#define MINPRESSURE      15
#define MAXPRESSURE      60

//touchscreen coordinates
#define TS_MINX          150
#define TS_MINY          130
#define TS_MAXX          3800
#define TS_MAXY          4000

//tft pin assignments
#define STMPE_CS         8
#define TFT_CS           10
#define TFT_DC           9

//define hex colours
#define WHITE            0xFFFF
#define BLACK            0x0000
#define BLUE             0x001F
#define RED              0xF800
#define GREEN            0x07E0

//pin assignments
//int     pinRelay                  = 3;   // Relay Output
int     pinTempSensor               = 7;   // Temp Sensor
//int     pinXbeeRX                 = 2;   //use SoftwareSerial for XBee I/O
//int     pinXbeeTX                 = 3;   //use SoftwareSerial for XBee I/O

//operating variables
//boolean   bDebug                    = false;  //used for debugging, serial prints in certain places
boolean   bReset                    = false;  //set true when user or head unit resets settings
boolean   bOverride                 = false;  //used when schedule is overridden
boolean   bHeatOn                   = false;  //heating on or off
//boolean   bCoolOn                   = false;  //cooling on or off
boolean   bLagCalc                  = false;  //does lag time need to be checked on this cycle

float   fCurrentTemp                = 0.0; //0.0;  //current temp, read from DS18B20
float   fSetPoint                   = 14.5; //0.0;  //set temp from program or override
float   fOutdoorTemp                = 0.0; //outdoor temperature
float   fDriftUpper                 = 0.35; //allow temperature to rise .35 degrees above the set temperature.
float   fDriftLower                 = 0.40; //allow temperature to fall .40 defress below the set temperature.
float   fStartTemp                  = 0.0; //temp at start of cycle
float   fEndTemp                    = 0.0; //temp at end of cycle

//unsigned long   lHeatCycleTime    = 0;  //number of seconds heating cycle lasted
//unsigned long   lCoolCycleTime    = 0;  //number of seconds cooling cycle lasted
unsigned long   lCurrentMSSchTemp   = 0;  //timer for checking temp and schedule settings
unsigned long   lPreviousMSSchTemp  = 0;  //timer for checking temp and schedule settings
unsigned long   lCycleStartMS       = 0;  //timer for calculating cycle time
unsigned long   lCycleEndMS         = 0;  //timer for calculating cycle time
unsigned long   lTimeToSetPoint     = 0;  //number of minutes until set point is reached

byte   byMode                       = MODE_HEAT;  //system mode MODE_OFF = 0, MODE_COOL = 1, MODE_HEAT = 2 USE WITH OUTDOOR TEMP
byte   byOverrideHour               = 0;         //hour that program was over ridden

//stored setting structure
struct config {
  byte   byScheduleHourArray[TOTAL_SCHEDULES][SCHEDULES_DAY]; //array for schedule hours over 7 days, changes 4 times per day - 24 hour format                                                       
  byte   byScheduleMinArray[TOTAL_SCHEDULES][SCHEDULES_DAY]; //array for schedule minutes over 7 days, changes 4 times per day                                  
  float  fHeatSetPointArray[TOTAL_SCHEDULES][SCHEDULES_DAY]; //array for heating temps over 7 days, changes 4 times per day - temp in C                                                           
  //float  fCoolSetPointArray[TOTAL_SCHEDULES][SCHEDULES_DAY]; //array for cooling temps over 7 days, changes 4 times per day - temp in C
  unsigned long   lHeatCycleLag; //number of ms before system started heating room
  //unsigned long   lCoolCycleLag; //number of ms before system started cooling room
  unsigned long   lTimeToHeat;   //number of ms to raise temperature 1 degree
  //unsigned long   lTimeToCool;   //number of ms to lower temperature 1 degree
 } configuration;
 
//temperature structure
struct temp {
  int  iWhole;                                                
  int  iDecimal;
} temperature;

//library setup
OneWire ds(pinTempSensor); 
DallasTemperature dsTemp(&ds);
Adafruit_STMPE610 ts = Adafruit_STMPE610(STMPE_CS);
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC);
//SoftwareSerial xbeeSS(pinXbeeRX, pinXbeeTX);  //setup software serial for XBee
//XBee xbee = XBee();

//program setup
void setup() { 
  //setup serial communication
  Serial.begin(9600);
  //xbeeSS.begin(9600);
  //xbee.setSerial(xbeeSS);
  
  //setup pins
  //pinMode(pinRelay, OUTPUT);
  //digitalWrite(pinRelay, HIGH);  //pin LOW engages (closes) relay (normally Open). HIGH = OFF
  
  //pinMode(pinXbeeRX, INPUT);
  //pinMode(pinXbeeTX, OUTPUT);
  
  //time 
  setSyncProvider(fnGetTime);  //set time
  setSyncInterval(3600); //update time hourly

  //load config from EEPROM
  EEPROM_readAnything(0, configuration);  //load config from EEPROM

  //start libraries
  dsTemp.begin();
  ts.begin();
  tft.begin();
  
  //function calls
  tft.setRotation(1); //set screen rotation - usb upper, left
  fnDrawMainScreen(); //DRAW ALL
}

//program loop
void loop() {

  TS_Point p = ts.getPoint();
  p.x = map(p.x, TS_MINX, TS_MAXX, 0, tft.width());
  p.y = map(p.y, TS_MINY, TS_MAXY, 0, tft.height());
  if (ts.touched()) {
  if (!ts.bufferEmpty()) {
    if (p.z < MINPRESSURE) { // && p.z < MAXPRESSURE) {
      if (p.x > 0 && p.x < 320 && p.y > 0 && p.y < 240 ) { //up
        fSetPoint = fSetPoint + 0.5;
        //ALLOW A MIN AND MAX TEMP
        byOverrideHour = hour();
        bOverride = true;
        fnDrawSetPoint();
        delay(10);
      }
  
    }
  }
  }
  
  if (bReset) {  //call reset function if flag is set
    fnReset();
  }

  lCurrentMSSchTemp = millis(); //current time in ms
 
  if((lCurrentMSSchTemp - lPreviousMSSchTemp) > MS_1MIN) {  //1 minute timer - check temp, check schedule
    lPreviousMSSchTemp = lCurrentMSSchTemp; //if time has passed, update previous time  
    fCurrentTemp = fnGetTemp(); //call function to get current temp
    //DRAW UPDATED TEMPS
    fnCheckScheduledTemp(); //call function to check schedule
    //fnDrawMainScreen(); //REMOVE
    /*if (bDebug) {
      Serial.print("Heat lag: ");
      Serial.println(configuration.lHeatCycleLag); //number of ms before system started heating room
      Serial.print("Time to heat: ");
      Serial.println(configuration.lTimeToHeat);
      Serial.print("Set temp = ");
      Serial.print(fSetPoint);
      Serial.print(" Current temp = ");
      Serial.println(fCurrentTemp);
    }*/
  }
  
  float fLowTempDiff  = fSetPoint - fDriftLower - fCurrentTemp; //CHECK THESE
  float fHighTempDiff = fSetPoint + fDriftUpper - fCurrentTemp; //CHECK THESE

  //turn heat on or off
  if (fLowTempDiff >= 0 && bHeatOn == false) { //turn heat on
    bHeatOn = true;  //////////turn heat flag on CHECK THIS
    lCycleStartMS = millis(); //get current millis() for cycle start time
    fStartTemp = fCurrentTemp; //get start temp
    bLagCalc = true;
    fnTimeToSetPoint(fStartTemp, fSetPoint); //call function that calculates time to reach setpoint
  }
  else if (fHighTempDiff <= 0 && bHeatOn == true) {  //turn heat off
    bHeatOn = false; //////turn heat flag off CHECK THIS
    lCycleEndMS = millis(); //get current millis() for cycle end time
    fEndTemp = fCurrentTemp; //get end temp
    fnCalcMSPerDegree(lCycleStartMS, lCycleEndMS, fStartTemp, fEndTemp); //call function that calculate/update time to set point 
  }
 
  //calculate lag time if necessary
  if (byMode == MODE_HEAT && bHeatOn == true && bLagCalc == true) { //check time the it took system to start heating room - lag time
    if (fCurrentTemp - fStartTemp >= 0.10) {
      fnCalculateLagTime(lCycleStartMS);
      bLagCalc = false; 
    }
  }
}

Functions

//functions

//returns current temperature as a float
float fnGetTemp() {
  float fNewTemp = 0.0;
  
  dsTemp.requestTemperatures();
  delay(1000); //delay for 1 sec, time it takes for sensor to respond
  fNewTemp = dsTemp.getTempCByIndex(0) + TEMP_ADJUST; //ADD CALIBRATION
  
  return fNewTemp;
}

//blanks EEPROM and restores 'factory' settings
void fnReset() { 
  for (int i = 0; i < 1024; i++) {
    EEPROM.write(i, 0);
  }
  
  configuration.lTimeToHeat = 0;
  configuration.lHeatCycleLag = 0;
  //configuration.lTimeToCool = 0;
  //configuration.lCoolCycleLag = 0;
 
  //schedule change times (hours) in 24hr format
  for (int i = 0; i < TOTAL_SCHEDULES; i++) {
    for (int j = 0; j < SCHEDULES_DAY; j++) {
      if (j == 0) {
        configuration.byScheduleHourArray[i][j] = 6;
      }
      else if (j == 1) {
        configuration.byScheduleHourArray[i][j] = 8;
      }
      else if (j == 2) {
        configuration.byScheduleHourArray[i][j] = 16;
      }
      else if (j == 3) {
        configuration.byScheduleHourArray[i][j] = 22;
      }
    }
  }
  
  //schedule change times (minutes)
  for (int i = 0; i < TOTAL_SCHEDULES; i++) {
    for (int j = 0; j < SCHEDULES_DAY; j++) { 
      configuration.byScheduleMinArray[i][j] = 0;
    }
  }
  
  //temperature at schedule change
  for (int i = 0; i < TOTAL_SCHEDULES; i++) {
    for (int j = 0; j < SCHEDULES_DAY; j++) {
      if (j == 0) {
        configuration.fHeatSetPointArray[i][j] = 22.0;
      }
      else if (j == 1) {
        configuration.fHeatSetPointArray[i][j] = 18.0;
      }
      else if (j == 2) {
        configuration.fHeatSetPointArray[i][j] = 22.0;
      }
      else if (j == 3) {
        configuration.fHeatSetPointArray[i][j] = 19.0;
      }
    }
  }
  /* //cooling array - not used yet
  for (int i = 0; i < TOTAL_SCHEDULES; i++) {
    for (int j = 0; j < SCHEDULES_DAY; j++) {
      if (j == 0) {
        configuration.fCoolSetPointArray[i][j] = 24.0;
      }
      else if (j == 1) {
        configuration.fCoolSetPointArray[i][j] = 27.0;
      }
      else if (j == 2) {
        configuration.fCoolSetPointArray[i][j] = 23.0;
      }
      else if (j == 3) {
        configuration.fCoolSetPointArray[i][j] = 22.0;
      }
    }
  }
  */                                          
  EEPROM_writeAnything(0, configuration);

  bReset = false;
}

//gets current time wirelessly via xbee
time_t fnGetTime () {
  time_t tCurrentTime = 1411019940; //Thurs Sept 18 2014 @ 5:59 am
  return tCurrentTime;
  //XBEE
  //xbee.readPacket();
  //if (xbee.getResponse().isAvailable()) {
  //}
}

//check if new schedule is avalibale and updates setpoint //SUBTRACT PRE-HEAT TIME
void fnCheckScheduledTemp() {
  byte byScheduledStart;
  byte byScheduledEnd;
  byte byCurrentDay = weekday() - 1; //weekday() returns 1-7, minus 1 to return 0-6 for array references
  byte byNextDay;
  byte byCurrentHour = hour();
  byte byCurrentMinute = minute();
  float fScheduledTemp;

  if (byCurrentDay == 6) { //if current day is saturday (6), then next day is sunday (0)
    byNextDay = 0;
  }
  else {
    byNextDay = byCurrentDay + 1;
  }

  for (int i = 0; i < SCHEDULES_DAY; i++) {  //loop through each set of start/end time for the set temps
    byScheduledStart = configuration.byScheduleHourArray[byCurrentDay][i];  //get scheduled event start time from array
    
    if (i == (SCHEDULES_DAY  - 1 )) { //if at end of time slots for day
      byScheduledEnd = configuration.byScheduleHourArray[byNextDay][0]; //get first time from next day
    }
    else {
      byScheduledEnd = configuration.byScheduleHourArray[byCurrentDay][i+1]; //get current days next time slot
    }
    
    /*if (bDebug == true) { //for debugging
      Serial.print("Current Day: ");
      Serial.print(byCurrentDay);
      Serial.print(" Next Day: ");
      Serial.print(byNextDay);
      Serial.print(" Current Hour: ");
      Serial.print(byCurrentHour);
      Serial.print(" Sch Start: ");
      Serial.print(byScheduledStart);
      Serial.print(" Sch End: ");
      Serial.print(byScheduledEnd);
      Serial.print(" Sch Temp: ");
      Serial.println(fScheduledTemp);
    }*/

    if (byCurrentHour >= byScheduledStart && byCurrentHour < byScheduledEnd) { //current hour falls between scheduled times, otherwise temp is already set
      if (bOverride && byOverrideHour >= byScheduledStart && byOverrideHour < byScheduledEnd) { //system on override, override hour in current schedule
        return;
      }
      else {
        fScheduledTemp = configuration.fHeatSetPointArray[byCurrentDay][i]; //get scheduled temperature from array
        fSetPoint = fScheduledTemp; //set temperature
        bOverride = false;
      }
    }
  }
}

//calculates how many ms to take to raise/lower the temp by 1 degree
//check stored value for the time, if it differs by more then 5% it updates the valve in the EEPROM
void fnCalcMSPerDegree(long lCycleStart, long lCycleEnd, float fStart, float fEnd) {
  long lCycleTime;
  long lMSPerDegree;
  float fTempChange;
  boolean bUpdate = false;
  
  lCycleTime = lCycleEnd - lCycleStart; //calculate cycle time in ms
  fTempChange = fabs(fStart - fEnd); //calc temp change
  lMSPerDegree = lCycleTime / fTempChange; //ms per degree

  if (byMode == MODE_HEAT) { //if in heating mode
    if (lMSPerDegree <= configuration.lTimeToHeat) { //this cycle ms/degree is higher than stored, puts lower number first for difference calculation
      if ((configuration.lTimeToHeat/lMSPerDegree) * 100 > 105) { //calculate if difference is more then 5%
        bUpdate = true;
      }
    }
    else if (configuration.lTimeToHeat <= lMSPerDegree) { //this cycle ms/degree is lower than stored, puts lower number first for difference calculation
      if ((lMSPerDegree/configuration.lTimeToHeat) * 100 > 105) { //calculate if difference is more then 5%
        bUpdate = true;
      }
    }
    if (bUpdate == true) { //return lmsperdegree
      configuration.lTimeToHeat = lMSPerDegree; //update variable
      fnWriteEEPROM(); //call function to update EEPROM
    }
  }
}

//calculates the time it takes for the system to start producing heat or cooling.  will be used to adjust schedule start time, so temps will be reached at scheduled time
//check stored value for the time, if it differs by more then 5% it updates the valve in the EEPROM
long fnCalculateLagTime(long lCycleStart) {
  long lLagThisCycle;
  boolean bUpdate = false;

  lLagThisCycle = millis() - lCycleStart;

  if (byMode == MODE_HEAT) { //if in heating mode
    if (lLagThisCycle <= configuration.lHeatCycleLag) { //this cycle lag is higher than stored, puts lower number first for difference calculation
      if ((configuration.lHeatCycleLag/lLagThisCycle) * 100 > 105) { //calculate if difference is more then 5%
        bUpdate = true;
      }
    }
    else if (configuration.lHeatCycleLag <= lLagThisCycle) { //this stored lag is higher than stored, puts lower number first for difference calculation
      if ((lLagThisCycle/configuration.lHeatCycleLag) * 100 > 105) { //calculate if difference is more then 5%
        bUpdate = true;
      }
    }
    if (bUpdate == true) {
      configuration.lHeatCycleLag = lLagThisCycle; //update variable
      fnWriteEEPROM(); //call function to update EEPROM
      /*if (bDebug) {
        Serial.print("Lag time = ");
        Serial.println(configuration.lHeatCycleLag);
      }*/
    }
  }
}

//calculates time it will take to reach desired set point
void fnTimeToSetPoint(float fCurr, float fSet) { //current temp, set point
  float fTempDiff = 0.0; //temperature difference
  
  if (byMode == MODE_HEAT) { //if in heat mode
    fTempDiff = fSet - fCurr; //calculate temp difference
    lTimeToSetPoint = fTempDiff * configuration.lTimeToHeat / 60000; //(temp diff * ms per degree) / ms per minute - gives minutes to set point
    //lTimeToSetPoint = (fTempDiff * 180000) / 60000; 
  }
  fnDrawTimeToSetPoint(); //draw time to set point
}

//write config to EEPROM
void fnWriteEEPROM() {
  EEPROM_writeAnything(0, configuration);
}

//get updated outdoor temp from head unit
void fnGetOutdoorTemp() {
  //XBEE
}

//split temperature reading into 2 parts for display
void fnSplitTemp (float fTemp) {
  temperature.iWhole = (int)(fTemp);  //get whole number
  temperature.iDecimal = 10 * (fTemp - temperature.iWhole); //isolate decmial portion
}

TFT Draw Functions

//display current set point
void fnDrawSetPoint() { //print as 12 then .5 smaller below with oC above
  fnSplitTemp(fSetPoint); 
  
  tft.fillRect(2, 110, 103, 49, RED); //draw rectangle to clear area
  tft.setCursor(2, 110);
  tft.setTextColor(WHITE);
  tft.setTextSize(7);
  tft.print(temperature.iWhole); //print whole number
  
  tft.setCursor(75, 138); //print . (decimal)
  tft.setTextSize(3);
  tft.print(".");
  
  tft.setCursor(90, 138); //print decimal portion
  tft.setTextSize(3);
  tft.print(temperature.iDecimal);
  
  tft.drawCircle(84, 113, 3, WHITE);  //print o (degrees)
  
  tft.setCursor(90, 110); //print C
  tft.setTextSize(3);
  tft.print("C");
  
}

//draw current room temp
void fnDrawCurrentTemp() {
  fnSplitTemp(fCurrentTemp);
  
  tft.fillRect(109, 110, 103, 49, RED); 
  tft.setCursor(109, 110); 
  tft.setTextColor(WHITE);
  tft.setTextSize(7);
  tft.print(temperature.iWhole);
  
  tft.setCursor(182, 138);
  tft.setTextSize(3);
  tft.print(".");
  
  tft.setCursor(197, 138);
  tft.setTextSize(3);
  tft.print(temperature.iDecimal); 
  
  tft.drawCircle(191, 113, 3, WHITE); 
  
  tft.setCursor(197, 110);
  tft.setTextSize(3);
  tft.print("C");
}

//draw outdoor temp
void fnDrawOutdoorTemp() { //NEGATIVE TEMPS
  fnSplitTemp(fOutdoorTemp);
  
  tft.fillRect(216, 110, 103, 49, RED); //x, y, w, h
  tft.setCursor(216, 110); //print temp without decimal
  tft.setTextColor(WHITE);
  tft.setTextSize(7);
  tft.print(temperature.iWhole);
  
  tft.setCursor(289, 138); //print . (decimal)
  tft.setTextSize(3);
  tft.print(".");
  
  tft.setCursor(304, 138); //print decimal portion
  tft.setTextSize(3);
  tft.print(temperature.iDecimal); //extract decimal portion
  
  tft.drawCircle(298, 113, 3, WHITE);  //print o (degrees)
  
  tft.setCursor(304, 110); //print C
  tft.setTextSize(3);
  tft.print("C");
}

//draws date on screen in 'Sunday,September 21,2014' format
void fnDrawDate() {
  String sMonth;
  String sWeekday;
  String sDate;
  int iXpos;

  if (weekday() == 1) {
    sWeekday = "Sunday";
  }
  else if (weekday() == 2) {
    sWeekday = "Monday";
  }
  else if (weekday() == 3) {
    sWeekday = "Tuesday";
  }
  else if (weekday() == 4) {
    sWeekday = "Wednesday";
  }
  else if (weekday() == 5) {
    sWeekday = "Thursday";
  }
  else if (weekday() == 6) {
    sWeekday = "Friday";
  }
  else if (weekday() == 7) {
    sWeekday = "Saturday";
  }

  if (month() == 1) {
    sMonth = "January";
  }
  else if (month() == 2) {
    sMonth = "Febuary";
  }
  else if (month() == 3) {
    sMonth = "March";
  }
  else if (month() == 4) {
    sMonth = "April";
  }
  else if (month() == 5) {
    sMonth = "May";
  }
  else if (month() == 6) {
    sMonth = "June";
  }
  else if (month() == 7) {
    sMonth = "July";
  }
  else if (month() == 8) {
    sMonth = "August";
  }
  else if (month() == 9) {
    sMonth = "September";
  }
  else if (month() == 10) {
    sMonth = "October";
  }
  else if (month() == 11) {
    sMonth = "November";
  }
  else if (month() == 12) {
    sMonth = "December";
  }
  
  sDate = sWeekday + "," + sMonth + " " + String(day()) + "," + String(year());  //concatinate date
  iXpos = (320 - (sDate.length() * 12)) / 2; //10 pixels per letter at size 2 plus 2 pixel space, calculate center position
  
  tft.fillRect(0,0,320,20,BLACK); 
  tft.setCursor(iXpos, 2);
  tft.setTextColor(WHITE);
  tft.setTextSize(2);
  tft.print(sDate);
}

void fnDrawTime() {
  String sTime;
  String sHour;
  String sMinute;
  int iXpos;
  
  sHour = String(hourFormat12());
  
  if (minute() < 10) { //print leading "0" for time under 10 minutes
    sMinute = "0" + String(minute());
  }
  else {
    sMinute = String(minute());
  }
  
  if (isAM()) {  //check if AM or PM
    sTime = sHour + ":" + sMinute + " AM";
  }
  else {
    sTime = sHour + ":" + sMinute + " PM";
  }
  
  iXpos = (320 - (sTime.length() * 19)) / 2; //15 pixels per letter at size 3 plus 4 pixel space, calculate center position
  
  tft.fillRect(0,21,320,30,BLACK); 
  tft.setCursor(iXpos, 21);
  tft.setTextSize(3);
  tft.setTextColor(WHITE);
  tft.print(sTime);
}

void fnDrawMainScreen() {
  tft.fillScreen(BLACK);
  
  tft.setCursor(16, 90);
  tft.setTextSize(2);
  tft.setTextColor(WHITE);
  tft.print("SET TO");
  
  tft.setCursor(123, 90);
  tft.setTextSize(2);
  tft.setTextColor(WHITE);
  tft.print("INDOOR");
  
  tft.setCursor(226, 90);
  tft.setTextSize(2);
  tft.setTextColor(WHITE);
  tft.print("OUTDOOR");
  
  fnDrawTime();
  fnDrawDate();
  fnDrawSetPoint();
  fnDrawUpDown();
  fnDrawCurrentTemp();
  fnDrawOutdoorTemp();
  fnDrawMode();
  fnDrawScheduleStatus();
  fnDrawTimeToSetPoint();
}

void fnDrawMode() {
  tft.fillRect(100, 209, 200, 21, RED);
  tft.setCursor(100, 209);
  tft.setTextSize(3);
  tft.setTextColor(WHITE);
  tft.print("HEATING");
}

void fnDrawScheduleStatus() {
  tft.fillRect(100, 186, 200, 21, RED);
  tft.setCursor(100, 186);
  tft.setTextSize(3);
  tft.setTextColor(WHITE);
  tft.print("ON SCHEDULE");
}

void fnDrawUpDown() {
  tft.fillTriangle(4, 82, 55, 34, 106, 82, RED); //up arrow
  tft.setCursor(44, 45);
  tft.setTextSize(5);
  tft.setTextColor(WHITE);
  tft.print("+");
  tft.fillTriangle(4, 166, 55, 214, 106, 166, RED); //down arrow
  tft.setCursor(44, 170);
  tft.setTextSize(5);
  tft.setTextColor(WHITE);
  tft.print("-");
}

void fnDrawTimeToSetPoint() {
  long lMinutes = 0;  //minutes to set point
  long lHours = 0;  //hours to set point
  
  lMinutes = lTimeToSetPoint % 60;  //minutes to set point - modulo total time by 60
  lHours = (lTimeToSetPoint - lMinutes) / 60;  //hours to set point - total time minus minutes
    
  tft.fillRect(120, 50, 180, 14, RED);
  
  if (! MODE_OFF) { //if the system is active, is this needed? - HOW TO PRINT AS BLANK
    tft.setCursor(120, 50);
    tft.setTextSize(2);
    tft.setTextColor(WHITE);
    tft.print("TIME TO TEMP");
    tft.fillRect(120, 65, 180, 14, RED);
    tft.setCursor(120, 65);
    tft.setTextSize(2);
    tft.setTextColor(WHITE);
  
    if (lHours == 0) {
      tft.print(lMinutes);
      tft.print(" MINUTES");
    }
    else {
      tft.print(lHours);
      tft.print(" HR ");
      tft.print(lMinutes);
      tft.print(" MINS");
    }
  }  
}

That looks like a very nice piece of work.

I too am building a home automation system having moved into a new house. You can see it at http://219.88.69.69/2WG/

I use ethernet for input and output and to use its world wide connectivity.

I am planning to pull excess heat out of the roof space in winter to reduce heater use. Hence I have thermometers in the roof space to measure the resource and will need to implement a fan controlled by the Arduino.

My focus is just on lounge heating - we do not spend enough time anywhere else to want the whole house at the same temperature and since we can open or close internal doors as required we can keep other areas at a reasonable temperature if required and can make occasional use of electric heaters when needed.

Good luck with your project.

Cheers

Catweazle NZ

I'm working on something similar as a part of a giant home automation setup, I'm in the planning phase right now though.

Looks like the reply might be what you need so I had an aside to ask.

What touch screen is that?

gordonfreeman:
I'm working on something similar as a part of a giant home automation setup, I'm in the planning phase right now though.

Looks like the reply might be what you need so I had an aside to ask.

What touch screen is that?

Fixed the touch screen problem.

if (ts.touched()) {
    if (!ts.bufferEmpty()) {
      TS_Point p = ts.getPoint();  //get point
    
      p.x = map(p.x, TS_MINX, TS_MAXX, 0, tft.width());  //map point to relate to screen co-ords
      p.y = map(p.y, TS_MINY, TS_MAXY, 0, tft.height());
    
      if (p.x > 180 && p.x < 280 && p.y > 0 && p.y < 70 ) { //up
        fSetPoint = fSetPoint + 0.5; //increase set point by 0.5 degrees
        if (fSetPoint > MAX_TEMP) { //if at max set point, dont go beyond
          fSetPoint = MAX_TEMP;
        }
        byOverrideHour = hour(); //current hour for schedule override
        bOverride = true; //set override flag to true
        fnDrawSetPoint(); //redraw current set point
      }
      
      if (p.x > 30 && p.x < 110 && p.y > 0 && p.y < 70 ) { //down
        fSetPoint = fSetPoint - 0.5;
        if (fSetPoint < MIN_TEMP) {
          fSetPoint = MIN_TEMP;
        }
        byOverrideHour = hour();
        bOverride = true;
        fnDrawSetPoint();
      }
      delay(100);

      while (!ts.bufferEmpty()) {
        TS_Point p = ts.getPoint();
      }
    }
  }

Silly question: do you have a separate Arduino board for every thermostat ie. separate Arduino in each room you want to control?

I'm planning a similar project, but only to turn the electronic floor heating on or off depending on the thermostat setpoint in the current room. The thermostat exist already, but they are mostly broken and very old. I am planning on using the existing places of the thermostat boxes on the walls, so I need to install the on/off -relay and the temperature sensor in their place.

I am planning on having a separate Arduino in each room and a usb-connection (this needs more thinking, wireless connection would be optimal) to a raspberry pi hosting a web server that shows a layout of the house and the temperature setpoints for each room. It would also have a weekly timetable for different temperatures, so it would be possible to lower the temperatures for night time, and an "away" -function that lowers the temperatures throughout the house for a set period of time.

A simple selection dial ranging from -3 to +3 will also control the room temperature, so once a setpoint has been set from the central system on raspberry pi, say +20 degrees for a room, the setpoint can be diverted by occupants in the room by 3 degrees to either direction.