Remote Arduino Thermostat

First Post! Well anyway, right to it..
Here are some quick links to the essential parts:

  1. thermostat control: how the Arduino will control the thermostat
  2. circuit design: layout of all the components
  3. server-side control: how a php webpage will talk to the Arduino
  4. demo: play with the control page and view the Arduino code

goal

  • Monitor room and outdoor temperatures from a web interface.
  • Control my thermostat, maintaining original button functionality.

motivation

  • My Honeywell FocusPro th5110d is boring.
  • It’s been really hot and it would be nice to turn on the AC before I get home.
  • The prospect of control via internet is too fun a project to pass up.
  • Good reason to learn php.
  • Did I say it was hot?

approach

I’ve found a few projects online with people replacing their thermostat with an Arduino to control their own relays. My case is different in that I wanted to leave the original thermostat where it is. At first, I thought, “Yeah I’ll use a couple servos to push the buttons!” But then I actually thought about it for more than two seconds.



The thermostat buttons are backed with some kinda conductive rubber; When the button is pushed, it closes the circuit-pad-switch-thingy for that button. Counting all the button traces, there are a total of two high and two low. I initially thought the two low traces were just a common ground and I could get away with programming the Arduino to send my own signals on the high traces. Doesn’t work like that. It’s possible that the thermostat microcontroller drives the two high traces at different frequencies (undetected by my DMM) and the low traces are inputs, waiting to read those known frequencies.



So I decided to switch the four traces using transistors. I needed to attach my own wires to the pcb to run out of the thermostat case to the Arduino/transistor rig. Using the DMM continuity, I found the traces connect to four vias, visible on the other side of the board. They run through a 1k resistor and then to the microcontroller. I just stuck my copper wires in the vias. The video below shows how connecting the wires changes the button states. A truth table and simple schematic sketch for the switches were shown earlier.


Some preliminary code:

#define v1Pin 2
#define v2Pin 3
#define g1Pin 4
#define g2Pin 5
// toggle combination of thermostat's wire leads
void switches(int a, int b)
{
  Serial.print("setting.. ");
  digitalWrite(a,HIGH);
  digitalWrite(b,HIGH);
  delay(300);
  digitalWrite(a, LOW);
  digitalWrite(b, LOW);
  // wait to make sure the switch was made
  delay(2000);
  Serial.println("set");
}


So, for example, calling switches(v1Pin,g2Pin) would change the system state.

design

Parts used:

  • Arduino Uno
  • Arduino Ethernet Shield
  • 4x 2n3904 transistors
  • 4x 1k resistors
  • 4.7k resistor
  • tmp36 temperature sensor
  • dth22 temperature/humidity sensor

In the drawings below, the 102 component corresponds to the 1k surface-mount resistor on the thermostat pcb (the four vias).


 

click to enlarge

connecting..

The biggest stump of the project was connecting the ethernet shield to my webserver. My advice for other novices:

  • Make sure the server IP is correct (even the Arduino example webclient sketch’s google server IP is outdated). And if your webhost doesn’t provide static IP’s, make sure to also declare the correct hostname (I forgot to include ‘www.’)
  • If your ethernet shield connects to an IP two or three times then fails forevermore, make sure to loop the ‘connect’ bit of code.
    It’s a source port number issue (I have the nice people at my webhost to thank for helping). Some webhosts treat repeated requests from the same source port number as a DDoS attack. The Arduino ethernet library overcomes this problem by starting at 1024 and incrementing every time the ‘connect’ function is called. But, in my case, instead of looping the code, I just physically reset the board after each connection. So
    every time it would be 1024.
    Incidentally, it was only last year when it actually was the Arduino
    library’s fault. I spent a lot of time tweaking the library code until
    i realized my stupidity.

Another prevailing issue with the ethernet shield is that it often needs to be reset before it can start connecting properly. I think this problem was mitigated for the newest shield version. But just to be sure, I modded my ethernet shield so that I could reset it without resetting the Arduino.



The shield’s reset is connected to the Arduino by the reset pin (near the 3.3V) and the ICSP header. So I bent the 1st and pulled out the 2nd.

