So I automated the thermostats, and the pool; there are various sensors reporting things and also some X10 devices. That means something needs to watch over things for me and respond to my requests. I bought an Aduino Mega2560 and have it set up as my house controller. It really does very little autonomously....yet. But it will over time. Right now it handles the feed to Pachube where I collect data and has a web server that I use as an interface to control the house functions.
This is by far my most complex project, but not the hardest. I didn't have to invent anything for this, just interface with the other stuff around the house. The support devices were much harder because I had to invent most of them or study complex protocols to get them going. So, this will probably be the longest writeup I have. Scroll down to whatever interests you if you get bored with the narrative.
This is the device on a shelf working away. The display alternates between several items I want to watch, including data on its actual memory usage and current status.
.
An interior view. Notice that I have a lot of room to expand. I could easily fit another Mega2560 in there. The physical size was partially dictated by the huge SparkFun 5V serial display. I wanted some room on the left side of the display for buttons and such over time and this footprint is what I wound up with. Not a bad size since it is going on a shelf though. Notice the XBee stuck to the top of the box on the right. The wire antenna sticks through a hole in the top of the box and is barely visible in the picture above.
Here's a little video of the initialization sequence and rotating display in action. Notice how the data is updating asynchronously from the various devices around the house. As each one reports, the corresponding LED flashes. I also track the updates to my Pachube feed so I can tell if data is going out for graphing later. And, yes, I have a parrot that is screeching in the background.
Software:
It takes a whole lot of code to do this and one long sketch was too darn hard to manipulate. I found that scrolling through a couple of hundred lines of code to find some variable was a real pain so I started using the tabs. I have 'modules' for the major functions: HandlePacube for the data feed code, Handle Ethernet for the raw ethernet interaction, HandleThermostats, HandleXbee, etc. Makes it much easier to find things.
I also used timers to control things. Timers are different from alarms; alarms fire at a certain time. So something that happens at noon every day is an alarm. Something that happens a few seconds from now, is a timer. So to flash an LED I turn it on and set a timer for one second from now to turn it off. That way I don't have to have delays that block actions and I can let the code go off doing other things until the timer fires. The TimeAlarm arduino library is great for this. Timers can also simplify things like command retries updating displays and anything that needs to happen in sequence.
For example, do thing 1, set a timer for thing 2. When the thing 2 timer fires, set a timer for thing 3, etc. This is basically how I handle a rotating display. I have a timer that fires every few seconds and a state machine that steps between items I want to show up on an LCD display. This way I can show the time, date, temperature, etc one after the other on a display without having to worry about timing it.
The main sketch:
This is where the initialization and main loop are. I have the web server code in here also. I keep certain items such as the last IP address and values of interest that I accumulate over time. Since there is a bug in the Arduino Mega2560 bootloader where the watchdog timer doesn't work, I use timer3 as an interrupt timer and monitor the board's activity. If the timer fires, I reboot the board. Since there are many libraries and heavy interaction, chasing down all the possible bugs and hardware hangups is practically impossible. Using the timer interrupt allows the board to reset itself if something hangs it up. This can be a problem sometimes when it reboots because of a lockup on the internet. Fortunately, this doesn't happen often, but I will have to investigate this in more detail over time.
Main Sketch
/*
to do:
I need to allow for Pachube to fail and still control stuff
inside the house. This will entail removing the failsafe from
pachube. I think I still need the failsafes for the internal
network though.
*/
#include <SPI.h>
#include <Ethernet.h>
#include <EthernetDNS.h>
#include <avr/io.h>
#include <avr/wdt.h>
#include <MemoryFree.h>
#include <SoftwareSerial.h>
#include <Time.h>
#include <TimeAlarms.h>
#include <EEPROM.h>
#include <TextFinder.h>
#include <TimerThree.h>
#define ethernetResetPin 2 // used to reset the ethernet board
#define rxSense 3 // used to check the state of the ethernet board
#define lcdDisplayTx 23 // serial lcd display
#define lcdDisplayRx 22 // I don't use this, but it has to be sent to NSS
SoftwareSerial lcdDisplay (lcdDisplayRx, lcdDisplayTx);
//Status LEDs so I can see what's happening
// first the communication LEDs
#define timeRxLed 25
#define tempRxLed 26
#define powerRxLed 24
#define etherRxLed 27
#define pachubeUpdateLed 28
#define poolRxLed 29
#define statusRxLed 30
#define securityOffLed 31
#define extEtherRxLed 39
// Now the heat pump status LEDs
#define nRecircLed 36
#define nCoolLed 34
#define nHeatLed 32
#define sRecircLed 37
#define sCoolLed 35
#define sHeatLed 33
struct savedstuff //data that needs to be saved across reboots
{
long magic_number;
int highestTemp;
int lowestTemp;
byte lastIp[4];
} eepromData, tempEepromData;
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;
}
time_t pachubeUpdateTime = 0;
// Enter a MAC address and IP address for your controller below.
// The IP address will be dependent on your local network:
byte mac[] = {0x90, 0xA2, 0xDA, 0x00, 0x33, 0x33};
byte ip[] = { 192,168,0,205 };
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 pachubeServer[] = {173,203,98,29 };
byte dnsServerIp[] = { 192, 168, 0, 1 };
byte pachubeServerIp[] = {0,0,0,0}; //set by DNS
byte ipCheckIp[] = {0,0,0,0};
byte dyndnsIp[] = {0,0,0,0};
byte externalIp[] = {0,0,0,0};
byte zeroMask[] = {0,0,0,0};
Client ipCheck(ipCheckIp, 80);
Client dyndns(dyndnsIp, 80);
TextFinder ipfinder(ipCheck);
char Dbuf[100]; //general purpose buffer for strings
char Dbuf2[100]; // this is only for sprintf stuff
char etherIn[50]; // accumulated ethernet input
char xbeeIn[100]; // accumulated XBee input
float realPower,apparentPower,powerFactor,rmsCurrent,rmsVoltage,frequency;
struct PoolData{
char motorState [10];
char waterfallState [5];
char lightState [5];
char fountainState [5];
char solarState [5];
int poolTemp;
int airTemp;
} poolData;
time_t tNow;
// Initialize the Ethernet server library
// with the IP address and port
Server Control(80);
// the pachube connection
Client pachube(pachubeServerIp, 80);
// Thermostats 0 is the North one and 1 is the South
byte NThermoAddress[] = {192,168,0,202};
byte SThermoAddress[] = {192,168,0,203};
Client NThermo(NThermoAddress, 80);
Client SThermo(SThermoAddress, 80);
Client Thermostats[] = {NThermo, SThermo};
struct thermoData{
char name[10];
int currentTemp;
char state[10];
int tempSetting;
char mode[10];
char fan[10];
char peak[10];
} ThermoData[2];
float outsideTemp = 0;
void showMem(){
strcpy_P(Dbuf,PSTR("Mem = "));
Serial.print(Dbuf);
Serial.println(freeMemory());
}
void eepromAdjust(){
EEPROM_readAnything(0, tempEepromData); // get saved settings
if((eepromData.magic_number != tempEepromData.magic_number) ||
(eepromData.highestTemp != tempEepromData.highestTemp) ||
(eepromData.lowestTemp != tempEepromData.lowestTemp) ||
(memcmp(eepromData.lastIp, tempEepromData.lastIp,4) != 0)){
strcpy_P(Dbuf, PSTR("eeprom Data updated"));
Serial.println(Dbuf);
EEPROM_writeAnything(0, eepromData);
}
}
unsigned long resetTimer = 0;
void checkActive(){
if(millis() - resetTimer > 10000){
strcpy_P(Dbuf,PSTR("Fake Watchdog Timer Reset.........."));
Serial.print(Dbuf);
resetMe();
}
}
void setup()
{
int errorCount = 0;
Serial.begin(57600);
Serial.println("Initializing..");
EEPROM_readAnything(0,eepromData);
if (eepromData.magic_number != 1234){ //initialize the eeprom first time
eepromData.magic_number = 1234;
eepromData.highestTemp = 0;
eepromData.lowestTemp = 0;
memcpy (eepromData.lastIp, externalIp,4);
EEPROM_writeAnything(0,eepromData);
strcpy_P(Dbuf,PSTR("Initialized EEPROM "));
Serial.print(Dbuf);
}
lcdDisplay.begin(9600);
lcdInit();
lcdLine1Print("Initializing..");
// set up the Status LEDs
initLeds();
// start the Ethernet connection and the server:
Serial.println("Ethernet..");
lcdLine2Print("Ethernet..");
ethernetReset();
Ethernet.begin(mac, ip);
EthernetDNS.setDNSServer(dnsServerIp);
lcdLine2Print("Pachube");
getIpAddress("www.pachube.com", pachubeServerIp);
strcpy_P(Dbuf,PSTR("Pachube address is: "));
Serial.print(Dbuf);
Serial.println(ip_to_str(pachubeServerIp));
lcdLine2Print("Public IP Addr");
getIpAddress("members.dyndns.org", dyndnsIp);
strcpy_P(Dbuf,PSTR("Members dyndns address is: "));
Serial.print(Dbuf);
Serial.println(ip_to_str(dyndnsIp));
getIpAddress("checkip.dyndns.com", ipCheckIp);
strcpy_P(Dbuf,PSTR("ipCheck address is: "));
Serial.print(Dbuf);
Serial.println(ip_to_str(ipCheckIp));
getPublicIp();
strcpy_P(Dbuf,PSTR("Public address is: "));
Serial.print(Dbuf);
Serial.println(ip_to_str(externalIp));
if (ipEqual(externalIp, eepromData.lastIp)){
Serial.println("Public IP didn't change");
}
else {
Serial.println("Public IP changed");
memcpy(eepromData.lastIp,externalIp,4);
eepromAdjust();
publishIp();
}
Control.begin(); // this starts the ethernet server
Serial.print("XBee..");
Serial1.begin(9600);
Serial.println("Done");
Serial.println("All Done.");
lcdLine2Print("Complete");
}
int etherIndex = 0;
boolean firsttime = true;
boolean alarmsSet = false;
int maxAlarms = 0;
void loop(){
if(firsttime == true){
Serial.print("I'm alive ");
showMem();
firsttime = false;
memset(xbeeIn,0,sizeof(xbeeIn));
resetTimer = millis();
Timer3.initialize(8000000);
Timer3.attachInterrupt(checkActive);
}
if( Alarm.count() > maxAlarms){ // keep track of the number of alarms used
maxAlarms = Alarm.count();
// Serial.print("Alarms at: ");
// Serial.println(maxAlarms);
}
resetTimer = millis(); // This tickles the watchdog
// if you don't wait until the time is set the timers will fire
// this could be bad if you have an alarm that you don't expect to go
// off when you first start up.
if (timeStatus() != timeNotSet){
tNow = now(); // just to be sure the time routines are ok
if (!alarmsSet){
initTimers();
alarmsSet = true;
Serial.println("Timers set up");
lcdClear();
lcdLine1Print("Timers Set up");
}
Alarm.delay(0); // and the alarm routines as well
}
// First, check for XBee updates to the power and time
while (Serial1.available() > 0){ //check for XBee input
checkXbee();
}
if (pachube.available()){ //for debugging pachube interaction
char c = pachube.read();
// Serial.print(c);
}
// listen for incoming clients that want to know what is happening.
if (Client ControlClient = Control.available()) { // is there a client connected?
memset(etherIn,0,sizeof(etherIn));
etherIndex=0;
while (ControlClient.connected()){ // get whatever it was they sent
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') {
etherIn[etherIndex++] = c;
if (etherIndex >= sizeof(etherIn)) // max size of buffer
etherIndex = sizeof(etherIn) - 1;
continue;
}
etherIn[etherIndex] = 0; // null terminate the line for parsing
ControlClient.flush(); // I really don't care about the rest
if(isLocal(ControlClient)){
strcpy_P(Dbuf,PSTR("Local "));
}
else {
strcpy_P(Dbuf, PSTR("Outside "));
}
strcat_P(Dbuf, PSTR("Machine Request:"));
Serial.println(Dbuf);
digitalWrite(etherRxLed,LOW); // indicator for internet activity
Alarm.timerOnce(1,etherRxLedOff); // this will turn it off in a second
if(!isLocal(ControlClient)){
digitalWrite(extEtherRxLed,LOW); // indicator for external access
Alarm.timerOnce(1,extEtherRxLedOff); // and turn it off in a second too
}
// Serial.println(etherIn);
processEtherIn(ControlClient, etherIn, etherIndex); // go parse the stuff they sent
break;
}
}
// give the web browser time to receive the data
Alarm.delay(1); // actually this isn't needed, but it checks alarms
// close the connection:
ControlClient.stop(); // signal client that we're done
}
}
// turn the security back on after timer fires
// I allow someone outside to change things for a time period
// and then automatically shut it off so I don't forget
boolean securityOn = true;
void turnSecurityOn(){
securityOn = true;
digitalWrite(securityOffLed,HIGH);
strcpy_P(Dbuf,PSTR("Security back on"));
Serial.println(Dbuf);
}
void processEtherIn(Client ControlClient, char *etherIn, int etherIndex){
char* buf;
// 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(etherIn, PSTR("GET / ")) != 0){ // root level request, send status
sendStatus(ControlClient);
return;
}
else if (strstr_P(etherIn,PSTR("GET /show")) != 0){ // alternative display command
sendStatus(ControlClient);
return;
}
else if (strstr_P(etherIn,PSTR("GET /favicon.ico")) != 0){ // stupid double hit for this thing
return;
}
else if (strstr_P(etherIn, PSTR("GET /supersecretpassword")) != 0){ // super secret password for external access
if(securityOn == true){ // you don't get to extend the time.
Alarm.timerOnce(5*60,turnSecurityOn);
digitalWrite(securityOffLed,LOW);
strcpy_P(Dbuf,PSTR("Security off"));
Serial.println(Dbuf);
securityOn = false;
}
sendStatus(ControlClient);
return;
}
//
// exclude external machines from making changes
// unless the machine has the secret command
//
if (!isLocal(ControlClient) && securityOn){ // not a local machine, bye
strcpy_P(Dbuf,PSTR("Denied"));
Serial.println(Dbuf);
ControlClient.println(Dbuf);
return;
}
else if (strstr_P(etherIn,PSTR("GET /reset")) != 0){ // remote reset, just for the heck of it
resetMe();
return; //Won't ever get here
}
else if ((buf=strstr_P(etherIn,PSTR("GET /"))) != 0){
int i = 0;
char *data, *thing, *state, *where;
char command[20];
data = strtok_r(buf, "/", &buf); // skip to actual command
thing = strtok_r(0, ",", &buf); // which thing is being changed
state = strtok_r(0, ",", &buf); // how it's being changed
where = strtok_r(0, ",", &buf); // where it is
if (strncmp_P(thing,PSTR("temp"),4) == 0){
if(strncmp_P(state,PSTR("up"),2) == 0){
strcpy_P(command, PSTR("+"));
}
else if (strncmp_P(state,PSTR("down"),4) == 0){
strcpy(command, "-");
}
else {
commandError(ControlClient);
return;
}
if(strncmp_P(where,PSTR("n"),1) == 0){
getThermostat(0, command, false);
}
else if(strncmp_P(where, PSTR("s"),1) == 0){
getThermostat(1, command, false);
}
else{
commandError(ControlClient);
return;
}
getThermostat(1, "status", true);
getThermostat(0, "status", true);
redirectBack(ControlClient);
return;
}
else if (strncmp_P(thing,PSTR("fan"),3) == 0){
if(strncmp_P(state,PSTR("on"),2) == 0){
strcpy_P(command, PSTR("fan=on"));
}
else if (strncmp_P(state,PSTR("auto"),4) == 0){
strcpy_P(command, PSTR("fan=auto"));
}
else if (strncmp_P(state,PSTR("recirc"),6) == 0){
strcpy_P(command, PSTR("fan=recirc"));
}
else {
commandError(ControlClient);
return;
}
if(strncmp_P(where,PSTR("n"),1) == 0){
getThermostat(0, command, false);
}
else if(strncmp_P(where, PSTR("s"),1) == 0){
getThermostat(1, command, false);
}
else{
commandError(ControlClient);
return;
}
getThermostat(1, "status", true);
getThermostat(0, "status", true);
redirectBack(ControlClient);
return;
}
else if (strncmp_P(thing,PSTR("mode"),4) == 0){
if(strncmp_P(state,PSTR("heat"),4) == 0){
strcpy_P(command, PSTR("heat"));
}
else if (strncmp_P(state,PSTR("cool"),4) == 0){
strcpy_P(command, PSTR("cool"));
}
else if (strncmp_P(state,PSTR("off"),3) == 0){
strcpy_P(command, PSTR("off"));
}
else {
commandError(ControlClient);
return;
}
if(strncmp_P(where,PSTR("n"),1) == 0){
getThermostat(0, command, false);
}
else if(strncmp_P(where, PSTR("s"),1) == 0){
getThermostat(1, command, false);
}
else{
commandError(ControlClient);
return;
}
getThermostat(1, "status", true);
getThermostat(0, "status", true);
redirectBack(ControlClient);
return;
}
else if (strncmp_P(thing,PSTR("pool"),4) == 0){
Serial.println("Pool command");
if(strncmp_P(state,PSTR("motorOff"),8) == 0){
Serial.println("motor off");
poolMotorOff();
redirectBack(ControlClient);
}
else if(strncmp_P(state,PSTR("motorHigh"),9) == 0){
Serial.println("motor high");
poolMotorHigh();
redirectBack(ControlClient);
}
else if(strncmp_P(state,PSTR("motorLow"),8) == 0){
Serial.println("motor low");
poolMotorLow();
redirectBack(ControlClient);
}
else if(strncmp_P(state,PSTR("waterfallOn"),11) == 0){
Serial.println("waterfall on");
poolWaterfallOn();
redirectBack(ControlClient);
}
else if(strncmp_P(state,PSTR("waterfallOff"),12) == 0){
Serial.println("waterfall off");
poolWaterfallOff();
redirectBack(ControlClient);
}
else if(strncmp_P(state,PSTR("fountainOn"),10) == 0){
Serial.println("fountain on");
poolFountainOn();
redirectBack(ControlClient);
}
else if(strncmp_P(state,PSTR("fountainOff"),11) == 0){
Serial.println("fountain off");
poolFountainOff();
redirectBack(ControlClient);
}
else if(strncmp_P(state,PSTR("lightOn"),7) == 0){
Serial.println("light on");
poolLightOn();
redirectBack(ControlClient);
}
else if(strncmp_P(state,PSTR("lightOff"),8) == 0){
Serial.println("light off");
poolLightOff();
redirectBack(ControlClient);
}
else {
commandError(ControlClient);
}
return;
}
else {
commandError(ControlClient);
return;
}
}
else{
commandError(ControlClient);
return;
}
}
void commandError(Client client){
strcpy_P(Dbuf,PSTR("command error"));
client.println(Dbuf);
}
void redirectBack(Client ControlClient){
strcpy_P(Dbuf, PSTR("<strong>Hold on a few seconds while I do that...</strong>"));
ControlClient.print(Dbuf);
strcpy_P(Dbuf, PSTR("<meta http-equiv=\"refresh\" content=\"5; URL=show\">"));
ControlClient.print(Dbuf);
}
void sendStatus(Client ControlClient){
// standard http response header
strcpy_P(Dbuf, PSTR("<html><title>Desert Home</title>"));
ControlClient.print(Dbuf);
strcpy_P(Dbuf, PSTR("<head><style type=\"text/css\"> p.ex{width:700px;}</style></head>"));
ControlClient.print(Dbuf);
strcpy_P(Dbuf, PSTR("<body><h1><center>Desert Home Controller<br>"));
ControlClient.print(Dbuf);
// output the time
tNow = now();
strcpy_P(Dbuf2,PSTR("%02d/%02d/%4d %02d:%02d:%02d <br></center></h1><br><h2>"));
sprintf(Dbuf,Dbuf2,month(tNow),day(tNow),year(tNow),hour(tNow),minute(tNow),second(tNow));
ControlClient.print(Dbuf);
strcpy_P(Dbuf, PSTR("<div>"));
ControlClient.print(Dbuf);
strcpy_P(Dbuf2,PSTR("Current power usage is %d Watts, Outside Temp is %dF<br>"));
sprintf(Dbuf,Dbuf2, (int)round(realPower), (int)round(outsideTemp));
ControlClient.println(Dbuf);
strcpy_P(Dbuf, PSTR("<img src=\"http://www.pachube.com/feeds/9511/datastreams/0/history.png?w=300&h=100&b=true&g=true\" /> "));
ControlClient.print(Dbuf);
strcpy_P(Dbuf, PSTR("<img src=\"http://www.pachube.com/feeds/9511/datastreams/7/history.png?w=300&h=100&b=true&g=true\" />"));
ControlClient.print(Dbuf);
strcpy_P(Dbuf, PSTR("</div>"));
ControlClient.print(Dbuf);
strcpy_P(Dbuf, PSTR("<div>"));
ControlClient.print(Dbuf);
for ( int i=0; i<=1; i++){
strcpy_P(Dbuf,PSTR("<p class=\"ex\">"));
ControlClient.print(Dbuf);
strcpy_P(Dbuf2,PSTR("<img src=\"%s\" alt=\"FAN\" align=\"right\" align=\"middle\" width=\"50\" height=\"50\" />"));
if(strcmp_P(ThermoData[i].state, PSTR("Recirc")) == 0){
sprintf(Dbuf,Dbuf2,"http://tinyurl.com/3umagvq");
}
else if(strcmp_P(ThermoData[i].state, PSTR("Heating")) == 0){
sprintf(Dbuf,Dbuf2,"http://tinyurl.com/63zc8gb");
}
else if(strcmp_P(ThermoData[i].state, PSTR("Cooling")) == 0){
sprintf(Dbuf,Dbuf2,"http://tinyurl.com/3n95mqz");
}
else if (strcmp_P(ThermoData[i].state, PSTR("Idle")) == 0){
sprintf(Dbuf,Dbuf2,"http://tinyurl.com/44h9o4c");
}
ControlClient.print(Dbuf);
strcpy_P(Dbuf2,PSTR("%s Thermostat:<br>"));
sprintf(Dbuf,Dbuf2, i==0 ? "North" : "South");
ControlClient.print(Dbuf);
strcpy_P(Dbuf2,PSTR("Temp is %d, Heat pump is %s, <br>Temp Setting is %d, <br>Mode %s, <br>Fan %s, <br>%s Period<br>"));
sprintf(Dbuf,Dbuf2, ThermoData[i].currentTemp,
ThermoData[i].state,
ThermoData[i].tempSetting,
ThermoData[i].mode,
ThermoData[i].fan,
ThermoData[i].peak);
ControlClient.println(Dbuf);
strcpy_P(Dbuf2, PSTR("<button onClick=\"window.location='temp,up,%s'\"><img src=\"http://tinyurl.com/4yqtv5a\"></button>"));
sprintf(Dbuf,Dbuf2, i==0 ? "n" : "s");
ControlClient.print(Dbuf);
strcpy_P(Dbuf2, PSTR("<button onClick=\"window.location='temp,down,%s'\"><img src=\"http://tinyurl.com/3vr29ek\"></button>"));
sprintf(Dbuf,Dbuf2, i==0 ? "n" : "s");
ControlClient.print(Dbuf);
strcpy_P(Dbuf2, PSTR("<button onClick=\"window.location='fan,auto,%s'\"><img src=\"http://tinyurl.com/3v49j26\"></button>"));
sprintf(Dbuf,Dbuf2, i==0 ? "n" : "s");
ControlClient.print(Dbuf);
strcpy_P(Dbuf2, PSTR("<button onClick=\"window.location='fan,on,%s'\"><img src=\"http://tinyurl.com/3wfcrmr\"></button>"));
sprintf(Dbuf,Dbuf2, i==0 ? "n" : "s");
ControlClient.print(Dbuf);
strcpy_P(Dbuf2, PSTR("<button onClick=\"window.location='fan,recirc,%s'\"><img src=\"http://tinyurl.com/3u3qewu\"></button>"));
sprintf(Dbuf,Dbuf2, i==0 ? "n" : "s");
ControlClient.print(Dbuf);
strcpy_P(Dbuf2, PSTR("<button onClick=\"window.location='mode,off,%s'\"><img src=\"http://tinyurl.com/3vdjdbg\"></button>"));
sprintf(Dbuf,Dbuf2, i==0 ? "n" : "s");
ControlClient.print(Dbuf);
strcpy_P(Dbuf2, PSTR("<button onClick=\"window.location='mode,heat,%s'\"><img src=\"http://tinyurl.com/3swle62\"></button>"));
sprintf(Dbuf,Dbuf2, i==0 ? "n" : "s");
ControlClient.print(Dbuf);
strcpy_P(Dbuf2, PSTR("<button onClick=\"window.location='mode,cool,%s'\"><img src=\"http://tinyurl.com/44fyvmt\"></button>"));
sprintf(Dbuf,Dbuf2, i==0 ? "n" : "s");
ControlClient.print(Dbuf);
// these are the URLs for the animated fans
// black stationary http://tinyurl.com/44h9o4c
// red fan http://tinyurl.com/63zc8gb
// green fan http://tinyurl.com/3umagvq
// blue fan http://tinyurl.com/3n95mqz
strcpy_P(Dbuf, PSTR("<br><p>"));
ControlClient.print(Dbuf);
}
strcpy_P(Dbuf, PSTR("</div>"));
ControlClient.print(Dbuf);
// pool Status
strcpy_P(Dbuf, PSTR("<div>"));
ControlClient.print(Dbuf);
strcpy_P(Dbuf, PSTR("Pool: <br>"));
ControlClient.print(Dbuf);
strcpy_P(Dbuf2, PSTR("Motor is %s <br>"));
sprintf(Dbuf, Dbuf2, poolData.motorState);
ControlClient.print(Dbuf);
// if the motor isn't running, the temperature is invalid
if (strcmp_P(poolData.motorState, PSTR("Off")) != 0){
strcpy_P(Dbuf2, PSTR("Water Temperature is %d <br>"));
sprintf(Dbuf, Dbuf2, poolData.poolTemp);
ControlClient.print(Dbuf);
}
strcpy_P(Dbuf2, PSTR("Light is %s <br>"));
sprintf(Dbuf, Dbuf2, poolData.lightState);
ControlClient.print(Dbuf);
strcpy_P(Dbuf2, PSTR("Solar Heating is %s <br>"));
sprintf(Dbuf, Dbuf2, poolData.solarState);
ControlClient.print(Dbuf);
strcpy_P(Dbuf2, PSTR("Waterfall is %s <br>"));
sprintf(Dbuf, Dbuf2, poolData.waterfallState);
ControlClient.print(Dbuf);
strcpy_P(Dbuf2, PSTR("Fountain is %s <br>"));
sprintf(Dbuf, Dbuf2, poolData.fountainState);
ControlClient.print(Dbuf);
strcpy_P(Dbuf, PSTR("<button onClick=\"window.location='pool,motorOff'\">Motor Off</button>"));
ControlClient.print(Dbuf);
strcpy_P(Dbuf, PSTR("<button onClick=\"window.location='pool,motorHigh'\">Motor High</button>"));
ControlClient.print(Dbuf);
strcpy_P(Dbuf, PSTR("<button onClick=\"window.location='pool,motorLow'\">Motor Low</button>"));
ControlClient.print(Dbuf);
strcpy_P(Dbuf, PSTR("<button onClick=\"window.location='pool,waterfallOn '\">Waterfall On</button>"));
ControlClient.print(Dbuf);
strcpy_P(Dbuf, PSTR("<button onClick=\"window.location='pool,waterfallOff'\">Waterfall Off</button>"));
ControlClient.print(Dbuf);
strcpy_P(Dbuf, PSTR("<button onClick=\"window.location='pool,fountainOn'\">Fountain On</button>"));
ControlClient.print(Dbuf);
strcpy_P(Dbuf, PSTR("<button onClick=\"window.location='pool,fountainOff'\">Fountain Off</button>"));
ControlClient.print(Dbuf);
strcpy_P(Dbuf, PSTR("<button onClick=\"window.location='pool,lightOn'\">Light On</button>"));
ControlClient.print(Dbuf);
strcpy_P(Dbuf, PSTR("<button onClick=\"window.location='pool,lightOff'\">Light Off</button>"));
ControlClient.print(Dbuf);
strcpy_P(Dbuf, PSTR("</div>"));
ControlClient.print(Dbuf);
strcpy_P(Dbuf, PSTR("<br></h2></body></head></html>"));
ControlClient.println(Dbuf);
}
This handles the LCD display and various LEDs. I have a RGB LED for each heat pump which is red for heat, green for fan only and blue for cooling. Makes for a cool way to tell what is going on with the air conditioning. The LCD display is a SparkFun 5V serial. This is the last one of these I have and I won't get another. The boards are way too big for the size of the display and this just makes it hard to mount and requires a large enclosure. This is a rotating display that steps through several values so I don't have to have a complicated large display to show the various house data. This is where I have a timer that fires and calls this LCD display routine and updates a state variable to step between the values. I also initialize the LEDs here in sequence for fun. When you turn the monitor on all the lights come on and then it steps through the colors of the RGB LED and turns the lights off in sequence. This is totally for fun, but it does tell you the lights are all working.
char lcdBuf[17]; // to handle the various text items
//
// Sparkfun LCD display routines
//
void lcdGoto(int position) { //position = line 1: 0-15, line 2: 16-31, 31+ defaults back to 0
if (position<16)
{
lcdDisplay.print(0xFE, BYTE); //command flag
lcdDisplay.print((position+128), BYTE); //position
}
else if (position<32){
lcdDisplay.print(0xFE, BYTE); //command flag
lcdDisplay.print((position+48+128), BYTE); //position
}
else
{
lcdGoto(0);
}
}
void lcdLine1()
{
lcdDisplay.print(0xFE, BYTE); //command flag
lcdDisplay.print(128, BYTE); //position to line 1
}
void lcdLine1Print(char *buf){
lcdLine1();
lcdDisplay.print(" ");
lcdLine1();
lcdDisplay.print(buf);
}
void lcdLine2()
{
lcdDisplay.print(0xFE, BYTE); //command flag
lcdDisplay.print(192, BYTE); //position to line 2
}
void lcdLine2Print(char *buf){
lcdLine2();
lcdDisplay.print(" ");
lcdLine2();
lcdDisplay.print(buf);
}
void lcdClear(){
lcdDisplay.print(0xFE, BYTE); //command flag
lcdDisplay.print(0x01, BYTE); //clear command.
delay(100);
}
void lcdInit(){
lcdDisplay.begin(9600);
lcdDisplay.print(0x7C, BYTE); // Intensity to max
lcdDisplay.print(157, BYTE);
delay(100);
lcdClear();
}
#define showTime 1
#define showDate 2
#define showPower 3
#define showInsideTemp 4
#define showOutsideTemp 5
#define showPoolTemp 6
#define showDataUpdateTime 7
#define showMemLeft 8
int displayState = showTime;
void rotatingDisplay(){
int poolMotorState = 0;
switch (displayState){
case showTime:
displayState = showDate;
lcdLine1Print(" Time");
strcpy_P(Dbuf2,PSTR(" %3s %2d:%02d%c"));
sprintf(lcdBuf, Dbuf2, dayShortStr(weekday()),hourFormat12(),minute(),(isAM()?'A':'P'));
lcdLine2Print(lcdBuf);
// while(1){}; //interrupt test
break;
case showDate:
displayState = showPower;
lcdLine1Print(" Date");
strcpy_P(Dbuf2,PSTR(" %d/%d/%d"));
sprintf(lcdBuf, Dbuf2, month(), day(), year());
lcdLine2Print(lcdBuf);
break;
case showPower:
displayState = showInsideTemp;
lcdLine1Print(" Power Usage");
strcpy_P(Dbuf2,PSTR(" %5dW"));
sprintf(lcdBuf, Dbuf2, (int)round(realPower));
lcdLine2Print(lcdBuf);
break;
case showInsideTemp:
displayState = showOutsideTemp;
lcdLine1Print(" Inside Temp");
strcpy_P(Dbuf2,PSTR(" %dF"));
sprintf(lcdBuf, Dbuf2, (ThermoData[0].currentTemp + ThermoData[1].currentTemp)/2);
lcdLine2Print(lcdBuf);
break;
case showOutsideTemp:
displayState = showPoolTemp;
lcdLine1Print(" Outside Temp");
strcpy_P(Dbuf2,PSTR(" %dF"));
sprintf(lcdBuf, Dbuf2, (int)round(outsideTemp));
lcdLine2Print(lcdBuf);
break;
case showPoolTemp:
displayState = showDataUpdateTime;
lcdLine1Print(" Pool Temp");
if(strcmp(poolData.motorState,"High") == 0)
poolMotorState = 2;
else if(strcmp(poolData.motorState,"Low") == 0)
poolMotorState = 1;
if(poolMotorState == 0){
strcpy_P(lcdBuf,PSTR(" Motor Off"));
lcdLine2Print(lcdBuf);
}
else{
strcpy_P(Dbuf2,PSTR(" %dF"));
sprintf(lcdBuf, Dbuf2, poolData.poolTemp);
lcdLine2Print(lcdBuf);
}
break;
case showDataUpdateTime:
displayState = showMemLeft;
lcdLine1Print("Data Update Time");
strcpy_P(Dbuf2,PSTR(" %2d:%02d"));
sprintf(lcdBuf, Dbuf2, hour(pachubeUpdateTime), minute(pachubeUpdateTime));
lcdLine2Print(lcdBuf);
break;
case showMemLeft:
displayState = showTime;
strcpy_P(Dbuf2,PSTR("Timers: %d of %d"));
sprintf(lcdBuf, Dbuf2, maxAlarms,dtNBR_ALARMS );
lcdLine1Print (lcdBuf);
strcpy_P(Dbuf2,PSTR("Mem Left: %d"));
sprintf(lcdBuf, Dbuf2, freeMemory());
lcdLine2Print(lcdBuf);
break;
default:
displayState = showTime;
lcdLine1Print(" OOPS! ");
lcdLine2Print(" BUG ");
break;
}
Alarm.timerOnce(3,rotatingDisplay);
}
// The routine below is totally useless.
// I just thought it would be cool to flash the lignts in sequence
// purely for fun.
// I guess it does show that the leds work OK though
void initLeds(){
int i;
for(i=24; i<32; i++){
pinMode(i,OUTPUT);
digitalWrite(i,LOW);
}
pinMode(39,OUTPUT);
digitalWrite(39,LOW);
delay(250);
pinMode(nRecircLed,OUTPUT);
digitalWrite(nRecircLed,LOW);
pinMode(sRecircLed,OUTPUT);
digitalWrite(sRecircLed,LOW);
delay(500);
digitalWrite(nRecircLed,HIGH);
digitalWrite(sRecircLed,HIGH);
pinMode(nCoolLed,OUTPUT);
digitalWrite(nCoolLed,LOW);
pinMode(sCoolLed,OUTPUT);
digitalWrite(sCoolLed,LOW);
delay(500);
digitalWrite(nCoolLed,HIGH);
digitalWrite(sCoolLed,HIGH);
pinMode(nHeatLed,OUTPUT);
digitalWrite(nHeatLed,LOW);
pinMode(sHeatLed,OUTPUT);
digitalWrite(sHeatLed,LOW);
delay(500);
digitalWrite(nHeatLed,HIGH);
digitalWrite(sHeatLed,HIGH);
delay(250);
digitalWrite(powerRxLed, HIGH);
delay(250);
digitalWrite(timeRxLed, HIGH);
delay(250);
digitalWrite(tempRxLed, HIGH);
delay(250);
digitalWrite(etherRxLed, HIGH);
delay(250);
digitalWrite(extEtherRxLed, HIGH);
delay(250);
digitalWrite(securityOffLed, HIGH);
delay(250);
digitalWrite(statusRxLed, HIGH);
delay(250);
digitalWrite(poolRxLed, HIGH);
delay(250);
digitalWrite(pachubeUpdateLed, HIGH);
}
HandleDyndns:
OK, if you're going to have it on the web, you have to have a web address. Dyndns is a service that handles this for me. See, the address your DSL modem uses isn't associated with a name and Dyndns will take care of this association for you. However, since my ISP changes my darn address a couple of times a day, I have to actually get my new address and update the Dyndns server periodically. This code does this for me on a 10 minute interval. Again, this is done with a timer.
HandleDyndns
boolean ipCheckConnected = false;
boolean dyndnsConnected = false;
boolean tryGetPublicIp()
{
int waitcnt = 0;
long timer = 0;
while(1){
if (!ipCheckConnected){
strcpy_P(Dbuf,PSTR("ipCheck Connecting..."));
Serial.print(Dbuf);
if (ipCheck.connect()) {
ipCheckConnected = true;
waitcnt++;
strcpy_P(Dbuf,PSTR("OK"));
Serial.println(Dbuf);
strcpy_P(Dbuf,PSTR("GET / HTTP/1.1\n"));
// Serial.println(Dbuf); // for debug
ipCheck.println(Dbuf);
timer = millis(); // to time the response, just in case
}
else{
strcpy_P(Dbuf,PSTR("failed"));
Serial.println(Dbuf);
waitcnt++;
timer = millis(); // if the connection failed I don't need to time it.
ipCheck.flush();
ipCheck.stop();
while (ipCheck.status() != 0){
delay(5);
}
}
}
if (ipCheck.available() > 0){
if(ipfinder.find("Address:")){
externalIp[0] = (byte)ipfinder.getValue();
externalIp[1] = (byte)ipfinder.getValue();
externalIp[2] = (byte)ipfinder.getValue();
externalIp[3] = (byte)ipfinder.getValue();
ipCheck.flush();
ipCheck.stop();
while (ipCheck.status() != 0){
delay(5);
}
return(true);
}
strcpy_P(Dbuf,PSTR("Invalid Response"));
Serial.println(Dbuf);
ipCheck.flush();
ipCheck.stop();
while (ipCheck.status() != 0){
delay(5);
}
waitcnt++;
ipCheckConnected = false;
}
if (ipCheckConnected && (millis() - timer >6000)){
strcpy_P(Dbuf,PSTR("Timeout waiting"));
Serial.println(Dbuf);
ipCheck.flush();
ipCheck.stop();
while (ipCheck.status() != 0){
delay(5);
}
ipCheckConnected = false;
waitcnt++;
}
if (!ipCheckConnected){
strcpy_P(Dbuf2,PSTR("ipCheck try # %d"));
sprintf(Dbuf,Dbuf2,waitcnt);
Serial.println(Dbuf);
delay (1000);
}
if (waitcnt > 9){
ipCheck.flush();
ipCheck.stop();
while (ipCheck.status() != 0){
delay(5);
}
return(false);
}
}
}
void getPublicIp(){
if(tryGetPublicIp() == false){
strcpy_P(Dbuf,PSTR("Public IP failure, rebooting"));
Serial.println(Dbuf);
resetMe(); //call reset if you can't get the comm to work
}
return;
}
boolean tryPublishIp()
{
int waitcnt = 0;
long timer = 0;
while(1){
if (!dyndnsConnected){
strcpy_P(Dbuf,PSTR("dyndns Connecting..."));
Serial.print(Dbuf);
if (dyndns.connect()) {
dyndnsConnected = true;
waitcnt++;
strcpy_P(Dbuf,PSTR("OK"));
Serial.println(Dbuf);
strcpy_P(Dbuf,PSTR("GET /nic/update?hostname=thomeauto.is-a-geek.org"));
dyndns.print(Dbuf);
strcpy_P(Dbuf2,PSTR("&myip=%d.%d.%d.%d&wildcard=NOCHG"));
sprintf(Dbuf,Dbuf2,externalIp[0],externalIp[1],externalIp[2],externalIp[3]);
dyndns.print(Dbuf);
strcpy_P(Dbuf,PSTR("&mx=NOCHG&backmx=NOCHG HTTP/1.0"));
dyndns.println(Dbuf);
strcpy_P(Dbuf,PSTR("Host: members.dyndns.org"));
dyndns.println(Dbuf);
/*let me in */
strcpy_P(Dbuf,PSTR("Authorization: Basic secretnumbergoesinhere"));
dyndns.println(Dbuf);
strcpy_P(Dbuf,PSTR("User-Agent: Thompson Home Control - Arduino Controller"));
dyndns.print(Dbuf);
strcpy_P(Dbuf,PSTR(" - Version 5"));
dyndns.println(Dbuf);
dyndns.println(""); //empty line to indicate end
timer = millis(); // to time the response, just in case
}
else {
strcpy_P(Dbuf,PSTR("failed"));
Serial.println(Dbuf);
waitcnt++;
timer = millis(); // if the connection failed I don't need to time it.
dyndns.flush();
dyndns.stop();
while (dyndns.status() != 0){
delay(5);
}
}
}
if (dyndns.available() > 0){
while(dyndns.available()){
int c = dyndns.read(); //Sometime I need to check this
Serial.print(c,BYTE);
}
dyndns.flush();
dyndns.stop();
while (dyndns.status() != 0){
delay(5);
}
return(true);
}
if (dyndnsConnected && (millis() - timer >5000)){
strcpy_P(Dbuf,PSTR("Timeout waiting"));
Serial.println(Dbuf);
dyndns.flush();
dyndns.stop();
while (dyndns.status() != 0){
delay(5);
}
dyndnsConnected = false;
}
if (!dyndnsConnected){
strcpy_P(Dbuf2,PSTR("dyndns try # %d"));
sprintf(Dbuf,Dbuf2,waitcnt);
Serial.println(Dbuf);
delay (1000);
}
if (waitcnt > 9){
dyndns.flush();
dyndns.stop();
while (dyndns.status() != 0){
delay(5);
}
return(false);
}
}
}
void publishIp(){
if(tryPublishIp() == false){
strcpy_P(Dbuf,PSTR("Publish IP failure, rebooting"));
Serial.println(Dbuf);
resetMe(); //call reset if you can't get the comm to work
}
return;
}
HandleEthernet:
This is the ethernet hardware handling code. I have had a ton of trouble establishing a connection and keeping it working. The code in here can reset the board and retry the connection. It'll also cause a board reset if necessary because I need the ethernet to make things work properly. There's also a couple of cool little routines in here to compare IP addresses. The routines use the netmask so one can tell if the address incoming is on the local LAN. This is important since I really, really don't want someone on the internet being able to turn the heater on in the middle of summer. Additionally, I use it to tell if the public IP address of my house has changed. This lets me know if I need to update the address in the DynDNS database (see directly above for more information on this). The DHCP handling is in here as well. DHCP helps me keep track of addresses that used to be hard coded. This is a nice little piece of code that is available on the Arduino web site.
HandleEthernet
/*
macro to compare two ip addresses using the netmask
Basically, it compares the addresses using only the zeros
in the net mask. This will tell you if the incoming
machine is local to your network.
*/
#define maskcmp(addr1, addr2, mask) \
(((((uint16_t *)addr1)[0] & ((uint16_t *)mask)[0]) == \
(((uint16_t *)addr2)[0] & ((uint16_t *)mask)[0])) && \
((((uint16_t *)addr1)[1] & ((uint16_t *)mask)[1]) == \
(((uint16_t *)addr2)[1] & ((uint16_t *)mask)[1])))
void ethernetReset(){
pinMode(rxSense, INPUT); //for stabilizing the ethernet board
pinMode(ethernetResetPin,INPUT);
while(1){
digitalWrite(ethernetResetPin, HIGH);
pinMode(ethernetResetPin, OUTPUT);
digitalWrite(ethernetResetPin, LOW); // ethernet board reset
delay(100);
digitalWrite(ethernetResetPin, HIGH);
delay(5000);
// now, after the reset, check the rx pin for constant on
if (ethernetOk())
return;
delay(100);
}
}
boolean ethernetOk(){
int cnt = 10, result;
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);
}
strcpy_P(Dbuf,PSTR("Ethernet setup result "));
Serial.print(Dbuf);
Serial.println(result,DEC);
if (result >=6) // experimentation gave me this number YMMV
return(true);
return(false);
}
boolean isLocal(Client incoming){
/*
this depends on code changes to the ethernet library
as detailed in http://arduino.cc/forum/index.php/topic,54378.0.html
*/
uint8_t clientIp[4];
uint8_t clientMac[6];
incoming.getRemoteIP_address(&clientIp[0]);
incoming.getRemoteMAC_address(&clientMac[0]);
/* This is for debugging
Serial.print("Incoming IP is: ");
for (int n = 0; n<4; n++){
Serial.print((int)clientIp[n]);
Serial.print(".");
}
Serial.print(" MAC is:");
for (int n = 0; n<6; n++){
Serial.print(clientMac[n] >> 4, HEX);
Serial.print(clientMac[n] & 0x0f, HEX);
Serial.print(".");
// my router has two mac addresses
//00.1F.B3.79.B3.69 seen inside
//00.1F.B3.79.B3.68 seen outside
//So, if you're coming from outside, the .69 address will
//be seen. Coming from inside you will see the actual
//mac address. That kinda sucks for using the mac address to
//identify machines, but the ip address can still be used.
}
Serial.println();
*/
return(maskcmp(clientIp,ip,subnet_mask));
}
boolean ipEqual(uint8_t * addr1, uint8_t * addr2){
return (memcmp(addr1,addr2,4)==0);
}
boolean tryGetIpAddress(char* hostName, byte* ipAddr){
Serial.print("Resolving ");
Serial.print(hostName);
Serial.println("...");
// Resolve the host name and block until a result has been obtained.
DNSError err = EthernetDNS.resolveHostName(hostName, ipAddr);
// Finally, we have a result.
if (DNSSuccess == err) {
// Serial.print("The IP address is ");
// Serial.print(ip_to_str(ipAddr));
} else if (DNSTimedOut == err) {
strcpy_P(Dbuf,PSTR("Timed out"));
Serial.println(Dbuf);
} else if (DNSNotFound == err) {
strcpy_P(Dbuf,PSTR("Does not exist"));
Serial.println(Dbuf);
} else {
strcpy_P(Dbuf,PSTR("Failed with error code "));
Serial.print(Dbuf);
Serial.println((int)err, DEC);
}
return(err == DNSSuccess);
}
void getIpAddress(char* name, byte* ipPtr){
int attempt = 0;
while (!tryGetIpAddress(name, ipPtr)){
if (attempt++ >=2)
resetMe();
if(!ethernetOk())
resetMe();
}
}
// format an IP address. (I stole this)
const char* ip_to_str(const uint8_t* ipAddr)
{
strcpy_P(Dbuf2,PSTR("%d.%d.%d.%d\0"));
sprintf(Dbuf, Dbuf2, ipAddr[0], ipAddr[1], ipAddr[2], ipAddr[3]);
return (Dbuf);
}
HandlePachube:
This handles the Pachube feed. It's not much code, but separating it like this keeps it out of the way when I make changes to something. There is actually a problem here. I followed my normal procedure of rebooting the controller if I can't connect in this code. Well, twice now Pachube has gone off line. This means the controller constantly reboots until Pachube comes back online. Sigh, I'll have to change this soon.
HandlePachube
void sendPachubeData(){
char dataBuf[100];
if(pachube.connected()) // already trying to get data, just leave
return;
digitalWrite(pachubeUpdateLed,LOW);
Alarm.timerOnce(1,pachubeUpdateLedOff);
if(round(realPower) == 0.0) // don't send invalid data
return;
tNow = now();
strcpy_P(Dbuf2,PSTR("Data at %02d:%02d:%02d: ")); // Debugging
sprintf(Dbuf,Dbuf2,hour(tNow),minute(tNow),second(tNow));
// Serial.print(Dbuf);
// construct the data buffer so we know how long it is
int poolMotorState = 0;
if(strcmp(poolData.motorState,"High") == 0)
poolMotorState = 2;
else if(strcmp(poolData.motorState,"Low") == 0)
poolMotorState = 1;
strcpy_P(Dbuf2, PSTR("%d,%d,0.%d,%d.%02d,%d.%02d,%d.%02d,%d,%d,%d,%d"));
sprintf(dataBuf,Dbuf2,
(int)round(realPower),
(int)round(apparentPower),
(int)(powerFactor*100),
(int)rmsCurrent,
(int)(((rmsCurrent+0.005) - (int)rmsCurrent) * 100),
(int)rmsVoltage,
(int)(((rmsVoltage+0.005) - (int)rmsVoltage) * 100),
(int)(frequency),
(int)(((frequency+0.005) - (int)frequency) * 100),
(ThermoData[0].currentTemp + ThermoData[1].currentTemp)/2,
(int)round(outsideTemp),
poolMotorState, poolMotorState == 0 ? 10 : poolData.poolTemp);
Serial.println(dataBuf);
//return;
strcpy_P(Dbuf,PSTR("Pachube Connecting..."));
Serial.print(Dbuf);
if(pachube.connect()){
strcpy_P(Dbuf,PSTR("OK"));
Serial.println(Dbuf);
tNow = now();
strcpy_P(Dbuf,PSTR("PUT /api/9511.csv HTTP/1.1\n"));
pachube.print(Dbuf);
strcpy_P(Dbuf,PSTR("Host: www.pachube.com\n"));
pachube.print(Dbuf);
strcpy_P(Dbuf,PSTR("X-PachubeApiKey: supersecretapikey\n"));
pachube.print(Dbuf);
strcpy_P(Dbuf,PSTR("Content-Length: "));
pachube.print(Dbuf);
pachube.println(strlen(dataBuf), DEC); // this has to be a println
strcpy_P(Dbuf,PSTR("Content-Type: text/csv\n"));
pachube.print(Dbuf);
strcpy_P(Dbuf,PSTR("Connection: close\n"));
pachube.println(Dbuf);
pachube.println(dataBuf);
pachube.stop();
pachubeUpdateTime = now();
}
else {
pachube.stop();
while(pachube.status() != 0){
delay(5);
}
strcpy_P(Dbuf,PSTR("failed"));
Serial.println(Dbuf);
}
}
HandleThermostat:
My two thermostats are web enabled and have a page on this blog describing their construction and code. This module talks to them and gathers their current status as well as passing on commands to enable their various functions. Since these devices are hardwired into my home ethernet, I don't use XBee communication on them. But, I do have to have retries and status checks on the network since even the hardwired ethernet has troubles. If I can't talk to the thermostats, the controller will reboot and try it again. When you look at the thermostat page, you'll see that it has the ability to reboot as well. Defensive programming carried to an extreme.
HandleThermostat
//
boolean Thermoconnected = false;
boolean tryGetThermostat(int idx,char* command, boolean respReq){
char* ptr;
char* data;
int cnt, len;
int waitcnt = 0;
long timer = 0;
while(1){
if (!Thermoconnected){
strcpy_P(Dbuf2,PSTR("%s Thermostat Connecting..."));
sprintf(Dbuf,Dbuf2, (idx==0) ? "North" : "South");
Serial.print(Dbuf);
if (Thermostats[idx].connect()) {
Thermoconnected = true;
waitcnt++;
strcpy_P(Dbuf,PSTR("OK"));
Serial.println(Dbuf);
strcpy_P(Dbuf2,PSTR("GET /%s HTTP/1.0"));
sprintf(Dbuf,Dbuf2,command);
// Serial.println(Dbuf); // for debug
Thermostats[idx].println(Dbuf);
timer = millis(); // to time the response, just in case
}
else{
strcpy_P(Dbuf,PSTR("failed"));
Serial.println(Dbuf);
waitcnt++;
timer = millis(); // if the connection failed I don't need to time it.
Thermostats[idx].flush();
Thermostats[idx].stop();
while (Thermostats[idx].status() != 0){
delay(5);
}
}
}
if (Thermostats[idx].available() > 0){
ptr = Dbuf;
while (Thermostats[idx].available()) {
*ptr++ = Thermostats[idx].read();
}
Thermostats[idx].flush(); //suck out any extra chars
Thermostats[idx].stop();
while (Thermostats[idx].status() != 0){
delay(5);
}
Thermoconnected = false;
*ptr='\0';
if(!respReq){ //response not required
return(true);
}
// Serial.println(Dbuf+44); //debug only
// finally got some data
ThermoData[idx].currentTemp = atof(strtok_r(Dbuf+44, ",", &ptr)); // first item is current temp
data = strtok_r(0, ",", &ptr); // first find the token
len = strcspn(data, ","); // then the length
memset(ThermoData[idx].state,0,sizeof(ThermoData[idx].state)); //make sure it's empty
strncpy(ThermoData[idx].state, data, len); // copy out what it's doing currently
ThermoData[idx].tempSetting = atof(strtok_r(ptr, ",", &ptr)); // temp setting
data = strtok_r(0, ",", &ptr);
len = strcspn(data, ",");
memset(ThermoData[idx].mode,0,sizeof(ThermoData[idx].mode));
strncpy(ThermoData[idx].mode, data, len); // Mode setting (cool, heat, off)
data = strtok_r(0, ",", &ptr);
len = strcspn(data, ",");
memset(ThermoData[idx].fan,0,sizeof(ThermoData[idx].fan));
strncpy(ThermoData[idx].fan, data, len); // Fan setting (Auto, On, Recirc - no off)
data = strtok_r(0, ",", &ptr);
len = strcspn(data, ",\n\r");
memset(ThermoData[idx].peak,0,sizeof(ThermoData[idx].peak));
strncpy(ThermoData[idx].peak, data, len); // In peak period ?
strcpy_P(Dbuf2,PSTR("Temp is %d, Heat pump is %s, \nTemp Setting is %d, Mode %s, Fan %s, %s Period"));
/* sprintf(Dbuf,Dbuf2, ThermoData[idx].currentTemp,
ThermoData[idx].state,
ThermoData[idx].tempSetting,
ThermoData[idx].mode,
ThermoData[idx].fan,
ThermoData[idx].peak);
Serial.println(Dbuf); */
// status LEDs are updated here
if (strcmp_P(ThermoData[idx].state,PSTR("Cooling")) == 0){
if (idx == 0){
digitalWrite(nRecircLed, HIGH);
digitalWrite(nCoolLed, LOW);
digitalWrite(nHeatLed, HIGH);
}
else {
digitalWrite(sRecircLed, HIGH);
digitalWrite(sCoolLed, LOW);
digitalWrite(sHeatLed, HIGH);
}
}
else if (strcmp_P(ThermoData[idx].state,PSTR("Heating")) == 0){
if (idx == 0){
digitalWrite(nRecircLed, HIGH);
digitalWrite(nCoolLed, HIGH);
digitalWrite(nHeatLed, LOW);
}
else {
digitalWrite(sRecircLed, HIGH);
digitalWrite(sCoolLed, HIGH);
digitalWrite(sHeatLed, LOW);
}
}
else if (strcmp_P(ThermoData[idx].state,PSTR("Recirc")) == 0){
if (idx == 0){
digitalWrite(nRecircLed, LOW);
digitalWrite(nCoolLed, HIGH);
digitalWrite(nHeatLed, HIGH);
}
else {
digitalWrite(sRecircLed, LOW);
digitalWrite(sCoolLed, HIGH);
digitalWrite(sHeatLed, HIGH);
}
}
else { // idle state is all LEDs off
if (idx == 0){
digitalWrite(nRecircLed, HIGH);
digitalWrite(nCoolLed, HIGH);
digitalWrite(nHeatLed, HIGH);
}
else {
digitalWrite(sRecircLed, HIGH);
digitalWrite(sCoolLed, HIGH);
digitalWrite(sHeatLed, HIGH);
}
}
return(true);
}
if (millis() - timer >5000){
strcpy_P(Dbuf,PSTR("Timeout waiting"));
Serial.println(Dbuf);
Thermostats[idx].flush();
Thermostats[idx].stop();
while (Thermostats[idx].status() != 0){
delay(5);
}
Thermoconnected = false;
}
if (!Thermoconnected){
strcpy_P(Dbuf2,PSTR("Thermo try # %d"));
sprintf(Dbuf,Dbuf2,waitcnt);
Serial.println(Dbuf);
delay (1000);
}
if (waitcnt > 9){
Thermostats[idx].flush();
Thermostats[idx].stop();
while (Thermostats[idx].status() != 0){
delay(5);
}
return(false);
}
}
}
void getThermostat(int idx, char* command, boolean respReq ){
if(tryGetThermostat(idx, command, respReq) == false){
strcpy_P(Dbuf,PSTR("Thermo comm failure, rebooting"));
Serial.println(Dbuf);
resetMe(); //call reset if you can't get the comm to work
}
return;
}
HandleTimers:
This code is what actually does all the asynchronous work. Things like the rotating display, data updates from various devices are handled by setting a timer, writing the routines and then forgetting about them. If I need to change the update frequency, I just change the timer. This code is called TimeAlarm and is available on the Arduino web site. I added a couple of routines to the library to handle some special items like counting the number of timers active. Using this I can make sure I don't run out during the asynchronous operations. I could easily have and XBee update, web update, web interrogation, all happen within a second. Currently I just display the maximum number of timers ever used, but I could also pause until something timed out and freed up one of the timers. It's unlikely I'll do that though, this is a Mega2560 and has pleanty of memory, at least for now.
HandleTimers
// All the timers and some of the supporting routines are here
// if the number of timers and alarms exceeds 6 update constant dtNBR_ALARMS
// to match
void initTimers(){
Alarm.timerRepeat(30, updateThermostats); //Thermostats every 30 secs
Alarm.timerRepeat(60, updatePachube); // update Pachube every minute
Alarm.timerRepeat(10*60,checkPublicIp); // every 10 min check public IP
Alarm.alarmRepeat(11,55,0,heatpumpFanAuto); // every day set the fans to auto to prevent recirc
Alarm.alarmRepeat(19,5,0,heatpumpFanRecirc); // fans back to recirc after peak period
Alarm.alarmRepeat(22,0,0,poolMotorOff); // Pool motor off at 10PM
Alarm.alarmOnce(dowTuesday,7,30,0,testRoutine);
Alarm.timerOnce(5,rotatingDisplay); // this kicks off the rotating display
Alarm.alarmRepeat(23,59,0,resetMe); // periodic reset to keep things cleaned up
// I use a lot of libraries and sometimes they have bugs
// as well as hang ups from various hardware devices
Alarm.delay(0);
}
void testRoutine(){
strcpy_P(Dbuf2,PSTR("Alarm or timer fired at %02d/%02d/%02d %02d:%02d:%02d"));
sprintf(Dbuf,Dbuf2,month(),day(),year(),hour(),minute(),second());
Serial.println(Dbuf);
}
void(* resetFunc) (void) = 0; //declare reset function @ address 0
void resetMe(){ // for periodic resets to be sure nothing clogs it up
resetFunc();
}
void updatePachube(){
/*
Since the data all comes in asyncronously these values
can be invalid when the device starts up. Check to be
sure there is something there before continuing
*/
if(pachube.connected()) // already trying to do this, just leave
return;
if(round(realPower) == 0.0) // don't send invalid data
return;
if(round(outsideTemp) == 0.0)
return;
if(ThermoData[0].currentTemp == 0)
return;
if(ThermoData[1].currentTemp == 0)
return;
sendPachubeData();
}
void updateThermostats(){
if(!Thermostats[0].connected())
getThermostat(0, "status", true);
if(!Thermostats[1].connected())
getThermostat(1, "status", true);
}
void heatpumpFanRecirc(){
if (Thermostats[0].connected() || Thermostats[1].connected()){
Alarm.timerOnce(30, heatpumpFanRecirc); //thermostat is busy check back in 30 secs
return;
}
//they're not busy so set the fan on both of them.
getThermostat(0,"fan=recirc", false); //Set the North one to auto
getThermostat(1,"fan=recirc", false); //and the South one too
}
void heatpumpFanAuto(){
if (Thermostats[0].connected() || Thermostats[1].connected()){
Alarm.timerOnce(30, heatpumpFanAuto); //thermostat is busy check back in 30 secs
return;
}
//they're not busy so set the fan on both of them.
getThermostat(0,"fan=auto", false); //Set the North one to auto
getThermostat(1,"fan=auto", false); //and the South one too
}
void checkPublicIp(){
//Check the externally visible IP and update if necessary.
strcpy_P(Dbuf,PSTR("Public IP address check"));
Serial.println(Dbuf);
getPublicIp();
if(!ipEqual(externalIp, eepromData.lastIp)){
publishIp();
memcpy(eepromData.lastIp,externalIp,4);
eepromAdjust();
strcpy_P(Dbuf,PSTR("Public IP address updated"));
Serial.println(Dbuf);
}
}
void timeRxLedOff(){
digitalWrite(timeRxLed,HIGH);
}
void tempRxLedOff(){
digitalWrite(tempRxLed,HIGH);
}
void powerRxLedOff(){
digitalWrite(powerRxLed,HIGH);
}
void etherRxLedOff(){
digitalWrite(etherRxLed,HIGH);
}
void extEtherRxLedOff(){
digitalWrite(extEtherRxLed,HIGH);
}
void statusRxLedOff(){
digitalWrite(statusRxLed,HIGH);
}
void pachubeUpdateLedOff(){
digitalWrite(pachubeUpdateLed,HIGH);
}
void poolRxLedOff(){
digitalWrite(poolRxLed,HIGH);
}
HandleXbee:
The XBee protocol and interaction is in this module. I only implemented the portions of the protocol that I'm interested in. No, I didn't use the XBee library. The library doesn't handle the multiple serial ports on the 2560, nor does it handle the SoftwareSerial library. At least it doesn't without some serious changes. I decided to just write my own handler so I understood what was going on for later when a bug turned up. Also, I can use any level of protocol without having to adhere to some rules someone else made up about how to use it.
This module is where the data samples from an outdoor thermometer are captured and calculated. I also receive broadcasts from my Power Monitor and Satellite Clock. These data are displayed on the rotating display. I also receive status from the pool and send commands to the pool controller from this code.
HandleXbee
uint8_t received;
#define WaitForFrameStart 1
#define LengthHighByte 2
#define LengthLowByte 3
#define PayloadCapture 4
#define CheckChecksum 5
int framestatus = WaitForFrameStart;
byte checksum;
char* payloadbuffer;
int datalength = 0;
int savedDataLength;
const char poolMotorCommand[] PROGMEM = {0x10,0x01,0x00,0x00,0x00,0x00,
0x00,0x00,0xff,0xff,0xff,0xfe,0x00,0x00,'p','o','o','l',',','m','\r'};
const char poolLightCommand[] PROGMEM = {0x10,0x01,0x00,0x00,0x00,0x00,
0x00,0x00,0xff,0xff,0xff,0xfe,0x00,0x00,'p','o','o','l',',','l','\r'};
const char poolFountainCommand[] PROGMEM = {0x10,0x01,0x00,0x00,0x00,0x00,
0x00,0x00,0xff,0xff,0xff,0xfe,0x00,0x00,'p','o','o','l',',','f','\r'};
const char poolWaterfallCommand[] PROGMEM = {0x10,0x01,0x00,0x00,0x00,0x00,
0x00,0x00,0xff,0xff,0xff,0xfe,0x00,0x00,'p','o','o','l',',','w','\r'};
const char textPrefix[] PROGMEM = {0x10,0x01,0x00,0x00,0x00,0x00,
0x00,0x00,0xff,0xff,0xff,0xfe,0x00,0x00};
void sendXbee_P(const char* command, int length){
byte checksum = 0;
int i;
Dbuf[0] = 0x7e; // begin frame
Dbuf[1] = length >>8;
Dbuf[2] = length & 0xff;
// copy command into buffer calculating checksum at the same time
for(i = 0; i<length; i++){
char c = pgm_read_byte(command + i); // can't use command[i]
Dbuf[i+3] = c;
checksum += c;
printByteData(Dbuf[i+3]);
Serial.print(" ");
}
Dbuf[i+3] = 0xff - checksum;
Serial.println();
for(i = 0; i<length + 4; i++){
Serial1.print(Dbuf[i]);
printByteData(Dbuf[i]);
Serial.print(" ");
}
Serial.println();
}
void sendStatusXbee(){
byte checksum = 0;
int i, length;
digitalWrite(statusRxLed,LOW);
Alarm.timerOnce(1,statusRxLedOff);
//power, time, outside temp, inside temp ---more to come someday
sprintf(Dbuf2,"Status,%d,%lu,%d,%d\r",(int)round(realPower), now(),
(int)round(outsideTemp),
(ThermoData[0].currentTemp + ThermoData[1].currentTemp)/2);
Dbuf[0] = 0x7e; // begin frame
//length is sum of prefix and text string
length = sizeof(textPrefix) + strlen(Dbuf2);
Dbuf[1] = length >>8;
Dbuf[2] = length & 0xff;
//copy in the prefix
memcpy(Dbuf + 3,textPrefix,sizeof(textPrefix));
for(i=0; i < sizeof(textPrefix); i++){
Dbuf[i + 3] = pgm_read_byte(textPrefix + i);
}
//then the text string
memcpy(Dbuf + 3 + sizeof(textPrefix),Dbuf2,strlen(Dbuf2));
for(i = 0; i<length; i++){
checksum += Dbuf[i+3];
}
Dbuf[3 + length] = 0xff - checksum;
for(i = 0; i<length + 4; i++){
Serial1.print(Dbuf[i]); // out to XBee
}
}
void checkXbee(){
if (Serial1.available()) {
received = (uint8_t)Serial1.read();
switch( framestatus ){
case WaitForFrameStart:
if(received != 0x7E)
break;
else {
framestatus = LengthHighByte;
// Serial.print("Frame start, ");
checksum = 0;
payloadbuffer = xbeeIn;
}
break;
case LengthHighByte:
datalength = received;
framestatus = LengthLowByte;
break;
case LengthLowByte:
datalength = (datalength * 256) + received;
savedDataLength = datalength;
// Serial.print("length ");
// Serial.print(datalength);
// Serial.print(", ");
framestatus = PayloadCapture;
break;
case PayloadCapture:
if(payloadbuffer >= xbeeIn + sizeof(xbeeIn)){
strcpy_P(Dbuf,PSTR("Reached end of buffer, ignore packet"));
Serial.println(Dbuf);
framestatus = WaitForFrameStart;
break;
}
*payloadbuffer++ = received;
// printByteData(received);
// Serial.print(" ");
datalength--;
checksum += received;
if (datalength == 0){
framestatus = CheckChecksum;
*payloadbuffer = '\0';
// Serial.print("received, ");
}
break;
case CheckChecksum:
checksum += received;
if (checksum == 0xFF){
// Serial.println("Checksum valid.");
xbeeFrameDecode(xbeeIn, savedDataLength);
}
else {
// Serial.println("Checksum invalid.");
}
framestatus = WaitForFrameStart;
break;
default:
break;
}
}
}
void xbeeFrameDecode(char* buffer, int length){
switch ( *buffer){
case 0x90: {
// Serial.println("Receive Data Frame");
buffer++; //skip over the frame type
length--;
// Serial.print("Source 64 bit address: ");
for(int i=0; i<8; i++){ //address the frame came from
// printByteData(*buffer);
// if (i == 3)
// Serial.print(" ");
buffer++;
length--;
}
// Serial.println();
// Serial.print("Source 16 bit network address: ");
for(int i=0; i<2; i++){ //16 bit network address the frame came from
// printByteData(*buffer);
buffer++;
length--;
}
// Serial.println();
// Serial.print("Receive options: "); // options byte
// printByteData(*buffer);
// Serial.println();
buffer++;
length--;
// char* tmp = buffer;
// while(length-- > 0){ //assuming the actual data is ascii
// Serial.print(*tmp++);
// }
processXbee(buffer);
break;
}
case 0x92: {
Serial.println("Data Sample");
buffer++; //skip over the frame type
length--;
// Serial.print("Source 64 bit address: ");
for(int i=0; i<8; i++){ //address the frame came from
// printByteData(*buffer);
// if (i == 3)
// Serial.print(" ");
buffer++;
length--;
}
// Serial.println();
// Serial.print("Source 16 bit network address: ");
for(int i=0; i<2; i++){ //16 bit network address the frame came from
// printByteData(*buffer);
buffer++;
length--;
}
// Serial.println();
// Serial.print("Receive options: "); // options byte
// printByteData(*buffer);
// Serial.println();
buffer++;
length--;
buffer++; // skip past the number of sample byte, always set to one
length--;
// Serial.print("Digital Channel Mask: ");
for(int i=0; i<2; i++){ //bitmask of digital pins reported
// printByteData(*buffer);
buffer++; // Digital Channel Mask is two bytes long
length--;
}
// Serial.println();
// Serial.print("Analog Channel Mask: ");
// printByteData(*buffer);
buffer++; // Analog Channel Mask is only one byte long
length--;
// Serial.println();
unsigned int n = 0;
// Serial.print("Analog data: ");
for(int i=0; i<2; i++){ // Analog Value
// printByteData(*buffer);
n = (n << 8) | ((uint8_t)*buffer);
buffer++; // Analog value is two bytes long
length--;
}
// Serial.println();
float f = ((((float)n * 1200.0) / 1024.0) / 10.0) * 2.0;
// Serial.print("Temp is: ");
// Serial.println(f);
outsideTemp = f;
digitalWrite(tempRxLed,LOW);
Alarm.timerOnce(1,tempRxLedOff);
break;
}
case 0x88:{
Serial.println("AT Command Response Frame");
break;
}
case 0x8B: {
Serial.println("Transmit Status Frame");
break;
}
default: {
Serial.println("Unimplemented Frame Type");
break;
}
}
}
void printByteData(uint8_t Byte){
Serial.print((uint8_t)Byte >> 4, HEX);
Serial.print((uint8_t)Byte & 0x0f, HEX);
}
void processXbee(char* buf){
time_t temptime = 0;
char* data;
// Serial.println(buf);
if((strstr_P(buf,PSTR("Time"))) != 0){
Serial.println("Time Data");
data = strtok_r(buf, ",", &buf); // skip to the actual t_time value
data = strtok_r(0, ",", &buf), // get the t_time variable
temptime = atol(data);
if(abs(temptime - now()) >= 5){ // only if it matters
Serial.println("Setting Time");
setTime(temptime);
}
digitalWrite(timeRxLed,LOW);
Alarm.timerOnce(1,timeRxLedOff);
}
else if (strstr_P(buf,PSTR("Power")) != 0){
Serial.println("Power Data");
data = strtok_r(buf, ",", &buf); // skip to actual power data
realPower = atof(strtok_r(0, ",", &buf));
apparentPower = atof(strtok_r(0, ",", &buf));
powerFactor = atof(strtok_r(0, ",", &buf));
rmsVoltage = atof(strtok_r(0, ",", &buf)); //reverse these after fixing monitor !!
rmsCurrent = atof(strtok_r(0, ",", &buf));
frequency = atof(strtok_r(0, ",", &buf));
/*
Serial.println(realPower,2);
Serial.println(apparentPower,2);
Serial.println(powerFactor,2);
Serial.println(rmsCurrent,2);
Serial.println(rmsVoltage,2);
Serial.println(frequency,2);
*/
digitalWrite(powerRxLed,LOW);
Alarm.timerOnce(1,powerRxLedOff);
}
else if (strstr_P(buf,PSTR("Pool")) != 0){
Serial.println("Pool Data");
digitalWrite(poolRxLed,LOW);
Alarm.timerOnce(1,poolRxLedOff);
// Serial.print(buf);
data = strtok_r(buf, ",", &buf); // skip to actual pool data
buf+=2; //skip the "M "
char *tb = buf;
strtok_r(buf, ",", &buf); //location of the next comma
memset(poolData.motorState,0,sizeof(poolData.motorState));
strncpy(poolData.motorState,tb,buf-tb);
// the pointer buf was left at the waterfall
// Serial.print("Motor ");
// Serial.println(poolData.motorState);
buf+=2;
tb = buf;
strtok_r(buf, ",", &buf); //location of the next comma
memset(poolData.waterfallState,0,sizeof(poolData.waterfallState));
strncpy(poolData.waterfallState,tb,buf-tb);
// the pointer buf was left at the light
// Serial.print("Waterfall ");
// Serial.println(poolData.waterfallState);
buf+=2;
tb = buf;
strtok_r(buf, ",", &buf); //location of the next comma
memset(poolData.lightState,0,sizeof(poolData.lightState));
strncpy(poolData.lightState,tb,buf-tb);
// the pointer buf was left at the fountain
// Serial.print("Light ");
// Serial.println(poolData.lightState);
buf+=2;
tb = buf;
strtok_r(buf, ",", &buf); //location of the next comma
memset(poolData.fountainState,0,sizeof(poolData.fountainState));
strncpy(poolData.fountainState,tb,buf-tb);
// the pointer buf was left at the solar valve
// Serial.print("Fountain ");
// Serial.println(poolData.fountainState);
buf+=2;
tb = buf;
strtok_r(buf, ",", &buf); //location of the next comma
memset(poolData.solarState,0,sizeof(poolData.solarState));
strncpy(poolData.solarState,tb,buf-tb);
// the pointer buf was left at the water temp
// Serial.print("Solar ");
// Serial.println(poolData.solarState);
buf+=3; //OK, now I'm pointing at the pool temperature
// however, if the motor is off, the temp is invalid; no water past the sensor.
int tmp = poolData.poolTemp;
poolData.poolTemp = atoi(strtok_r(0, ",", &buf)); // so get the temperature and
// position the pointer correctly for the air temp below
// but set it to zero if the motor is off
if(strcmp_P(poolData.motorState, PSTR("Off")) == 0)
poolData.poolTemp = 10; // Lowest temp I'm allowing to display
// if the motor is on and the temp is zero (this happens sometimes ??)
// keep the last temperature reading
// That's why I saved the temperature in tmp above
else if (poolData.poolTemp < 10) // only one digit came back
poolData.poolTemp = tmp; //this way it doesn't screw up graphs
// Serial.print("Pool Temp ");
// Serial.println(poolData.poolTemp);
buf+=3;
poolData.airTemp = atoi(strtok_r(0, ",", &buf));
// Serial.print("Air Temp ");
// Serial.println(poolData.airTemp);
}
else if (strstr_P(buf,PSTR("?")) != 0){
Serial.println("Status Request");
sendStatusXbee();
}
}
// Pool Command send routines to make it easier to set timers and such
void poolMotor(){
sendXbee_P(poolMotorCommand, sizeof(poolMotorCommand));
}
void poolMotorOff(){
if (strcmp_P(poolData.motorState, PSTR("Off")) != 0) {
poolMotor();
Alarm.timerOnce(5,poolMotorOff); // check back in 5 seconds to see if it made it
}
}
void poolMotorHigh(){
if (strcmp_P(poolData.motorState, PSTR("High")) != 0) {
poolMotor();
Alarm.timerOnce(5,poolMotorHigh); // check back in 5 seconds to see if it made it
}
}
void poolMotorLow(){
if (strcmp_P(poolData.motorState, PSTR("Low")) != 0) {
poolMotor();
Alarm.timerOnce(5,poolMotorLow); // check back in 5 seconds to see if it made it
}
}
void poolWaterfall(){
sendXbee_P(poolWaterfallCommand, sizeof(poolWaterfallCommand));
}
void poolWaterfallOff(){
if (strcmp_P(poolData.waterfallState, PSTR("Off")) != 0) {
poolWaterfall();
Alarm.timerOnce(5,poolWaterfallOff); // check back in 5 seconds to see if it made it
}
}
void poolWaterfallOn(){
if (strcmp_P(poolData.waterfallState, PSTR("On")) != 0) {
poolWaterfall();
Alarm.timerOnce(5,poolWaterfallOn); // check back in 5 seconds to see if it made it
}
}
void poolLight(){
sendXbee_P(poolLightCommand, sizeof(poolLightCommand));
}
void poolLightOff(){
if (strcmp_P(poolData.lightState, PSTR("Off")) != 0) {
poolLight();
Alarm.timerOnce(5,poolLightOff); // check back in 5 seconds to see if it made it
}
}
void poolLightOn(){
if (strcmp_P(poolData.lightState, PSTR("On")) != 0) {
poolLight();
Alarm.timerOnce(5,poolLightOn); // check back in 5 seconds to see if it made it
}
}
void poolFountain(){
sendXbee_P(poolFountainCommand, sizeof(poolFountainCommand));
}
void poolFountainOff(){
if (strcmp_P(poolData.fountainState, PSTR("Off")) != 0) {
poolFountain();
Alarm.timerOnce(5,poolFountainOff); // check back in 5 seconds to see if it made it
}
}
void poolFountainOn(){
if (strcmp_P(poolData.fountainState, PSTR("On")) != 0) {
poolFountain();
Alarm.timerOnce(5,poolFountainOn); // check back in 5 seconds to see if it made it
}
}
HandleX10:
I'm undecided exactly how to handle this portion of the code. The X10 modules don't provide status and you just have to trust them to do what you tell them to do. However, X10 is famous for problems with this. You can send an ON command and nothing happens because of some noise on the power line, flaky hardware or just about anything else. I suspect I'll just send the commands multiple times and hope for the best, but I'm just starting on this piece so check back later.
HandleX10 (not implemented yet)
Out of curiosity, how large is this once compiled?
ReplyDeleteBTW, I'm taking different approaches from you on many things, but also am trying to accomplish many of the same things. I'm unabashedly stealing from your work all the while :D. Thanks for putting all of this information out there! you have saved me time and money more than once already, and I'm just getting started.
Right now, it is around 47K. I'm not making every effort to save memory (ROM) though since I'm using a 2560. Every time I think about it, I pull up the source and add something, so it gets bigger over time. For example, I'm going to add monitoring for my septic tank in the next couple of weeks, and I want to add an alarm to tell me if any of the sensors has encountered an odd condition (like a full septic tank).
ReplyDeleteHi,
ReplyDeletea very interesting project, I started to do a similar project (www.saulevire.lt so far only in Lithuanian). What version of the Arduino IDE are you using? I tried to compile superthermostat program, but failed of errors ...
I'm using IDE version 21. I use a number of libraries that haven't been moved up yet. Sort of got stuck on that version. I'm eventually going to move up to the latest when they settle it down for a few months.
ReplyDeleteMaybe you can programs and libraries to publicize in website https://github.com/.
ReplyDeleteIt would be interesting.