void ethernetReset() {
  pinMode(resetPin, OUTPUT);
  digitalWrite(resetPin, LOW);
  delay(200);
  digitalWrite(resetPin, HIGH);
  delay(200);
  pinMode(resetPin, INPUT);
  delay(200);
  Ethernet.begin(mac, ip);
}

php

The longest part of the project was on the server side. This was my first time using php. So the page code might not be pretty.

The Arduino sends temperature readings to the php webpage using a form request like so:

void connectAndWrite()
{
  Serial.println("connecting to send...");

  if (client.connect()) {
    Serial.println("connected. Sending..");
    client.println("GET /thermostat/control.php?temp="+ String(temp) +\
                   "&outtemp="+ String(outTemp) +\
                   "&hum="+ String(humidity) +" HTTP/1.0");
    client.println("Host: www.keelanchufor.com");
    client.println();
    delay(1000);  // dunno why, but necessary for successful request
    client.stop();
    client.flush();
    Serial.println("disconnected");
  } else {
    Serial.println("connection failed");
    failed++;
  }
}

And the webpage listens for the request then writes the value to a text file, also on the server.

if(isset($_GET['temp'])){
    $fp = fopen('reading.txt','r+');
    fwrite($fp,$_GET['temp']);
    fwrite($fp,$_GET['outtemp']);
    fwrite($fp,$_GET['hum']);
    fwrite($fp,time());
    fclose($fp);
}

The webpage ‘controls’ the thermostat by writing a string of thermostat-button-states to another text file and the Arduino reads it. The string looks something like “”. The first digit ’0′ is the fan state, ’1′ is system state, ’76′ is the cooling temperature and ’68′ is the heating temperature.

As an example, there’s a button for the system state on the webpage.

</pre>
<form action="" method="POST"><input type="image" name="system" src="thermostatBtn.png" /> class="Btn" id="systemBtn" value="System" />

And clicking the system button on the webpage is handled like so:

if(isset($_POST['system'])){
    $fp = fopen('control.txt','r+');
    fseek($fp,2);
    $systemState++;
    if($systemState == 3) $systemState = 0;
    fwrite($fp,$systemState);
    fclose($fp);
}

The Arduino requests the text file contents and parses the string. There’s a lot of extraneous information that’s returned, so the Arduino stores just what’s in between ‘<’ and ‘>’. A good example is found here.


There’s about a 10 second delay after every communication loop between the Arduino and the webpage. The timestamp of the last update is visible on the webpage. So if the webpage shows it hasn’t been updated in over a minute, it’s safe to assume the Arduino farted, and won’t respond to user input.

demo


Here’s the thermostat control page. Use your browser to view the page source to see the code.

thermostat.pde

/*
Controls a Honeywell FocusPro th5110d via a php webpage

Parts used:
* Arduino Uno
* Arduino Ethernet Shield
* 4x 2n3904 transistors
* 4x 1k resistors
* 4.7k resistor
* tmp36 temperature sensor
* dth22 temperature/humidity sensor

created 16 Aug 2011
by Keelan Chu For

http://www.keelanchufor.com

*/

#include
#include
#include   // for atoi in setStates()
#include

int lag = 1;    // 8/11/11: accounts for the display-lighting effect of the
                // temp change butttons.
                // 1st press only lights display. subsequent presses change temp
char instruction[6];
boolean startRead = false;
boolean startSwitch = false;
int failed = 0;
int temp=75;    // must be 2 digits
int outTemp;
int humidity;
int fanState, lastFanState = 0;
int systemState, lastSystemState = 0;
int coolTempState, lastCoolTempState = 76;
int heatTempState, lastHeatTempState = 70;
#define v1Pin 2
#define v2Pin 3
#define g1Pin 4
#define g2Pin 5
#define resetPin 8
#define tempPin A0
#define dhtPin 9

DHT dht(dhtPin, DHT22);
//byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };
byte mac[] = { 0x90, 0xA2, 0xDA, 0x00, 0x59, 0x94 };
byte ip[] = { 192, 168, 1, 199 };
byte server[] = { 208, 94, 116, 3 }; // keelanchufor
byte server2[] = { 208, 94, 116, 123 };

Client client(server, 80);

void setup()
{
  analogReference(EXTERNAL);
  pinMode(8,OUTPUT);
  pinMode(v1Pin, OUTPUT);
  pinMode(v2Pin, OUTPUT);
  pinMode(g1Pin, OUTPUT);
  pinMode(g2Pin, OUTPUT);
  dht.begin();
  //Ethernet.begin(mac, ip);
  ethernetReset();
  Serial.begin(9600);
  Serial.println("starting thermostat..");
  delay(2000);
}

void loop()
{
  readTemp();
  readDHT();
  connectAndWrite();
  Serial.println();
  delay(5000);
  connectAndRead();
  Serial.println();
  checkAndSwitch();
  // if failed twice or more, pause and wait for reset
  if (failed >= 2) {
    Serial.println("Failing. Resetting Ethernet Shield..");
    ethernetReset();
    failed = 0;
  }
  Serial.print("\n\n");
  delay(5000);
}

functions.pde

// connect to sever and set indoor temperature
void connectAndWrite()
{
  Serial.println("connecting to send...");

  if (client.connect()) {
    Serial.println("connected. Sending..");
    client.println("GET /thermostat/control.php?temp="+ String(temp) +\
                   "&outtemp="+ String(outTemp) +\
                   "&hum="+ String(humidity) +" HTTP/1.0");
    client.println("Host: www.keelanchufor.com");
    client.println();
    delay(1000);    // dunno why, but necessary for successful request
    client.stop();
    client.flush();
    Serial.println("disconnected");
  } else {
    Serial.println("connection failed");
    failed++;
  }
}

// connect to server and read control.txt
void connectAndRead()
{
  Serial.println("connecting to read...");

  if (client.connect()) {
    Serial.println("connected. Receiving..");
    client.println("GET /thermostat/control.txt HTTP/1.0");
    client.println("Host: www.keelanchufor.com");
    client.println();
    readPage();
    for(int i=0;i<6;i++) Serial.print(instruction[i]);
    Serial.println();
    setStates(fanState, systemState, coolTempState, heatTempState);
    Serial.print("fanstate "); Serial.println(fanState);
    Serial.print("systemstate "); Serial.println(systemState);
    Serial.print("coolstate "); Serial.println(coolTempState);
    Serial.print("hotstate "); Serial.println(heatTempState);
  } else {
    Serial.println("connection failed");
    failed++;
  }
}

// get character array between '<' and '>'
void readPage()
{
  int i = 0;
  while(1) {
    if (client.available()) {
      char c = client.read();
      //Serial.print(c);
      if (c=='<') startRead = true;       else if (startRead) {         if (c!='>') {
          instruction[i] = c;
          i++;
        }
        else {
          startRead = false;
          client.stop();
          client.flush();
          Serial.println("disconnected");
          break;
        }
      }
    }

/*    if (!client.connected()) {
       client.stop();
       Serial.println("disconnected");
       break;
    }*/
  }
}

void setStates(int &fan, int &sys, int &cool, int &heat)
{
  fan = instruction[0] - 48;  // convert char to int
  sys = instruction[1] - 48;
  char buffer[3] = { instruction[2], instruction[3] };
  cool = atoi(buffer);
  buffer[0] = instruction[4];
  buffer[1] = instruction[5];
  heat = atoi(buffer);
}

void checkAndSwitch()
{
  // Avoid switching if the connection has been failing. Without this check,
  // coolTempState and heatTempState (which haven't been set  by setStates yet,
  // due to connectAndRead 'failing') will try to switch to the last-state variables
  if (failed < 2) {     // has any state changed since last check?     if ( (startSwitch == false) && \         ((fanState != lastFanState) || (systemState != lastSystemState) || \          (coolTempState != lastCoolTempState) || (heatTempState != lastHeatTempState)) )     {       startSwitch = true;       Serial.println("a state has changed");       // for debugging //      Serial.println("last states: "); //      Serial.print(lastFanState); //      Serial.print(lastSystemState); //      Serial.print(lastCoolTempState); //      Serial.println(lastHeatTempState);     }     // if so, then wait a cycle then double-check, but individually     else if (startSwitch) {       Serial.println("checking individual state changes");       if (fanState != lastFanState) {         Serial.print("switching fan.. ");         switches(v1Pin,g1Pin);         lastFanState = fanState;         lag = 0;       }       while (systemState != lastSystemState) {         Serial.print("switching system.. ");         switches(v1Pin,g2Pin);         lastSystemState++;         if(lastSystemState == 3) lastSystemState = 0;         lag = 0;       }       if ((coolTempState != lastCoolTempState) && (systemState == 1)) {         Serial.print("changing cool temperature.. ");         int delta = coolTempState - lastCoolTempState;         if (delta > 0) for (int i=0; i<(delta+lag); i++) switches(v2Pin,g1Pin);
        else for (int i=0; i<(abs(delta)+lag); i++) switches(v2Pin,g2Pin);         lastCoolTempState = coolTempState;         lag = 0;       }       if ((heatTempState != lastHeatTempState) && (systemState == 2)) {         Serial.print("changing heat temperature.. ");         int delta = heatTempState - lastHeatTempState;         if (delta > 0) for (int i=0; i<(delta+lag); i++) switches(v2Pin,g1Pin);
        else for (int i=0; i<(abs(delta)+lag); i++) switches(v2Pin,g2Pin);
        lastHeatTempState = heatTempState;
      }
      startSwitch = false;
      lag = 1;
    }
  }
  else {
    startSwitch = false;
    Serial.println("No switching allowed. Failed too many times to trust.");
  }
}

// toggle combination of thermostat's wire leads
void switches(int a, int b)
{
  Serial.print("setting.. ");
  digitalWrite(a,HIGH);
  digitalWrite(b,HIGH);
  delay(300);
  digitalWrite(a, LOW);
  digitalWrite(b, LOW);
  // wait to make sure the switch was made
  delay(2000);
  Serial.println("set");
}

void ethernetReset() {
  pinMode(resetPin, OUTPUT);
  digitalWrite(resetPin, LOW);
  delay(200);
  digitalWrite(resetPin, HIGH);
  delay(200);
  pinMode(resetPin, INPUT);
  delay(200);
  Ethernet.begin(mac, ip);
}

// uses 3V3 as analog reference (3300/1024=3.223)
void readTemp() {
  temp = (analogRead(tempPin)*3.223 - 500)*0.18 + 32;  // convert to F
  Serial.print("Indoor temp: "); Serial.println(temp);
}

void readDHT() {
  // Reading temperature or humidity takes about 250 milliseconds!
  // Sensor readings may also be up to 2 seconds 'old' (its a very slow sensor)
  float h = dht.readHumidity();
  float t = dht.readTemperature();

  // check if returns are valid, if they are NaN (not a number) then something went wrong!
  if (isnan(t) || isnan(h)) {
    Serial.println("Failed to read from DHT");
  } else {
    outTemp = t*9/5+32;
    humidity = h;
    Serial.print("Humidity: ");
    Serial.print(humidity);
    Serial.print(" %\t");
    Serial.print("Temperature: ");
    Serial.print(outTemp);
    Serial.println(" *F");
  }
}

future work

The arduino and the thermostat can get out of sync if the thermostat buttons are used. As of now, the arduino has no way of knowing the state of the thermostat unless it is doing the changes itself.

The current solution is to reset the arduino to the default state (fan: auto; system: off; coolTemp: 76; heatTemp: 70) and manually input those settings on the thermostat, before leaving the house.

A few other remedies to consider:

  • Donate an iPod touch to the rig. I have an extra laying around, not being used. It could be connected to the thermostat webpage UI and placed over the actual thermostat, covering the display and buttons. This idea could hold more weight as I expand the system functions to automate more things.
  • Indicate the current state the arduino thinks the thermostat is in, using an LCD, 7 segment displays, LEDs or a beeper. Then use push buttons to adjust it.
  • Figure out an elegant way for the arduino to recognize a button has been pushed.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>