How I Control The Pool

REQUISITE DISCLAIMER:  THE ACTIONS I’VE TAKEN ON MY PENTAIR EASYTOUCH-BASED POOL CONTROLLER AND PERIPHERALS WERE UNDERTAKEN WITH THE CLEAR AND LUCID UNDERSTANDING THAT I COULD CAUSE SERIOUS DAMAGE SHOULD SOMETHING MALFUNCTION.  THIS IS SOMETHING I TAKE VERY SERIOUSLY.  YOU SHOULD BE MORE CAUTIOUS THAN ME BECAUSE IF YOU ATTEMPT ANY OF THE PROCEDURES I DESCRIBE HERE AND BREAK YOUR SYSTEM I WILL NOT BE HELD RESPONSIBLE.

There, I said it.  With that out of the way, if you have a Pentair EasyTouch control panel that runs your Intelliflo pool pump and possibly an Intellichlor salt chlorinator, and maybe even a wired or wireless remote, then you’re in luck.  All of these devices connect to a common RS-485 bus and for cheap, waaay cheap, like under $30 cheap, you can read the state of the pool devices and control them, too.

How, You Ask?

Pentair didn’t make it easy.  I called their support line and asked how one would go about getting their API.  The guy was like “You want what?”.  I repeated my question.  I knew this was doomed.  Again he seemed puzzled but took my number and said he’d “look into it”.  I think after three weeks without a call back we know my phone isn’t likely to ring.  So it’s off to Google.  No shortage of ambitious people all looking for the API or others that started decoding it themselves.  So that’s what I did.  I looked at what they figured out, hooked up my $5 RS-485 shield to my trusty Arduino, ran a two-wire cross-connect wire from the RS-485 port on the shield to the cleverly named “COM PORT” on the EasyTouch panel, hacked up some Arduino code and I was reading the packets in no time.

Here’s the Arduino code I used to see what was on the wire.

int onBoardLed = 13;
int DTR = 2; //set low for Rx, high for Tx
int inBytes;
void setup() {
pinMode(onBoardLed, OUTPUT);
pinMode(DTR, OUTPUT);
Serial.begin(115200);
Serial1.begin(9600); //Begin Serial to talk to the EasyTouch Panel
}
void loop() {
digitalWrite(DTR, LOW); //Enable Receiving Data
while (Serial1.available()) {
inBytes = Serial1.read();
if (inBytes < 0x10) {
Serial.write('0'); // if you don't do this, values 0 to 15 only print 1 character instead of 2
}
Serial.print(inBytes, HEX); // all hex here should print as 2 characters 00 to FF
Serial.print(" ");
}
}

And here’s what pumped out the serial monitor in the Arduino IDE.  No, it didn’t line wrap either.  It was one long line of WTF?

FF 00 FF A5 00 60 10 07 00 01 1C FF 00 FF A5 00 10 60 07 0F 0A 00 00 00 E3 05 78 00 00 00 00 00 01 09 09 02 A8 FF FF FF FF FF
FF FF FF 00 FF A5 07 10 20 D1 01 01 01 AF 00 FF FF FF FF FF FF FF FF 00 FF A5 07 0F 10 11 07 01 01 05 00 09 00 7F 01 72 FF FF
FF FF FF FF FF FF 00 FF A5 07 10 20 D1 01 02 01 B0 00 FF FF FF FF FF FF FF FF 00 FF A5 07 0F 10 11 07 02 06 09 00 0E 37 7F 01
B8 FF FF FF FF FF FF FF FF 00 FF A5 07 10 20 D1 01 03 01 B1 00 FF FF FF FF FF FF FF FF 00 FF A5 07 0F 10 11 07 03 06 12 05 14
00 7F 01 96 FF FF FF FF FF FF FF FF 00 FF A5 07 10 20 D1 01 04 01 B2 00 FF FF FF FF FF FF FF FF 00 FF A5 07 0F 10 11 07 04 03
19 00 03 00 7F 01 85 FF FF FF FF FF FF FF FF 00 FF A5 07 10 20 D1 01 05 01 B3 00 FF FF FF FF FF FF FF FF 00 FF A5 07 0F 10 11
07 05 00 00 00 00 00 00 00 E8 FF FF FF FF FF FF FF FF 00 FF A5 07 10 20 D1 01 06 01 B4 00 FF FF FF FF FF FF FF FF 00 FF A5 07
0F 10 11 07 06 00 00 00 00 00 00 00 E9 FF FF FF FF FF FF FF FF 00 FF A5 07 0F 10 02 1D 09 09 20 00 00 00 00 00 00 20 00 00 00
04 55 55 00 00 5B 00 00 00 00 00 00 81 83 03 0D 03 59 FF FF FF FF FF FF FF FF 00 FF A5 07 10 20 D1 01 07 01 B5 00 FF FF FF FF
FF FF FF FF 00 FF A5 07 0F 10 11 07 07 00 00 00 00 00 00 00 EA FF FF FF FF FF FF FF FF 00 FF A5 07 10 20 D1 01 08 01 B6 00 FF
FF FF FF FF FF FF FF 00 FF A5 07 0F 10 11 07 08 02 19 1E 03 00 7F 01 A6 FF FF FF FF FF FF FF FF 00 FF A5 07 10 20 D1 01 09 01
B7 00 FF FF FF FF FF FF FF FF 00 FF A5 07 0F 10 11 07 09 00 00 00 00 00 00 00 EC FF FF FF FF FF FF FF FF 00 FF A5 07 10 20 D1
01 0A 01 B8 00 FF FF FF FF FF FF FF FF 00 FF A5 07 0F 10 11 07 0A 00 00 00 00 00 00 00 ED FF FF FF FF FF FF FF FF 00 FF A5 07
10 20 D1 01 0B 01 B9 00 FF FF FF FF FF FF FF FF 00 FF A5 07 0F 10 11 07 0B 00 00 00 00 00 00 00 EE FF FF FF FF FF FF FF FF 00
FF A5 07 10 20 D1 01 0C 01 BA 00 FF FF FF FF FF FF FF FF 00 FF A5 07 0F 10 11 07 0C 00 00 00 00 00 00 00 EF FF FF FF FF FF FF
FF FF 00 FF A5 07 0F 10 02 1D 09 09 20 00 00 00 00 00 00 20 00 00 00 04 55 55 00 00 5B 00 00 00 00 00 00 81 83 03 0D 03 59 FF
FF FF FF FF FF FF FF 00 FF A5 07 0F 10 02 1D 09 09 20 00 00 00 00 00 00 20 00 00 00 04 55 55 00 00 5B 00 00 00 00 00 00 81 83
03 0D 03 59 FF 00 FF A5 00 60 10 04 01 FF 02 19 FF 00 FF A5 00 10 60 04 01 FF 02 19 FF 00 FF A5 00 60 10 06 01 0A 01 26 FF 00
FF A5 00 10 60 06 01 0A 01 26 FF 00 FF A5 00 60 10 01 04 02 C4 05 78 02 5D FF 00 FF A5 00 10 60 01 02 05 78 01 95 FF FF FF FF
FF FF FF FF 00 FF A5 07 0F 10 02 1D 09 09 20 00 00 00 00 00 00 20 00 00 00 04 55 55 00 00 5B 00 00 00 00 00 00 81 83 03 0D 03
59 FF FF FF FF FF FF FF FF 00 FF A5 07 0F 10 02 1D 09 09 20 00 00 00 00 00 00 20 00 00 00 04 55 55 00 00 5B 00 00 00 00 00 00
81 83 03 0D 03 59 FF FF FF FF FF FF FF FF 00 FF A5 07 0F 10 02 1D 09 09 20 00 00 00 00 00 00 20 00 00 00 04 55 55 00 00 5B 00
00 00 00 00 00 81 83 03 0D 03 59 FF FF FF FF FF FF FF FF 00 FF A5 07 0F 10 02 1D 09 09 20 00 00 00 00 00 00 20 00 00 00 04 55
55 00 00 5B 00 00 00 00 00 00 81 83 03 0D 03 59 FF 00 FF A5 00 60 10 07 00 01 1C

Oh Crap.  Now What?

Well, some people had already made headway into decoding the protocol so I tried to find similar messages in that mess.  It took some time but there they were.  They started appearing one after another.  Obviously they weren’t identical to their findings but the patterns were showing up.  Here is what happens when you start picking out the messages.  There are patterns in there, but it’s probably better left to my Pentair EasyTouch and Intellichlor protocol decoding page you can read here.  For now, all you care about is coding for these patterns.  You can do this…

FF FF FF FF FF FF FF FF 00 FF A5 07 0F 10 02 1D 09 08 20 00 00 00 00 00 00 20 00 00 00 04 56 56 00 00 5B 00 00 00 00 00 00 81 83 03 0D 03 5A
FF 00 FF A5 00 60 10 07 00 01 1C
FF 00 FF A5 00 10 60 07 0F 0A 00 00 00 E3 05 78 00 00 00 00 00 01 09 08 02 A7
FF FF FF FF FF FF FF FF 00 FF A5 07 0F 10 02 1D 09 08 20 00 00 00 00 00 00 20 00 00 00 04 54 54 00 00 5B 00 00 00 00 00 00 81 83 03 0D 03 56
FF FF FF FF FF FF FF FF 00 FF A5 07 0F 10 02 1D 09 08 20 00 00 00 00 00 00 20 00 00 00 04 54 54 00 00 5B 00 00 00 00 00 00 81 83 03 0D 03 56
FF FF FF FF FF FF FF FF 00 FF A5 07 0F 10 02 1D 09 08 20 00 00 00 00 00 00 20 00 00 00 04 54 54 00 00 5B 00 00 00 00 00 00 81 83 03 0D 03 56
FF 00 FF A5 00 60 10 04 01 FF 02 19
FF 00 FF A5 00 10 60 04 01 FF 02 19
FF 00 FF A5 00 60 10 06 01 0A 01 26
FF 00 FF A5 00 10 60 06 01 0A 01 26
FF 00 FF A5 00 60 10 01 04 02 C4 05 78 02 5D
FF 00 FF A5 00 10 60 01 02 05 78 01 95
FF FF FF FF FF FF FF FF 00 FF A5 07 0F 10 02 1D 09 08 20 00 00 00 00 00 00 20 00 00 00 04 54 54 00 00 5B 00 00 00 00 00 00 81 83 03 0D 03 56
FF FF FF FF FF FF FF FF 00 FF A5 07 0F 10 02 1D 09 08 20 00 00 00 00 00 00 20 00 00 00 04 54 54 00 00 5B 00 00 00 00 00 00 81 83 03 0D 03 56
FF FF FF FF FF FF FF FF 00 FF A5 07 0F 10 02 1D 09 08 20 00 00 00 00 00 00 20 00 00 00 04 54 54 00 00 5B 00 00 00 00 00 00 81 83 03 0D 03 56
FF FF FF FF FF FF FF FF 00 FF A5 07 0F 10 02 1D 09 08 20 00 00 00 00 00 00 20 00 00 00 04 54 54 00 00 5B 00 00 00 00 00 00 81 83 03 0D 03 56
FF FF FF FF FF FF FF FF 00 FF A5 07 0F 10 02 1D 09 08 20 00 00 00 00 00 00 20 00 00 00 04 54 54 00 00 5B 00 00 00 00 00 00 81 83 03 0D 03 56
FF FF FF FF FF FF FF FF 00 FF A5 07 0F 10 02 1D 09 08 20 00 00 00 00 00 00 20 00 00 00 04 54 54 00 00 5B 00 00 00 00 00 00 81 83 03 0D 03 56

What You Are Looking For

That’s actually not that hard.  As the protocol write-up explains, every message by the EasyTouch panel, Intelliflo pump and wired or wireless remote has the same format.  You’re looking for the sequence FF 00 FF A5.  The preceding FF FF FF FF FF FF FF are just noise.  There probably is a reason it’s there but we don’t care about them.  When you find the FF 00 FF A5 you’ve found the four byte preamble of the new packet.  After that are four bytes that include the source and destination.  OK, reasonably important, at least we know who sent the message and if it was targeting a certain device on the bus or if it’s a broadcast (lot’s of these BTW).  Using line three above, device 60 (the pump) is sending (really replying) to device 10 (the EasyTouch panel) with it’s statistics.  Byte nine is key.  It’s the length in bytes of the data payload, 0F, or 15 in this packet.  So now you know there are 17 more bytes to collect:  15 data bytes plus the two byte check sum.

And what’s in that data payload?  Well, the pump was kind enough to tell the EasyTouch panel it’s 9:08AM and it’s using 227 watts while spinning at 1400 RPM.  Good to know!  (Probably a bad example as the time is also in HEX, and unfortunately I selected a frame that works the same in decimal.  Just know that all bytes are in HEX so the hours and minutes can take the form 10 27, which would really be 16:39 or 4:39PM.  HEX can be deceiving until you start thinking that way.)

And how do you calculate the checksum?  Sum up the bytes A5 through the final data byte.  It should equal the check sum.  More on this here.

Are There Caveats?

Of course there are.  This Frankensystem wouldn’t be complete without another anomaly.  As if variable-length packets weren’t enough, this “protocol” has two different languages on the same bus, AT THE SAME TIME.  WTF?  Thanks, Pentair!

The IntelliChlor IC-40 (in my case) transmits in a different format.  Perhaps Pentair didn’t actually develop this unit themselves and it’s a rebranded unit.  I’ve seen the Goldline protocol, and it follows the IntelliChlor format, or is it the other way around?  This is also discussed on the protocol decoding page but suffice it to say, you also need to code for packets that begin with 10 02 and terminate with 10 03.  And of course it doesn’t stop there.  Nope.  The Pentair protocol can have the 10 02 pattern within it’s own non-IntelliChlor packets.  Take a look at most of the lines in the previous formatted output.  That’s right!  A whole bunch of non-IntelliChlor packets containing the 10 02 sequence.  So how do you accept those as non-IntelliChlor begin sequences and still catch the legitimate IntelliChlor packets that begin with 10 02?  You guessed it.  You code for it.

My Pain, Your Gain

Well, I’ve coded, if nothing else, equally as Frankenstyle as Pentair did.  It happens.  Clean it up yourself if you’re so inclined.

So below is my Arduino code.  I’m using a Mega 2560, Wiznet Ethernet and FunDuino RS-485 I/O Expander shields.  The latter is probably just a clone of this (now discontinued) DFRobot shield.  FWIW I paid $5 from eBay for mine.

Keep in mind what my goals were.  I wanted to read the water and air temperatures, pool mode (in-floor cleaner, low speed circulation, light state, waterfall state), pump RPM, pump watts, salinity reading, chlorinator setpoint, chlorinator activity, set system state with HTTP GET calls, update Xively and update my Vera home automation unit.  Pretty ambitious!  And I almost got all of these.  All except for one: salinity detected by the IntelliChlor.  I just can’t find the data in the RS-485 stream.  Even when I query the salinity in the EasyTouch menu DIAGNOSTICS >> CHLORINATOR, the display shows it but, the remote outputs a command on the RS-485 bus but the reply is absent of anything I can correlate with the reading.  If you figured it out, please share it with me!

So here you go!  Cajones grandes required.

Auduino Code v2.6.1 29-JUN-2016

// Source structure graciously provided by draythomp 
// (http://www.desert-home.com/p/swimming-pool.html) 
// and sufficiently bastardized for Pentair protocol
#include <HttpClient.h>
#include <SPI.h>
#include <Dhcp.h>
#include <Dns.h>
#include <Ethernet.h>
#include <EthernetClient.h>
#include <EthernetServer.h>s
#include <EthernetUdp.h>
#include <Time.h>
#include <CountingStream.h>
#include <avr/wdt.h>
//RS-485 write stuff
//   PACKET FORMAT        <------------------NOISE---------------->  <------PREAMBLE------>  Sub   Dest  Src   CFI   Len   Dat1  Dat2  ChkH  ChkL     //Checksum is sum of bytes A5 thru Dat2.
byte cleanerOn[] =        {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xA5, 0x07, 0x10, 0x20, 0x86, 0x02, 0x01, 0x01, 0x01, 0x66 };
byte cleanerOff[] =       {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xA5, 0x07, 0x10, 0x20, 0x86, 0x02, 0x01, 0x00, 0x01, 0x65 };
byte poolLightOn[] =      {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xA5, 0x07, 0x10, 0x20, 0x86, 0x02, 0x02, 0x01, 0x01, 0x67 };
byte poolLightOff[] =     {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xA5, 0x07, 0x10, 0x20, 0x86, 0x02, 0x02, 0x00, 0x01, 0x66 };
byte waterFallOn[] =      {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xA5, 0x07, 0x10, 0x20, 0x86, 0x02, 0x03, 0x01, 0x01, 0x68 };
byte waterFallOff[] =     {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xA5, 0x07, 0x10, 0x20, 0x86, 0x02, 0x03, 0x00, 0x01, 0x67 };
byte poolOn[] =           {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xA5, 0x07, 0x10, 0x20, 0x86, 0x02, 0x06, 0x01, 0x01, 0x6B };
byte poolOff[] =          {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xA5, 0x07, 0x10, 0x20, 0x86, 0x02, 0x06, 0x00, 0x01, 0x6A };
uint8_t saltPctQuery[] =  {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xA5, 0x07, 0x10, 0x20, 0xD9, 0x01, 0x00, 0x01, 0xB6 };                    //triggers salt percent setting
uint8_t chlorStep1[] =    {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xA5, 0x07, 0x10, 0x20, 0x99, 0x04, 0x01, 0x02, 0x00, 0x00, 0x01, 0x7C };  // 2% chlorination
uint8_t chlorStep2[] =    {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xA5, 0x07, 0x10, 0x20, 0x99, 0x04, 0x01, 0x05, 0x00, 0x00, 0x01, 0x7F };  // 5% chlorination
uint8_t chlorStep3[] =    {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xA5, 0x07, 0x10, 0x20, 0x99, 0x04, 0x01, 0x0A, 0x00, 0x00, 0x01, 0x84 };  //10% chlorination
uint8_t chlorStep4[] =    {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xA5, 0x07, 0x10, 0x20, 0x99, 0x04, 0x01, 0x0F, 0x00, 0x00, 0x01, 0x89 };  //15% chlorination
uint8_t chlorStep5[] =    {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xA5, 0x07, 0x10, 0x20, 0x99, 0x04, 0x01, 0x14, 0x00, 0x00, 0x01, 0x8E };  //20% chlorination
uint8_t chlorStep6[] =    {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xA5, 0x07, 0x10, 0x20, 0x99, 0x04, 0x01, 0x19, 0x00, 0x00, 0x01, 0x93 };  //25% chlorination
uint8_t chlorStep7[] =    {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xA5, 0x07, 0x10, 0x20, 0x99, 0x04, 0x01, 0x21, 0x00, 0x00, 0x01, 0x9B };  //33% chlorination
uint8_t chlorStep8[] =    {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xA5, 0x07, 0x10, 0x20, 0x99, 0x04, 0x01, 0x32, 0x00, 0x00, 0x01, 0xAC };  //50% chlorination
#define REQ_BUF_SZ 60
byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };
EthernetServer server(2560);                    // create a server at port 2560
char HTTP_req[REQ_BUF_SZ] = {0};                // buffered HTTP request stored as null terminated string
char req_index = 0;                             // index into HTTP_req buffer
EthernetClient php;
char phpServer[] = {"192.168.1.000"}; //RasPi Python Script
//NTP
IPAddress timeServer(132, 163, 4, 102);         // time-b.timefreq.bldrdoc.gov
EthernetUDP Udp;
unsigned int localPort = 8888;                  // local port to listen for UDP packets
int timeZone = -7;                              // set this shit later
//Vera HA info
char veraServer[] = {"192.168.1.000"};          //Vera HA IP
EthernetClient vera;
// RS-485 read stuff
uint8_t buffer[256];
uint8_t* bPointer;
uint8_t bufferOfBytes[256];
uint8_t* bPointerOfBytes;
#define DTR 2
//#define onBoardLed 13
#define header1 1
#define header3 2
#define header4 3
#define bufferData 4
#define calcCheckSum 5
#define saltHead2 6
#define saltTerm 7
#define bufferSaltData 8
String s1 = " This is version 2.6.1 compiled 29-JUN-2016\n Developed by Jason Young";
int goToCase = header1;
int byteNum = 0;
int remainingBytes = 0;
int bytesOfDataToGet = 0;
int chkSumBits = 0;
boolean veraUpdatePending = false;
byte veraVarDevId = 0;
byte veraVarTargetVal = 0;
byte veraVarIDs[] = { 0x1A, 0x22, 0x23, 0x24 };    //list of Vera VSwitch ID's (in HEX)
byte veraVarVals[4];                               //count of Vera Values to update
byte veraVarLight = 26;
byte veraVarWaterfall = 34;
byte veraVarCirculation = 35;
byte veraVarCleaner = 36;
volatile byte lightState = 0;
byte waterfallState = 0;
byte circulationState = 0;
byte cleanerState = 0;
byte veraMstringChlor = 0x1;
byte veraMstringChlorOn = 0x2;
byte veraMstringWaterTemp = 0x3;
byte veraMstringPumpRPM = 0x4;
byte veraMstringPumpWatts = 0x5;
volatile byte poolTemp = 0; 
byte oldPoolTemp = 0; 
volatile byte airTemp = 0; 
volatile byte poolMode = 0x0;
byte oldPoolMode = 0x0;
volatile byte pumpMode = 0x0;
volatile int pumpWatts = 0;
int oldPumpWatts = 0;
volatile int pumpRPM = 0;
int oldPumpRPM = 0;
volatile byte saltPct = 0; 
byte oldSaltPct = 0; 
volatile byte saltSetpoint = 0; 
boolean saltSetpointTrigger = false;
byte oldSaltSetpoint = 0;
byte saltStateLow = 0;
byte saltStateHigh = 0;
volatile byte saltStateResult = 0xF0; 
int saltBytes1 = 0;
int saltBytes2 = 0;
int sumOfBytesInChkSum = 0;
int chkSumValue = 0;
boolean salt = false;
boolean xivelyTrigger = false;
boolean veraSuccess = false;
//byte veraUpdateCount = 0;
unsigned long currentMillis = 0;
long days = 0;
long hours = 0;
long mins = 0;
long secs = 0;
unsigned long rawUptime = 0;
String strUptime;
boolean debug = true;
byte panelHour = 0;
byte panelMinute = 0;
byte ntpHours = 0;
byte ntpMinutes = 0;
unsigned long phpStart = 0;
unsigned long phpStop = 0;
unsigned long saltOutputToggle = 0;
boolean finalXivelyPost = false;
unsigned long xivelyMillis = 0;
int salinityNow = 0;
int freeRam () {
extern int __heap_start, *__brkval;
int v;
return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval);
};
void setup() {
pinMode(13, OUTPUT);
digitalWrite(13,HIGH);
Serial.println(s1);
Serial.begin(115200);
Serial.println(F("Initializing.."));
Serial1.begin(9600);
pinMode(DTR, INPUT);      
digitalWrite(DTR, HIGH);  
pinMode(DTR, OUTPUT);
digitalWrite(DTR, LOW);                          // to receive from rs485
pinMode(10,OUTPUT);
digitalWrite(10,LOW);
pinMode(53, OUTPUT); //Ethernet
pinMode(4,OUTPUT);
digitalWrite(4,HIGH);
Serial.print(F("SPI Bus initialized"));
Serial.println();
Serial.println();
Serial.print(F("Waiting for DHCP..."));
Serial.println();
byte i = 0; 
int DHCP = 0;
DHCP = Ethernet.begin(mac);
while( DHCP == 0 && i < 30) {                   //Try to get dhcp settings 30 times before giving up
delay(1000);
DHCP = Ethernet.begin(mac);
i++;
}
if(!DHCP) {
Serial.println(F("DHCP FAILED"));
for(;;);                                      //Infinite loop because DHCP Failed
}
Serial.println(F("DHCP Success"));
Serial.println();
Serial.println();
Serial.print(F("Ethernet initialized"));
Serial.println();
Serial.print(F("IP address assigned by DHCP is "));
Serial.println(Ethernet.localIP());
Serial.println();
Udp.begin(localPort);
Serial.println(F("Waiting for NTP sync"));
setSyncProvider(getNtpTime);
Serial.println();
server.begin();
Serial.print(F("HTTP Server started on port 2560 "));
Serial.println();
Serial.print(F("I'M ALIVE AT http://"));
Serial.print(Ethernet.localIP());
Serial.println(F(":2560"));
Serial.println();
Serial.println();
headerNotes();
memset(buffer, 0, sizeof(buffer));
bPointer = buffer;
memset(bufferOfBytes, 0, sizeof(bufferOfBytes));
bPointerOfBytes = bufferOfBytes;
Serial.println(F("Let's do this"));
Serial.println();
Serial.print(F( "\n Free RAM = " ));
Serial.println( freeRam());
Serial.println();
digitalWrite(13,LOW);
}
time_t prevDisplay = 0; // when the digital clock was displayed NTP STUFF, HERE FOR SOME REASON
void(* resetFunc) (void) = 0;                                       //declare reset function at address 0
void veraSendVswitch(byte veraVswitchID, byte veraVswitchVal) {
if (vera.connect(veraServer, 3480)) {
delay(250);
vera.print(F("GET /data_request?id=lu_action&output_format=json&serviceId=urn:upnp-org:serviceId:VSwitch1&DeviceNum="));
vera.print(veraVswitchID);
vera.print(F("&action=SetTarget&newTargetValue="));
vera.print(veraVswitchVal);
vera.println(F(" HTTP/1.1"));
vera.println(F("Host: www.sdyoung.com"));
vera.println(); 
delay(100); 
vera.println();
//    veraSuccess = true;
Serial.println(F("\n Updated datagram sent to Vera multistring app"));  
}
else {
//    veraSuccess = false;
//    Serial.println(F("\n ### FAILED POSTING VIRTUAL SWITCH UPDATE TO VERA.SDYOUNG.COM ###\r\n"));
resetFunc();
}
vera.stop();
while(vera.status() != 0) {
delay(5);
}
}
void veraSendMultiString(byte veraMstringID, int veraMstringVal) {
if (vera.connect(veraServer, 3480)) {
delay(250);
vera.print(F("GET /data_request?id=variableset&DeviceNum=33&serviceId=urn:upnp-org:serviceId:VContainer1&Variable=Variable"));
vera.print(veraMstringID);
vera.print(F("&Value="));
vera.print(veraMstringVal);
vera.println(F(" HTTP/1.1"));
vera.println(F("Host: www.sdyoung.com"));
vera.println(); 
delay(100); 
vera.println();
veraSuccess = true;
//Serial.println(F("\n Data sent to Vera"));  
}
else {
//    veraSuccess = false;
Serial.println(F("\n ### FAILED POSTING MULTISTRING UPDATE TO VERA.SDYOUNG.COM ###\r\n"));
resetFunc();
}
vera.stop();
while(vera.status() != 0) {
delay(5);
}
}
void loop() {
digitalWrite(DTR, LOW);                         // Enable Receiving Data
EthernetClient client = server.available();     // try to get client
if (client) {  // got client?
boolean currentLineIsBlank = true;
while (client.connected()) {
if (client.available()) {                   // client data available to read
char c = client.read();                   // read 1 byte (character) from client
if (req_index < (REQ_BUF_SZ - 1)) {
digitalWrite(13,HIGH);
HTTP_req[req_index] = c;                // save HTTP request character
req_index++;
digitalWrite(13,LOW);
}
if (c == '\n' && currentLineIsBlank) {
client.println(F("HTTP/1.1 200 OK"));   // send a standard http response header
if (StrContains(HTTP_req, "/pool/light/on")) {
client.println(F("Content-Type: text/plain"));
client.println(F("Connection: close"));
client.println();
lightState = 1;                       //light is on
veraVarDevId = veraVarLight;          //set devID var to 26 for Vera
veraVarTargetVal = lightState;        //set devID26 value to 1 which is on
veraUpdatePending = true;
xivelyTrigger = true;
digitalWrite(DTR, HIGH);
for (byte count = 0; count < 2; count++) { 
for(byte i = 0; i < sizeof(poolLightOn); i++) {   
Serial1.write(poolLightOn[i]);
}
}
}
else if (StrContains(HTTP_req, "/pool/light/off")) {
client.println(F("Content-Type: text/plain"));
client.println(F("Connection: close"));
client.println();
lightState = 0;
veraVarDevId = veraVarLight;          //set devID var to 26 for Vera
veraVarTargetVal = lightState;        //set devID26 value to 0 which is on
veraUpdatePending = true;
xivelyTrigger = true;
digitalWrite(DTR, HIGH);
for (byte count = 0; count < 2; count++) { 
for(byte i = 0; i < sizeof(poolLightOff); i++) {  
Serial1.write(poolLightOff[i]);
}
}
}
else if (StrContains(HTTP_req, "/pool/clean/on")) {
client.println(F("Content-Type: text/plain"));
client.println(F("Connection: close"));
client.println();
cleanerState = 1;
veraVarDevId = veraVarCleaner;        //set devID var to 36 for Vera
veraVarTargetVal = cleanerState;      //set devID36 value to 0 which is on
veraUpdatePending = true;
digitalWrite(DTR, HIGH);
for (byte count = 0; count < 2; count++) { 
for(byte i = 0; i < sizeof(cleanerOn); i++) {   
Serial1.write(cleanerOn[i]);
}
}
}
else if (StrContains(HTTP_req, "/pool/clean/off")) {
client.println(F("Content-Type: text/plain"));
client.println(F("Connection: close"));
client.println();
cleanerState = 0;
veraVarDevId = veraVarCleaner;        //set devID var to 36 for Vera
veraVarTargetVal = cleanerState;      //set devID36 value to 0 which is on
veraUpdatePending = true;
digitalWrite(DTR, HIGH);
for (byte count = 0; count < 2; count++) { 
for(byte i = 0; i < sizeof(cleanerOff); i++) {   
Serial1.write(cleanerOff[i]);
}
}
}
else if (StrContains(HTTP_req, "/pool/waterfall/on")) {
client.println(F("Content-Type: text/plain"));
client.println(F("Connection: close"));
client.println();
waterfallState = 1;
veraVarDevId = veraVarWaterfall;      //set devID var to 34 for Vera
veraVarTargetVal = waterfallState;    //set devID34 value to 1 which is on
veraUpdatePending = true;
digitalWrite(DTR, HIGH);
for (byte count = 0; count < 2; count++) { 
for(byte i = 0; i < sizeof(waterFallOn); i++) {   
Serial1.write(waterFallOn[i]);
}
}
}
else if (StrContains(HTTP_req, "/pool/waterfall/off")) {
client.println(F("Content-Type: text/plain"));
client.println(F("Connection: close"));
client.println();
waterfallState = 0;
veraVarDevId = veraVarWaterfall;      //set devID var to 34 for Vera
veraVarTargetVal = waterfallState;    //set devID34 value to 0 which is on
veraUpdatePending = true;
digitalWrite(DTR, HIGH);
for (byte count = 0; count < 2; count++) { 
for(byte i = 0; i < sizeof(waterFallOff); i++) {   
Serial1.write(waterFallOff[i]);
}
}
}
else if (StrContains(HTTP_req, "/pool/circulate/on")) {
client.println(F("Content-Type: text/plain"));
client.println(F("Connection: close"));
client.println();
circulationState = 1;
veraVarDevId = veraVarCirculation;    //set devID var to 35 for Vera
veraVarTargetVal = circulationState;  //set devID35 value to 0 which is on
veraUpdatePending = true;
digitalWrite(DTR, HIGH);
for (byte count = 0; count < 2; count++) { 
for(byte i = 0; i < sizeof(poolOn); i++) {   
Serial1.write(poolOn[i]);
}
}
}
else if (StrContains(HTTP_req, "/pool/circulate/off")) {
client.println(F("Content-Type: text/plain"));
client.println(F("Connection: close"));
client.println();
circulationState = 0;
veraVarDevId = veraVarCirculation;    //set devID var to 35 for Vera
veraVarTargetVal = circulationState;  //set devID35 value to 0 which is on
veraUpdatePending = true;
digitalWrite(DTR, HIGH);
for (byte count = 0; count < 2; count++) { 
for(byte i = 0; i < sizeof(poolOff); i++) {   
Serial1.write(poolOff[i]);
}
}
}
else if (StrContains(HTTP_req, "/xively/update")) {
client.println(F("Content-Type: text/plain"));
client.println(F("Connection: close"));
client.println();
xivelyTrigger = true;
}
else if (StrContains(HTTP_req, "/pool/chlorinator/getsetpoint")) {
client.println(F("Content-Type: text/plain"));
client.println(F("Connection: close"));
client.println();
client.print(F("[{\"chlorSetpoint\":\""));
client.print(saltSetpoint);
client.print(F("\"}]"));
}
else if (StrContains(HTTP_req, "/pool/chlorinator/getsalinity")) {
client.println(F("Content-Type: text/plain"));
client.println(F("Connection: close"));
client.println();
client.print(F("[{\"currentSalinityPPM\":\""));
client.print(salinityNow);
client.print(F("\"}]"));
}
else if (StrContains(HTTP_req, "/pool/chlorinator/error")) {
client.println(F("Content-Type: text/plain"));
client.println(F("Connection: close"));
client.println();
client.print(F("[{\"chlorError\":\""));
client.print(saltStateResult);
client.print(F("\"}]"));
} 
else if (StrContains(HTTP_req, "/pool/water/temp")) {
client.println(F("Content-Type: text/plain"));
client.println(F("Connection: close"));
client.println();
client.print(F("[{\"waterTemp\":\""));
client.print(poolTemp);
client.print(F("\"}]"));
}
else if (StrContains(HTTP_req, "/pool/pump/rpm")) {
client.println(F("Content-Type: text/plain"));
client.println(F("Connection: close"));
client.println();
client.print(F("[{\"pumpRPM\":\""));
client.print(pumpRPM);
client.print(F("\"}]"));
}
else if (StrContains(HTTP_req, "/pool/pump/watts")) {
client.println(F("Content-Type: text/plain"));
client.println(F("Connection: close"));
client.println();
client.print(F("[{\"pumpWatts\":\""));
client.print(pumpWatts);
client.print(F("\"}]"));
}
else if (StrContains(HTTP_req, "/pool/light/state")) {
client.println(F("Content-Type: text/plain"));
client.println(F("Connection: close"));
client.println();
client.print(F("[{\"lightState\":\""));
client.print(lightState);
client.print(F("\"}]"));
}
else if (StrContains(HTTP_req, "/avr/uptime")) {
rawUptime = (millis() / 1000);
client.println(F("Content-Type: text/plain"));
client.println(F("Connection: close"));
client.println();
client.print(F("[{\"rawUptimeInSecs\":\""));
client.print(rawUptime);
client.print(F("\"}]"));
}
else if (StrContains(HTTP_req, "/pool/chlorinator/set/1")) {
client.println(F("Content-Type: text/plain"));
client.println(F("Connection: close"));
client.println();
digitalWrite(DTR, HIGH);
for (byte count = 0; count < 8; count++) { 
for(byte i = 0; i < sizeof(chlorStep1); i++) {
Serial1.write(chlorStep1[i]);
}
delay(5);
}
}
else if (StrContains(HTTP_req, "/pool/chlorinator/set/2")) {
client.println(F("Content-Type: text/plain"));
client.println(F("Connection: close"));
client.println();
digitalWrite(DTR, HIGH);
for (byte count = 0; count < 8; count++) { 
for(byte i = 0; i < sizeof(chlorStep2); i++) {   
Serial1.write(chlorStep2[i]);
}
delay(5);
}
}
else if (StrContains(HTTP_req, "/pool/chlorinator/set/3")) {
client.println(F("Content-Type: text/plain"));
client.println(F("Connection: close"));
client.println();
digitalWrite(DTR, HIGH);
for (byte count = 0; count < 8; count++) { 
for(byte i = 0; i < sizeof(chlorStep3); i++) {   
Serial1.write(chlorStep3[i]);
}
delay(5);
}
}
else if (StrContains(HTTP_req, "/pool/chlorinator/set/4")) {
client.println(F("Content-Type: text/plain"));
client.println(F("Connection: close"));
client.println();
digitalWrite(DTR, HIGH);
for (byte count = 0; count < 8; count++) { 
for(byte i = 0; i < sizeof(chlorStep4); i++) {   
Serial1.write(chlorStep4[i]);
}
delay(5);
}
}
else if (StrContains(HTTP_req, "/pool/chlorinator/set/5")) {
client.println(F("Content-Type: text/plain"));
client.println(F("Connection: close"));
client.println();
digitalWrite(DTR, HIGH);
for (byte count = 0; count < 8; count++) { 
for(byte i = 0; i < sizeof(chlorStep5); i++) {   
Serial1.write(chlorStep5[i]);
}
delay(5);
}
}
else if (StrContains(HTTP_req, "/pool/chlorinator/set/6")) {
client.println(F("Content-Type: text/plain"));
client.println(F("Connection: close"));
client.println();
digitalWrite(DTR, HIGH);
for (byte count = 0; count < 8; count++) { 
for(byte i = 0; i < sizeof(chlorStep6); i++) {   
Serial1.write(chlorStep6[i]);
}
delay(5);
}
}
else if (StrContains(HTTP_req, "/pool/chlorinator/set/7")) {
client.println(F("Content-Type: text/plain"));
client.println(F("Connection: close"));
client.println();
digitalWrite(DTR, HIGH);
for (byte count = 0; count < 8; count++) { 
for(byte i = 0; i < sizeof(chlorStep7); i++) {   
Serial1.write(chlorStep7[i]);
}
delay(5);
}
}
else if (StrContains(HTTP_req, "/pool/chlorinator/set/8")) {
client.println(F("Content-Type: text/plain"));
client.println(F("Connection: close"));
client.println();
digitalWrite(DTR, HIGH);
for (byte count = 0; count < 8; count++) { 
for(byte i = 0; i < sizeof(chlorStep8); i++) {   
Serial1.write(chlorStep8[i]);
}
delay(5);
}
}
else if (StrContains(HTTP_req, "/pool/status")) {
rawUptime = (millis() / 1000);
client.println(F("HTTP/1.1 200 OK"));
client.println(F("Content-Type: text/html"));
client.println(F("Connnection: close"));
client.println();
client.println(F("<!DOCTYPE html>"));
client.println(F("<html>"));
client.println(F("<head>"));
client.println(F("<style>p {font-family: \"Lucida Console\", \"Lucida Sans Typewriter\", monaco, \"Bitstream Vera Sans Mono\", monospace; font-size: 16px;  font-style: normal;  font-variant: normal;  font-weight: 400;  line-height: 20px; margin:0px;} </style>"));
client.println(F("<title>Pool Params</title>"));
client.println(F("<meta http-equiv=\"refresh\" content=\"5\">"));
client.println(F("</head>"));
client.println(F("<body>"));
client.print(F("<p>"));
client.println(s1);
client.print(F("</p>"));
client.print(F("<p>Pump state........ "));
if (circulationState == 0 && waterfallState == 0 && cleanerState == 0) client.print(F("Off</p>"));
if (circulationState > 0 || waterfallState > 0 || cleanerState > 0) client.print(F("On</p>"));
client.print(F("<p>Pump RPM.......... "));
client.print(pumpRPM);
client.print(F("<p>Pump watts........ "));
client.print(pumpWatts);
client.print(F("<p>Pool temp......... "));
client.print(poolTemp);
client.print((char)176);
if (circulationState == 0 && waterfallState == 0 && cleanerState == 0) {
client.print(F("F (plumbing temp)</p>"));
} else {
client.print(F("F</p>"));
}
client.print(F("<p>Air temp.......... "));
client.print(airTemp);
client.print((char)176);
client.print(F("F</p>"));
client.print(F("<p>Waterfall state... "));
if (waterfallState == 0) client.print(F("Off</p>"));
if (waterfallState == 1) client.print(F("On</p>"));
client.print(F("<p>Cleaner state..... "));
if (cleanerState == 0) client.print(F("Off</p>"));
if (cleanerState == 1) client.print(F("On</p>"));
client.print(F("<p>Light state....... "));
if (lightState == 0) client.print(F("Off</p>"));
if (lightState == 1) client.print(F("On</p>"));
if (salinityNow <= 3800) client.print(F("<p>Water salinity.... "));
if (salinityNow >= 3801 && salinityNow <= 4200) client.print(F("<p style=\"background-color:yellow;\"> Water salinity.... "));
if (salinityNow >= 4201) client.print(F("<p style=\"background-color:red;\">Water salinity.... "));
client.print(salinityNow);
client.print(F(" PPM</p>"));
client.print(F("<p>Salt cell setpoint "));
client.print(saltSetpoint);
client.print(F("%</p>"));
client.print(F("<p>Salt cell output.. "));
client.print(saltPct);
if (saltPct > saltSetpoint) {
client.print(F("% - SUPERCHLOR ACTIVE"));
} else if (saltPct < saltSetpoint && (circulationState > 0 || waterfallState > 0 || cleanerState > 0)) {
client.print(F("% - ON, IDLE DWELL"));
} else if (saltPct == saltSetpoint) {
client.print(F("% - NORMAL OPERATION"));
} else {
client.print(F("% - PUMP IS IDLE"));
}
client.print(F("</p>"));
client.print(F("<p>Salt cell state... "));
if (saltStateResult == 0x0) {
client.print(F("RUNNING - NO ERRORS</p>"));
} else if (saltStateResult == 0x1) {
client.print(F("COMM ERROR</p>"));
} else if (saltStateResult == 0x2) {
client.print(F("RUNNING - LOW FLOW</p>"));
} else if (saltStateResult == 0x3) {
client.print(F("DISABLED - PUMP OFF</p>"));              
} else if (saltStateResult == 0xF0) {
client.print(F("PENDING QUERY RESPONSE</p>"));              
} else {
client.print(saltStateResult);
client.print(F("</p>"));
}
client.print(F("<p>Panel time........ "));
client.print(panelHour);
client.print(F(":"));
if(panelMinute < 10) {
client.print(F("0"));
}
client.print(panelMinute);
client.print(F("</p>"));
client.print(F("<p>NTP real time..... "));
client.print(ntpHours);
client.print(F(":"));
if(ntpMinutes < 10) {
client.print(F("0"));
}
client.print(ntpMinutes);
client.print(F("</p>"));
client.print(F("<p>AVR uptime........ "));
uptimeHttp();
client.print(strUptime);
client.print(F("</p>"));
strUptime = "";
client.print(F("\n<br/>"));
client.print(F("<p><a href=\"https://personal.xively.com/feeds/805849745\" target=\"_blank\">https://personal.xively.com/feeds/805849745</a></p>"));
client.print(F("<p><a href=\"../../avr/api/usage\" target=\"_blank\">HTTP API command usage</a></p>"));
client.print(F("</body>"));
client.print(F("</html>"));
}
else if (StrContains(HTTP_req, "/avr/api/usage")) {
client.println(F("HTTP/1.1 200 OK"));
client.println(F("Content-Type: text/html"));
client.println(F("Connnection: close"));
client.println();
client.println(F("<!DOCTYPE html>"));
client.println(F("\n<html>"));
client.println(F("\n<head>"));
client.println(F("\n<style>p {font-family: \"Lucida Console\", \"Lucida Sans Typewriter\", monaco, \"Bitstream Vera Sans Mono\", monospace; font-size: 16px;  font-style: normal;  font-variant: normal;  font-weight: 400;  line-height: 20px; margin:0px;} </style>"));
client.println(F("\n<title>Pool API Usage</title>"));
client.println(F("\n<meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\">"));
client.println(F("\n</head>"));
client.println(F("\n<body>"));
client.print(F("<p>"));
client.println(s1);
client.print(F("</p>"));
client.print(F("\n<p> Most commands should be self-explanatory.  Ambiguous commands have notes.</p>"));
client.print(F("\n<br/>"));
client.print(F("\n<p> /pool/light/on</p>"));
client.print(F("\n<p> /pool/light/off</p>"));
client.print(F("\n<p> /pool/clean/on <--------------------sets pump to in-floor cleaner RPM</p>"));
client.print(F("\n<p> /pool/clean/off</p>"));
client.print(F("\n<p> /pool/waterfall/on</p>"));
client.print(F("\n<p> /pool/waterfall/off</p>"));
client.print(F("\n<p> /pool/circulate/on <----------------sets pump to low speed circulation</p>"));
client.print(F("\n<p> /pool/circulate/off</p>"));
client.print(F("\n<p> /pool/chlorinator/set/X <-----------where X is 1-8 to change chlor setpoint</p>"));
client.print(F("\n<p> /pool/chlorinator/getsetpoint <-----returns JSON formatted setpoint</p>"));
client.print(F("\n<p> /pool/chlorinator/getsalinity <-----returns JSON formatted salinity</p>"));  
client.print(F("\n<p> /pool/chlorinator/error <-----------returns JSON formatted error level</p>"));
client.print(F("\n<p> /pool/water/temp <------------------returns JSON formatted water temp</p>"));
client.print(F("\n<p> /pool/pump/rpm <--------------------returns JSON formatted pump RPM</p>"));
client.print(F("\n<p> /pool/pump/watts <------------------returns JSON formatted pump watts</p>"));
client.print(F("\n<p> /pool/light/state <-----------------returns JSON formatted light status</p>"));
client.print(F("\n<p> /pool/status <----------------------returns HTML formatted pool summary</p>"));
client.print(F("\n<p> /avr/uptime <-----------------------returns JSON formatted arduino uptime</p>"));
client.print(F("\n<p> /avr/api/usage <--------------------returns HTML formatted HTTP API calls</p>"));
client.print(F("\n<p> /xively/update <--------------------manually force Xively update</p>"));
client.print(F("\n<br/>"));
client.print(F("\n<p><a href=\"../../pool/status\">Pool Status</a></p>"));
client.print(F("\n</body>"));
client.print(F("\n</html>"));
}
// display received HTTP request on serial port
Serial.print(F("\r\nHTTP --> "));
Serial.println(HTTP_req);
delay(20);
// reset buffer index and all buffer elements to 0
req_index = 0;
StrClear(HTTP_req, REQ_BUF_SZ);
if (debug == true) Serial.println(F("step 1"));
break;
}
if (c == '\n') {
// last character on line of received text
// starting new line with next character read
currentLineIsBlank = true;
} 
else if (c != '\r') {
// a text character was received from client
currentLineIsBlank = false;
}
} // end if (client.available())
}   // end while (client.connected())          
delay(1);                                                 // give the web browser time to receive the data
client.stop();                                            // close the connection
} // end if (client)
if (saltSetpointTrigger == true) {
digitalWrite(DTR, HIGH);
for (byte count = 0; count < 1; count++) {  //whenever update Xively occurs, query the chlorinator setpoint
for(byte i = 0; i < sizeof(saltPctQuery); i++) {
Serial1.write(saltPctQuery[i]);
Serial.print((saltPctQuery[i]),HEX);
}
}
saltSetpointTrigger = false;
}
char c;
while (Serial1.available()) {
c = (uint8_t)Serial1.read();
switch (goToCase) {
case header1:
if (c == 0xFFFFFFFF) {                               // ignoring leading FF so do nothing, repeat again
*bPointer++ = (char)c;
byteNum = 1;
if (debug == true) Serial.println(F("step 2"));
break;
}
else if (c == 0x0) {                                 // is this a 0 in byte 2?  could be Pentair packet
goToCase = header3;
*bPointer++ = (char)c;
byteNum++;
if (debug == true) Serial.println(F("step 3"));
break;
}
else { //if (c == 0x10)                              // is this an IntelliChlor header?  could be an IntelliChlor packet
goToCase = saltHead2;
*bPointer++ = (char)c;
byteNum = 1;
if (debug == true) Serial.println(F("step 4"));
break;
}
if (debug == true) Serial.println(F("step 5"));
break;
     
case header3:
*bPointer++ = (char)c;
if (c == 0xFFFFFFFF) {                               // it's not really the start of a frame, must be deeper into a Pentair packet
goToCase = header4;
byteNum++;
if (debug == true) Serial.println(F("step 6"));
break;
}
else {
clear485Bus();
goToCase = header1;
if (debug == true) Serial.println(F("step 7"));
break;
}
if (debug == true) Serial.println(F("step 8"));
break;
case header4:
if (c == 0xFFFFFFA5) {                              // it's not really the start of a frame, almost have a Pentair preamble match
goToCase = bufferData;
sumOfBytesInChkSum += (byte)c, HEX;
*bPointerOfBytes++ = (char)c;
*bPointer++ = (char)c;
byteNum++;
if (debug == true) Serial.println(F("step 9"));
break;
}
else {
clear485Bus();
goToCase = header1;
if (debug == true) Serial.println(F("step 10"));
break;
}
if (debug == true) Serial.println(F("step 11"));
break;
case bufferData:
*bPointer++ = (char)c;                             // loop until byte 9 is seen
*bPointerOfBytes++ = (char)c;                      // add up in the checksum bytes
byteNum++;
sumOfBytesInChkSum += (byte)c, HEX;
if (1 != 2) {                                      // janky code here... whatever.  you clean it up mr. awesome
if (byteNum == 9) {                              // get data payload length of bytes
bytesOfDataToGet = (c);
Serial.println();
Serial.println();
Serial.print(F( "\n Free RAM = " ));
Serial.print(freeRam());
Serial.println(F( " <-- watch for memory leaks" ));
Serial.println();
Serial.println();
digitalClockDisplay();
uptime();
Serial.println(F("NEW RS-485 FRAMES RECEIVED"));
Serial.print(F("Payload bytes to get... "));
Serial.println(bytesOfDataToGet);
if (bytesOfDataToGet < 0 || bytesOfDataToGet > 47) {  //uh oh.....buffer underflow or buffer overflow... Time to GTFO
clear485Bus();
if (debug == true) Serial.println(F("step 12"));
    break;
}
if (remainingBytes == bytesOfDataToGet) {
goToCase = calcCheckSum;
if (debug == true) Serial.println(F("step 13"));
break;
}
remainingBytes++;
if (debug == true) Serial.println(F("step 14"));
break;
}
if (byteNum >= 10) {
if (remainingBytes == bytesOfDataToGet) {
goToCase = calcCheckSum;
if (debug == true) Serial.println(F("step 15"));
break;
}
remainingBytes++;
if (debug == true) Serial.println(F("step 16"));
break;
}
if (debug == true) Serial.println(F("step 17"));
break;
}
if (debug == true) Serial.println(F("step 18")); 
break;     
case calcCheckSum:
if (chkSumBits < 2) {
*bPointer++ = (char)c;
if (chkSumBits == 0) {
Serial.print(F("Checksum high byte..... "));
Serial.println(c, HEX);
chkSumValue = (c * 256);
}
else if (chkSumBits == 1) {
Serial.print(F("Checksum low byte...... "));
  Serial.println((byte)c, HEX);
  goToCase = header1;
  byte len = (byte)(bPointer - buffer); 
chkSumValue += (byte)c;
  printFrameData(buffer, len);
  clear485Bus();
if (debug == true) Serial.println(F("step 19"));
  break;
}
chkSumBits++;
if (debug == true) Serial.println(F("step 20"));
break;
}
if (debug == true) Serial.println(F("step 21"));
break;
case saltHead2:
if (c == 0x02) {                                  // is this Intellichlor STX header frame 2 ?
goToCase = bufferSaltData;
*bPointer++ = (char)c;
byteNum++;
if (debug == true) Serial.println(F("step 22"));
break;
}
else {
clear485Bus();
goToCase = header1;
if (debug == true) Serial.println(F("step 23"));
break;
}
if (debug == true) Serial.println(F("step 24"));
break;
  
case bufferSaltData:
if (c != 0x10) {
*bPointer++ = (char)c;                          // loop until byte value 0x10 is seen
byteNum++;
if (debug == true) Serial.println(F("step 25"));
break;
}
else {                                            // a ha! found a 0x10, we're close
goToCase = saltTerm;
*bPointer++ = (char)c;
byteNum++;
if (debug == true) Serial.println(F("step 26"));
break;
}
if (debug == true) Serial.println(F("step 27"));
break;
case saltTerm:
*bPointer++ = (char)c;
byteNum++;
goToCase = header1;
if (c != 0x03) {
clear485Bus();
if (debug == true) Serial.println(F("step 28"));
break;
}
else {                                            // found an ETX 0x3.  See what we've got
byte len = (byte)(bPointer - buffer); 
Serial.println();
Serial.println();
digitalClockDisplay();
Serial.println(F("NEW RS-485 IntelliChlor FRAMES RECEIVED"));
if (len == 8) {
saltBytes1 = (buffer[2] + buffer[3] + buffer[4] + 18);
Serial.print(F("Short salt byte sum +18. "));
Serial.println(saltBytes1);
Serial.print(F("Short Salt checksum is.. "));
Serial.println(buffer[5]);
if (saltBytes1 == buffer[5]) {
salt = true;
oldSaltPct = saltPct;
saltPct = buffer[4];
Serial.println(F("Checksum is............. GOOD"));
  Serial.print(F("Chlorinator Load........ "));
Serial.print(saltPct);
Serial.println(F("%")); 
//Serial.println();
printFrameData(buffer, len);
//if (oldSaltPct != saltPct) { //disabled b/c when idle the output doesn't toggle, but provides interval updates anyway for other vars
//saltSetpointTrigger = true;
xivelyTrigger = true;
saltOutputToggle++;
//}
}
else {
Serial.println(F("Checksum is............. INVALID"));
  salt = true;
printFrameData(buffer, len);
//Serial.println();
}
clear485Bus();
if (debug == true) Serial.println(F("step 29"));
break;
}
else {
saltBytes2 = (buffer[2] + buffer[3] + buffer[4] + buffer[5] + 18);
Serial.print(F("Long salt byte sum +18.. "));
Serial.println(saltBytes2);
Serial.print(F("Long Salt checksum is... "));
Serial.println(buffer[6]);
if (saltBytes2 == buffer[6]) {
salt = true;
Serial.println(F("Checksum is............. GOOD"));
printFrameData(buffer, len);
//someVal = buffer[6];
digitalWrite(DTR, HIGH);
for (byte count = 0; count < 2; count++) {  //whenever the long salt string checksum checks out, query the chlorinator setpoint
for(byte i = 0; i < sizeof(saltPctQuery); i++) {   
Serial1.write(saltPctQuery[i]);
//Serial.print((byte)saltPctQuery[i], HEX);
}
}
salinityNow = (buffer[4] * 50);
}
else {
Serial.println(F("Checksum is............. INVALID"));
salt = true;
printFrameData(buffer, len);
//Serial.println();
}
  clear485Bus();
if (debug == true) Serial.println(F("step 30"));
  break;
}
if (debug == true) Serial.println(F("step 31"));
break;
}
clear485Bus();
if (debug == true) Serial.println(F("step 32"));
break;
}                                                          // end switch( goToCase )
}                                                            // while serial available
//these IF statements update the Vera HA unit with status changes.  May need to slow these down if lock-ups occur.
if (veraUpdatePending == true) {  //updates the Vera virtual switches from an HTTP call
veraUpdatePending = false;
veraSendVswitch(veraVarDevId, veraVarTargetVal);
}
if (oldPoolMode != poolMode) {    //updates the Vera virtual switches from Pentair remote or console panel
oldPoolMode = poolMode;
for (byte x = 0; x < sizeof(veraVarVals); x++) {
veraSendVswitch(veraVarIDs[x], veraVarVals[x]);
}
}
if (oldSaltSetpoint != saltSetpoint) {
oldSaltSetpoint = saltSetpoint;
//veraUpdateCount++;
veraSendMultiString(veraMstringChlor, saltSetpoint);
}
if (oldPoolTemp != poolTemp && pumpMode > 0) {
oldPoolTemp = poolTemp;
//veraUpdateCount++;
veraSendMultiString(veraMstringWaterTemp, poolTemp);
}
if (oldPumpRPM != pumpRPM) {
oldPumpRPM = pumpRPM;
//veraUpdateCount++;
veraSendMultiString(veraMstringPumpRPM, pumpRPM);
}
if (oldPumpWatts != pumpWatts) {
oldPumpWatts = pumpWatts;
//veraUpdateCount++;
veraSendMultiString(veraMstringPumpWatts, pumpWatts);
}
if (oldSaltPct != saltPct) {
oldSaltPct = saltPct;
//veraUpdateCount++;
veraSendMultiString(veraMstringChlorOn, saltPct);
}
//  if (finalXivelyPost == false) { //update Xively after transition from on to IDLE so values go to 0
//    if (poolMode == 0x0 && saltOutputToggle > 2) {  //pool idle but has run
//      finalXivelyPost = true;
//      xivelyMillis = millis();
//    }
//    if (poolMode == 0x2 || poolMode == 0x4 || poolMode == 0x6) {  //pool running but not generating salt
//      finalXivelyPost = true;
//      xivelyMillis = millis();
//      saltOutputToggle = 3; //just need to force it above threshold of 2
//    }
//  }
//
////  if (poolMode == 0x2 || poolMode == 0x4 || poolMode == 0x6) { //update Xively during light (no pump), waterfall & waterfall+light because chlorinator isn't generating in this mode
////    finalXivelyPost = true;
////    xivelyMillis = millis();
////  }
//  
////  if (((millis() - xivelyMillis) > 29995) && ((millis() - xivelyMillis) < 30005) && finalXivelyPost == true) {    //wait 30 seconds for all variables to update
////    xivelyMillis = millis();
////    Serial.print(F("30303030303030303030303030303030303030303030"));
////    //xivelyTrigger = true;
////  }
//  
//  if (finalXivelyPost == true) {
//    if (poolMode != 0x2 && poolMode != 0x4 && poolMode != 0x6) {
//      finalXivelyPost = false; //pool back on, posted final to Xively, re-arm final post to Xively
//    } 
//    if (millis() - xivelyMillis > 29000) {
//      xivelyMillis = millis();
//      xivelyTrigger = true;
//    }
//  }
if (xivelyTrigger == true) {
xivelyTrigger = false;
noInterrupts();   //disabled 3-16-16 >> re-enabled 5-18-16
//delay(5);
if (saltOutputToggle > 2) {
sendToRasPi();
}
//xivelyPost();   //disabled 5-20-16 renabled 6-1-16 disabled 6-4-16
//delay(5);
//xivelyStatus(); //disabled 5-20-16 renabled 6-1-16 disabled 6-4-16
//delay(5);            
veraPost();       //re-enabled 5-18-16
//delay(5);
interrupts();     //disabled 3-16-16 >> re-enabled 5-18-16
saltSetpointTrigger = true;
//      digitalWrite(DTR, HIGH);
//      for (byte count = 0; count < 1; count++) {  //whenever update Xively occurs, query the chlorinator setpoint
//        for(byte i = 0; i < sizeof(saltPctQuery); i++) {
//          Serial1.write(saltPctQuery[i]);
//          //Serial.print((saltPctQuery[i]),HEX);
//        }
//        //delay(5);
//      } 
}
currentMillis=millis();
}                                                              // end void loop
void veraPost() {
//  if (oldSaltSetpoint != saltSetpoint) {
//    oldSaltSetpoint = saltSetpoint;
//    //veraUpdateCount++;
//    veraSendMultiString(veraMstringChlor, saltSetpoint);
//  }
//  if (oldPoolTemp != poolTemp && pumpMode > 0) {
//    oldPoolTemp = poolTemp;
//    //veraUpdateCount++;
//    veraSendMultiString(veraMstringWaterTemp, poolTemp);
//  }
//  if (oldPumpRPM != pumpRPM) {
//    oldPumpRPM = pumpRPM;
//    //veraUpdateCount++;
//    veraSendMultiString(veraMstringPumpRPM, pumpRPM);
//  }
//  if (oldPumpWatts != pumpWatts) {
//    oldPumpWatts = pumpWatts;
//    //veraUpdateCount++;
//    veraSendMultiString(veraMstringPumpWatts, pumpWatts);
//  }
//  if (oldSaltPct != saltPct) {
//    oldSaltPct = saltPct;
//    //veraUpdateCount++;
//    veraSendMultiString(veraMstringChlorOn, saltPct);
//  }
}
void clear485Bus() {
memset(buffer, 0, sizeof(buffer));
bPointer = buffer;
memset(bufferOfBytes, 0, sizeof(bufferOfBytes));
bPointerOfBytes = bufferOfBytes;
byteNum = 0;
bytesOfDataToGet = 0;
remainingBytes = 0;
chkSumBits = 0;
saltBytes1 = 0;
saltBytes2 = 0;
salt = false;
sumOfBytesInChkSum = 0;
chkSumValue = 0;
}
void sendToRasPi() {
//analogWrite(A8,255);
//delay(250); //This one keeps it from hanging
if (php.connect(phpServer, 8080)) {
phpStart = millis();
Serial.print(F("Connected to RasPi >> "));
php.print(F("GET /xivelyPool?poolTemp="));
php.print(poolTemp);
php.print(F("&airTemp="));
php.print(airTemp);
php.print(F("&poolMode="));
php.print(poolMode);
php.print(F("&pumpMode="));
php.print(pumpMode);
php.print(F("&pumpWatts="));
php.print(pumpWatts);
php.print(F("&pumpRPM="));
php.print(pumpRPM);
php.print(F("&chlorOutput="));
php.print(saltPct);
php.print(F("&chlorSetpoint="));
php.print(saltSetpoint);
php.print(F("&lightState="));
php.print(lightState);
php.print(F("&chlorError="));
php.print(saltStateResult);
if (salinityNow > 0) {            //prevents posting 0ppm to Xively
php.print(F("&salinity="));
php.print(salinityNow);
}
php.println(F(" HTTP/1.1"));
php.println(F("Host: www.sdyoung.com"));
php.println();
Serial.print(F("### SUCCESSFULLY POSTED TO RASPBERRYPI.SDYOUNG.COM IN ")); 
//delay(100); 
}
else {
Serial.println(F("### FAILED POSTING TO RASPBERRYPI.SDYOUNG.COM ###\r\n"));
}
//stop client
php.stop();
while(php.status() != 0) {
delay(5);
}
//analogWrite(A8,0);
phpStop = millis();
Serial.print(phpStop - phpStart);
Serial.println(F("ms ###"));
}
void printFrameData(uint8_t* buffer, byte len) {
int i = 0;
if (salt == false) {                                         // salt does it's own check sum calculations
Serial.print(F("Sum of bytes........... "));
Serial.println(sumOfBytesInChkSum);
Serial.print(F("Check sum is........... "));
Serial.println(chkSumValue);
if (sumOfBytesInChkSum == chkSumValue) {
Serial.println(F("Check sum result....... GOOD"));
}
else {
Serial.println(F("Check sum result....... INVALID"));
}
}
int lenChkSumValue = (int)(bPointerOfBytes - bufferOfBytes);
while (i < len) {
printByteData(buffer[i++]);                                // Dumps the bytes to the serial mon
Serial.print(F(" "));
}
Serial.println(); 
if (sumOfBytesInChkSum == chkSumValue) {
if (bufferOfBytes[5] == 0x1D) {                            // 29 byte message is for broadcast display updates 
oldPoolTemp = poolTemp;
poolTemp = bufferOfBytes[20];
airTemp = bufferOfBytes[24];
oldPoolMode = poolMode;
poolMode = bufferOfBytes[8];
panelHour = bufferOfBytes[6];
panelMinute = bufferOfBytes[7];
Serial.print(F("Water Temp............. "));
Serial.print(poolTemp);
Serial.print((char)176);
Serial.println(F("F"));
Serial.print(F("Air Temp............... "));
Serial.print(airTemp);
Serial.print((char)176);
Serial.println(F("F"));
Serial.print(F("Panel time............. "));
if (panelHour < 10) {
//printByteData(panelHour)
}
Serial.print(panelHour);
Serial.print(F(":"));
if (panelMinute < 10) {
//printByteData(panelMinute);
}
Serial.println(panelMinute);
Serial.print(F("Pool Mode.............. "));
if (poolMode == 0x1) {
Serial.println(F("CLEANER"));
lightState = 0;
waterfallState = 0;
circulationState = 0;
cleanerState = 1;
}
else if (poolMode == 0x2) {
Serial.println(F("LIGHT ON"));
lightState = 1;
waterfallState = 0;
circulationState = 0;
cleanerState = 0;
}
else if (poolMode == 0x3) {
Serial.println(F("CLEANER & LIGHT ON"));
lightState = 1;
waterfallState = 0;
circulationState = 0;
cleanerState = 1;
}
else if (poolMode == 0x4 || poolMode == 0x24) {
Serial.println(F("WATERFALL"));
lightState = 0;
waterfallState = 1;
circulationState = 0;
cleanerState = 0;
}
else if (poolMode == 0x5) {
Serial.println(F("CLEANER & WATERFALL"));
lightState = 0;
waterfallState = 1;
circulationState = 0;
cleanerState = 1;
}
else if (poolMode == 0x6 || poolMode == 0x26) {
Serial.println(F("WATERFALL & LIGHT ON"));
lightState = 1;
waterfallState = 1;
circulationState = 0;
cleanerState = 0;
}
else if (poolMode == 0x7) {
Serial.println(F("CLEANER, WATERFALL & LIGHT ON"));
lightState = 1;
waterfallState = 1;
circulationState = 0;
cleanerState = 1;
}
else if (poolMode == 0x20) { 
Serial.println(F("CIRCULATION"));
lightState = 0;
waterfallState = 0;
circulationState = 1;
cleanerState = 0;
}
else if (poolMode == 0x22) {
Serial.println(F("CIRCULATION & LIGHT ON"));
lightState = 1;
waterfallState = 0;
circulationState = 1;
cleanerState = 0;
}
else {
Serial.println(F("IDLE"));
lightState = 0;
waterfallState = 0;
circulationState = 0;
cleanerState = 0;
}
}
else if (bufferOfBytes[2] == 0x10 && bufferOfBytes[3] == 0x60 && bufferOfBytes[5] == 0xF) { // 15 byte message is for pump updates
oldPumpWatts = pumpWatts;
oldPumpRPM = pumpRPM;
Serial.print(F("Pump Watts............. "));
if (buffer[9] > 0) {
pumpWatts = ((bufferOfBytes[9] * 256) + bufferOfBytes[10]);//high bit
Serial.println(pumpWatts);
Serial.print(F("Pump RPM............... "));
pumpRPM = ((bufferOfBytes[11] * 256) + bufferOfBytes[12]);
Serial.println(pumpRPM);
} 
else {
Serial.println(bufferOfBytes[9]);   //low bit
Serial.print(F("Pump RPM............... "));
pumpRPM = Serial.println((bufferOfBytes[11] * 256) + bufferOfBytes[12]);
}
pumpMode = bufferOfBytes[18];
Serial.print(F("Pump Mode.............. "));
if (pumpMode == 0x1) {
Serial.println(F("RUN"));
}
else if (pumpMode == 0x0B) {
Serial.println(F("PRIMING"));
}
else {
Serial.println(F("OFF"));
}
}
else if (bufferOfBytes[2] == 0xF && bufferOfBytes[3] == 0x10 && bufferOfBytes[24] == 0x2D && bufferOfBytes[25] == 0x2D) {
oldSaltSetpoint = saltSetpoint;
saltSetpoint = bufferOfBytes[7];
salinityNow = (bufferOfBytes[9] * 50);
Serial.print(F("Chlorinator setpoint... "));
Serial.print(saltSetpoint);
Serial.println(F("%")); 
Serial.print(F("Chlorinator errors..... "));
saltStateLow = bufferOfBytes[8];
saltStateHigh = bufferOfBytes[10];
if (saltStateLow == 0x81 && saltStateHigh == 0x81) { 
saltStateResult = 0x2; 
Serial.println(F("RUNNING - LOW FLOW"));
}
else if (saltStateLow == 0x81 && saltStateHigh == 0x80) {
saltStateResult = 0x0; 
Serial.println(F("RUNNING - NO ERRORS"));
}
else if (saltStateLow == 0x80 && saltStateHigh == 0x81) { 
saltStateResult = 0x1;
Serial.println(F("COMM ERROR"));
}
else if (saltStateLow == 0x80 && saltStateHigh == 0x80) { 
saltStateResult = 0x3;
Serial.println(F("DISABLED - PUMP OFF"));
}
}
// these are the virtual switches, not the multistring variables
veraVarVals[0] = lightState;
veraVarVals[1] = waterfallState;
veraVarVals[2] = circulationState;
veraVarVals[3] = cleanerState;
}
}
// routine to take binary numbers and show them as two bytes hex
void printByteData(uint8_t Byte) {
Serial.print((uint8_t)Byte >> 4, HEX);
Serial.print((uint8_t)Byte & 0x0f, HEX);
}
/*-------- NTP code ----------*/
const int NTP_PACKET_SIZE = 48; // NTP time is in the first 48 bytes of message
byte packetBuffer[NTP_PACKET_SIZE]; //buffer to hold incoming & outgoing packets
time_t getNtpTime() {
while (Udp.parsePacket() > 0) ; // discard any previously received packets
Serial.println(F("<--- Transmit NTP Request"));
sendNTPpacket(timeServer);
uint32_t beginWait = millis();
while (millis() - beginWait < 1500) {
int size = Udp.parsePacket();
if (size >= NTP_PACKET_SIZE) {
Serial.println(F("---> Received NTP Response"));
Udp.read(packetBuffer, NTP_PACKET_SIZE);  // read packet into the buffer
unsigned long secsSince1900;
// convert four bytes starting at location 40 to a long integer
secsSince1900 =  (unsigned long)packetBuffer[40] << 24;
secsSince1900 |= (unsigned long)packetBuffer[41] << 16;
secsSince1900 |= (unsigned long)packetBuffer[42] << 8;
secsSince1900 |= (unsigned long)packetBuffer[43];
return secsSince1900 - 2208988800UL + timeZone * SECS_PER_HOUR;
}
}
Serial.println(F("No NTP Response :-("));
return 0; // return 0 if unable to get the time
}
// send an NTP request to the time server at the given address
void sendNTPpacket(IPAddress &address) {
// set all bytes in the buffer to 0
memset(packetBuffer, 0, NTP_PACKET_SIZE);
// Initialize values needed to form NTP request
// (see URL above for details on the packets)
packetBuffer[0] = 0b11100011;   // LI, Version, Mode
packetBuffer[1] = 0;     // Stratum, or type of clock
packetBuffer[2] = 6;     // Polling Interval
packetBuffer[3] = 0xEC;  // Peer Clock Precision
// 8 bytes of zero for Root Delay & Root Dispersion
packetBuffer[12]  = 49;
packetBuffer[13]  = 0x4E;
packetBuffer[14]  = 49;
packetBuffer[15]  = 52;
// all NTP fields have been given values, now
// you can send a packet requesting a timestamp:                 
Udp.beginPacket(address, 123); //NTP requests are to port 123
Udp.write(packetBuffer, NTP_PACKET_SIZE);
Udp.endPacket();
} 
void digitalClockDisplay(){
// digital clock display of the time
ntpHours = hour();
ntpMinutes = minute();
Serial.print(hour());
printDigits(minute());
printDigits(second());
Serial.print(F(" "));
Serial.print(month());
Serial.print(F("/"));
Serial.print(day());
Serial.print(F("/"));
Serial.print(year()); 
Serial.println(); 
//for you non-Arizona folks.... weirdos
if (((month() == 11) && (day() >= 3) || (month() == 12) || (month() > 0) && (month() < 3) || (month() == 3) && (day() >= 10))) {
timeZone = timeZone++;
}
}
void printDigits(int digits){  // utility for digital clock display: prints preceding colon and leading 0
Serial.print(F(":"));
if(digits < 10)
Serial.print(F("0"));
Serial.print(digits);
}
// sets every element of str to 0 (clears array)
void StrClear(char *str, char length) {
for (int i = 0; i < length; i++) {
str[i] = 0;
}
}
// searches for the string sfind in the string str
// returns 1 if string found
// returns 0 if string not found
char StrContains(char *str, char *sfind) {
char found = 0;
char index = 0;
char len;
len = strlen(str);
if (strlen(sfind) > len) {
return 0;
}
while (index < len) {
if (str[index] == sfind[found]) {
found++;
if (strlen(sfind) == found) {
return 1;
}
}
else {
found = 0;
}
index++;
}
return 0;
}
void uptime() {
days = 0;
hours = 0;
mins = 0;
secs = 0;
secs = currentMillis/1000; //convect milliseconds to seconds
mins=secs/60; //convert seconds to minutes
hours=mins/60; //convert minutes to hours
days=hours/24; //convert hours to days
secs=secs-(mins*60); //subtract the coverted seconds to minutes in order to display 59 secs max 
mins=mins-(hours*60); //subtract the coverted minutes to hours in order to display 59 minutes max
hours=hours-(days*24); //subtract the coverted hours to days in order to display 23 hours max
//Display results
Serial.print(F("Current Uptime is "));
if (days > 0) { // days will displayed only if value is greater than zero
Serial.print(days);
Serial.print(" days and ");
}
Serial.print(hours);
Serial.print(":");
Serial.print(mins);
Serial.print(":");
Serial.println(secs);
}
void uptimeHttp() {
days = 0;
hours = 0;
mins = 0;
secs = 0;
secs = currentMillis/1000; //convect milliseconds to seconds
mins=secs/60; //convert seconds to minutes
hours=mins/60; //convert minutes to hours
days=hours/24; //convert hours to days
secs=secs-(mins*60); //subtract the coverted seconds to minutes in order to display 59 secs max 
mins=mins-(hours*60); //subtract the coverted minutes to hours in order to display 59 minutes max
hours=hours-(days*24); //subtract the coverted hours to days in order to display 23 hours max
//Display results
strUptime = String(strUptime + days);
if (days == 1) {
strUptime = String(strUptime + " day ");
} else {
strUptime = String(strUptime + " days ");
}
if (hours < 10) {
strUptime = String(strUptime + '0' + hours);
} else {
strUptime = String(strUptime + hours);
}
strUptime = String(strUptime + ":");
if (mins < 10) {
strUptime = String(strUptime + '0' + mins);
} else {
strUptime = String(strUptime + mins);
}
strUptime = String(strUptime + ":");
if (secs < 10 ) {
strUptime = String(strUptime + '0' + secs);
} else {
strUptime = String(strUptime + secs);
}
strUptime = String(strUptime + " (DD HH:MM:SS)");
}
void headerNotes() {
Serial.println(s1);
Serial.println();
Serial.print(F("\n This code is listening for HTTP traffic at the URL HTTP://<IP Address>:<Port> printed above"));
Serial.print(F("\n Most commands should be self-explanatory.  Ambiguous commands have notes."));
Serial.println();
Serial.print(F("\n /pool/light/on"));
Serial.print(F("\n /pool/light/off"));
Serial.print(F("\n /pool/clean/on <--------------------sets pump to in-floor cleaner RPM"));
Serial.print(F("\n /pool/clean/off"));
Serial.print(F("\n /pool/waterfall/on"));
Serial.print(F("\n /pool/waterfall/off"));
Serial.print(F("\n /pool/circulate/on <----------------sets pump to low speed circulation"));
Serial.print(F("\n /pool/circulate/off"));
Serial.print(F("\n /pool/chlorinator/set/X <-----------where X is 1-8 to change chlor setpoint"));
Serial.print(F("\n /pool/chlorinator/getsetpoint <-----returns JSON formatted setpoint"));
Serial.print(F("\n /pool/chlorinator/getsalinity <-----returns JSON formatted salinity"));  
Serial.print(F("\n /pool/chlorinator/error <-----------returns JSON formatted error level"));
Serial.print(F("\n /pool/water/temp <------------------returns JSON formatted water temp"));
Serial.print(F("\n /pool/pump/rpm <--------------------returns JSON formatted pump RPM"));
Serial.print(F("\n /pool/pump/watts <------------------returns JSON formatted pump watts"));
Serial.print(F("\n /pool/light/state <-----------------returns JSON formatted light status"));
Serial.print(F("\n /pool/status <----------------------returns HTML formatted pool summary"));
Serial.print(F("\n /avr/uptime <-----------------------returns JSON formatted arduino uptime"));
Serial.print(F("\n /avr/api/usage <--------------------returns HTML formatted HTTP API calls"));  
Serial.print(F("\n /xively/update <--------------------manually force Xively update"));
Serial.println();
Serial.println();
}

And the Receiving Python Script v1.0.1 28-JUN-2016

#!/usr/bin python
# cosm.py Copyright 2012 Itxaka Serrano Garcia <itxakaserrano@gmail.com>
# licensed under the GPL2
# see the full license at http://www.gnu.org/licenses/gpl-2.0.txt
#
# You only need to add 2 things, YOUR_API_KEY_HERE and YOUR_FEED_NUMBER_HERE 
# also, you can change your stream ids, in that case change the id names in the "data = json.dumps..." line
#
# version 1.0.1 28-JUN-2016
#
#
from bottle import route, run, template, request, response
import json, subprocess, os, time, glob, sys#, MySQLdb
from decimal import *
#time.sleep(1.0)
airTemp = 0
chlorError = 0
chlorOutput = 0
chlorSetpoint = 0
lightState = 0
poolMode = 0
poolTemp = 0
pumpMode = 0
pumpRpm = 0
pumpWatts = 0
salinityNow = 0
#ArduinoUptime = 0
@route('/xivelyPool')
def displayIt():
cpu = subprocess.check_output(["awk '{print $1,$2,$3}' /proc/loadavg"], shell=True)
cpu1 = cpu.split()[0]
cpu5 = cpu.split()[1]
cpu15 = cpu.split()[2]
#print "CPU Utilization 1m / 5m / 15m : %s / %s / %s " % (cpu1, cpu5, cpu15)
global airTemp
global chlorError
global chlorOutput
global chlorSetpoint
global lightState
global poolMode
global poolTemp
global pumpMode
global pumpRPM
global pumpWatts
global salinityNow
#global ArduinoUptime
airTemp = request.query['airTemp']
chlorError = request.query['chlorError']
chlorOutput = request.query['chlorOutput']
chlorSetpoint = request.query['chlorSetpoint']
lightState = request.query['lightState']
poolMode = request.query['poolMode']
poolTemp = request.query['poolTemp']
pumpMode = request.query['pumpMode']
pumpRPM = request.query['pumpRPM']
pumpWatts = request.query['pumpWatts']
salinityNow = request.query['salinity']
#ArduinoUptime = request.query['ArduinoUptime']
data = json.dumps({"version":"1.0.0", "datastreams":[
{"id":"cpu01MinAvg","current_value":cpu1},
{"id":"cpu05MinAvg","current_value":cpu5},
{"id":"cpu15MinAvg","current_value":cpu15},
{"id":"Air_Temperature","current_value":airTemp},
{"id":"Chlorinator_Error","current_value":chlorError},
{"id":"Chlorinator_Output","current_value":chlorOutput},
{"id":"Chlorinator_Setpoint","current_value":chlorSetpoint},
{"id":"Light_State","current_value":lightState},
{"id":"Pool_Mode","current_value":poolMode},
{"id":"Pool_Temperature","current_value":poolTemp},
{"id":"Pump_Mode","current_value":pumpMode},
{"id":"Pump_RPM","current_value":pumpRPM},
{"id":"Pump_Watts","current_value":pumpWatts},
{"id":"Salinity","current_value":salinityNow},
#{"id":"Uptime","current_value":ArduinoUptime}
]})
with open("temp.tmp", "w") as f:
f.write(data)
subprocess.call(['curl --insecure --request PUT --data-binary @temp.tmp --header "X-ApiKey: [YOUR API KEY HERE]" https://api.xively.com/v2/feeds/805849745'], shell=True)
os.remove("temp.tmp")
return poolTemp,airTemp
run(host='0.0.0.0', port=8080)

OK, Now What?  What Will I See?

Since this is my slightly redacted code you should be able to load it into a Mega with RS-485 and Ethernet shields and have something close to what I have.  Of course, it will not update Xively since I REM’d out the function call and sanitized the API key and feed ID’s, and it will try to update Vera and fail, but the rest should be a good jumping off point for you… as in since your circuits are possibly addressed differently than I’ve coded for, this is where we part ways.  If by some wild galactic stroke of luck your EasyTouch configuration matches mine, buy a lotto ticket, share the winnings with me, sit back and watch this roll into the Arduino serial monitor at break-neck speed.  The bus quite chatty.

Free RAM = 5893 <-- watch for memory leaks
NEW RS-485 FRAMES RECEIVED
Payload bytes to get... 29
Checksum high byte..... 3
Checksum low byte...... 62
Sum of bytes........... 866
Check sum is........... 866
Check sum result....... GOOD
FF FF FF FF FF FF FF FF 00 FF A5 07 0F 10 02 1D 17 34 22 00 08 00 00 00 00 20 00 00 00 04 51 51 00 00 52 00 00 00 00 00 00 63 78 03 0D 03 62
Water Temp............. 81°F
Air Temp............... 82°F
Pool Mode.............. CIRCULATION & LIGHT ON
NEW RS-485 IntelliChlor FRAMES RECEIVED
Short salt byte sum +18. 98
Short Salt checksum is.. 98
Checksum is good
Chlorinator Load: 50%
10 02 50 14 32 A8 10 03 
NEW RS-485 IntelliChlor FRAMES RECEIVED
Long salt byte sum +18.. 19
Long Salt checksum is... 19
Checksum is good
10 02 00 01 00 00 13 10 03 
Free RAM = 5893 <-- watch for memory leaks
NEW RS-485 FRAMES RECEIVED
Payload bytes to get... 0
Checksum high byte..... 1
Checksum low byte...... 1C
Sum of bytes........... 284
Check sum is........... 284
Check sum result....... GOOD
FF 00 FF A5 00 60 10 07 00 01 1C 
Free RAM = 5893 <-- watch for memory leaks
NEW RS-485 FRAMES RECEIVED
Payload bytes to get... 15
Checksum high byte..... 2
Checksum low byte...... F3
Sum of bytes........... 755
Check sum is........... 755
Check sum result....... GOOD
FF 00 FF A5 00 10 60 07 0F 0A 00 00 00 F5 05 78 00 00 00 00 00 01 17 34 02 F3
Pump Watts............. 245
Pump RPM............... 1400
Pump Mode.............. RUN
Data sent to Vera

Once Xively Is Updated With Your Account Info

Xively

And If You Have A Vera Home Automation Unit

vera

IMG_0619

19 Comments

  1. \\janky code here… whatever. you clean it up mr. awesome

    Too funny!

    I noticed your xively feed hasn’t updated since 5/20/16. Did you break something in the code?

    Also, I was wondering if you can control your pool system thru your home automation package, or just read the states of various components?

    • Yea, I took Xively offline to debug a lockup that plagues me every few days. I just can’t catch it but I know it’s related to the HTTP calls. The last thing I see is the bus chatter that occurs right before I pump to Xively. Since I disabled it no problems. Thought it was Xively so I wrote an HTTP page that meta refreshes every five seconds to hammer the Ethernet shield and it, too locks up every few days. I now suspect it’s a char read race condition between the RS-485 input and HTTP/Xively calls.

      My next plan is to pipe the vars to an rasPi or another Arduino and use that to update Xively.

      Yes, I can control it through HA. The lines 337 to 694 are all HTTP input that look at the URL as a GET and take some action. Some just return JSON or HTTP formatted output, others pump bytes onto the RS-485 bus emulating the wireless remote unit bus address to execute a command. I have my HA set so I can toggle the light on/off, pump on/off, in-floor cleaner heads on/off, waterfall on/off and change the salt chlorinator output percent. Technically they are just HTTP GET calls so they could come from browser but the HA bundles it nicely enough.

      The code also sends updates to the HA unit whenever veraUpdatePending == true or another variable changes like pump on or off, etc…, so the data flows two-way. Lines 991 to 1025 do this via the functions at lines 265 or 295, depending on the update type needed.

      And about that janky code. Some things just haunt me, like inexplicable logic. I just gave up debugging because this is MY time, and I’m not being paid. Same goes for the debug var. When it’s false for some reason the chlorinator setpoint query is sent but I don’t catch the resulting bytes, so I just leave debug = true now. F it.

    • A year later I finally fixed my Xively feed. I ended up passing the Xively update task off to a RasPi via a Python script. It works fine. I actually fixed this months ago but left the Pi on DHCP and had the Arduino pointing to the IP…. which changed. Lol.

  2. Nick Stone

    I am looking at doing something similar with my home automation system and appreciate you sharing the work you have done to reverse engineer pentair’s protocol.

    Thinking back to my system (intellitouch with ic40) the reason that you may not be able to pick up the salinity is that the system only sends the value once shortly after the salt cell is powered on. When my pump starts and the cell powers on the lights on the cell blink for a couple seconds before it indicates if the salinity is good/low/high and this may be the one and only time that it shares the water salinity levels with the intellitouch panel. I believe it only happens once is that I have never seen the salinity value change while the pump is running, even when I add 2-3 bags of salt after a heavy rain. It is only after a power cycle that it registers the added salt to the pool water.

    Once my hardware comes in (Arduino MKR1000 & rs485ttl) I’ll see if I can sniff this out and share the packet information.

    • Nick,
      Thanks for your suggestion about when the IC-40 sends it’s salinity reading. I never considered that it only sends it once per duty cycle. Since the bus is so chatty I kind of just assumed that it must be a periodic broadcast. I’ve taken two separate and well-intentioned sessions at looking for the data to no avail (and now I may know why!). Hopefully there is a query command that would allow you to programmatically query the salinity at a given time. I’ll try sniffing while idle and then transition to pump on to see what goes on the wire.

      Thanks,
      Jason

      • Charlie

        Jason / Nick,
        I have an Intellichlor IC-40 only system that I would like to be able to talk to on the RS485 bus. I saw some comment about needing to send a command to get your pump to start chatting, is this also true for the Intellichlor? If so, any chance you could sniff out what that command is…? I have tried sending it garbage or just listening for traffic, but have not had any success. This is really great work by the way!

        • Charlie,
          The IC-40 is a different animal for sure. If you didn’t yet read this post here, you might take a look. It explains the IC-40 parts I’ve been able to decode. In short each message opens with 10 02 and terminates with 10 03, so they’re easy to sniff out on the wire.

          I’m not sure about the unit requiring a “hello” message to wake up but since it magically comes to life when the pump starts there may be something to that (although I wonder how it would work in the absence of the EasyTouch panel).

      • Nick Stone

        Jason,
        I was able to capture the cross talk that occurs when a screenlogic2 protocol adapter is powered up in hopes that I could sniff out the salinity levels to no avail. I am begining to wonder if the salinity is included in the usual Intellichlor broadcast but is sent as a percentage of a known range instead of having a high value & low value like the other systems. This would make sense as the value is always rounded. I’ll keep digging, but apart from requesting User created names for features from the Easytouch I didn’t make any ground breaking discoveries.

        I would be more than happy to share the captured log if you want to try and dissect some of the communication.

        • Nick,
          Yes please share what you can. I think you might be on to something with the percent of a known constant… I just cannot extract anything usable for salinity from the chatter. I do see values change; however I can never correlate them to anything that I read on the panel or remote. Since I don’t have the ScreenLogic2 unit so hopefully there is something new that I can piece together.

          FWIW here’s what I’ve seen in the IC-40 chatter frames.

          • Nick Stone

            Jason, please email me at nstone97@ h o t m a i l .com and I’ll send over the files. I have them in a raw hex text dump, one packet per line, and an Excel spreadsheet all aligned.

            Also, I’m pretty sure I found the salinity in the chatter; data byte 4 in the “Intellichlor–40” message.

            Hex Dec Dec*50 Displayed Salinity
            45 69 3450 3450 From my 485 sniffing
            46 70 3500 3500 From my 485 sniffing
            4B 75 3750 3750 From your decoding page
            4C 76 3800 3800 From your decoding page

            The only anomaly is the first line on your decoding page where you have 49 in the 4th data byte with a noted salinity of 3750 – 49 -> 73 (Dec) * 50 -> 3650. I’m hoping that was a typo on your page as all the other values match exactly.

            Also, I am using a Feather M0 Wifi as the controller, so I plan to setup a telnet server on it so I can sniff packets at any time. If there is anything you want me to try to sniff out, let me know and I’ll send over the dumps.

          • Nick, I went back and looked at my raw files and in that chunk of output I definitely noted 3750. I’m much more optimistic about your x50 multiplier discovery and would certainly mine is likely a typo. I will add a parsing routine to extract that byte and log it to Xively so I can see what it does over a few days time. I’ll email you as well as I’d like to read the output you’re seeing.

            I also would note that the byte you discovered in the long “Intellichlor–40” message *may* also be linked to the longer 10 02 / 10 03 message. The one you found is contained in a panel broadcast frame (10 to 0F) but it also looks like in the IC-40 longer frame is where the IC-40 tells the panel the reading it has detected Ex. 10 02 0 12 4B 80 EF 10 03. So this may be the source of the message and the panel must just be shouting it on the bus.

            So the short 10 02 / 10 03 message outputs the active utilization percentage (on/off cycling %) and the longer looks to include the salinity reading. Mystery looks like it’s probably solved. Nice work!

          • Nick Stone

            Jason,
            I got a request today for my modified code to snoop the RS485 bus using MQTT as the backend. Since the majority of the RS485 code came from you I wanted to get your permission before posting the data. I was thinking about uploading it to Github for long term sharing.

            Thanks
            Nick

  3. Brent

    Jason,

    Excellent work! I am trying to adapt your code for use with an ISY-994i HA system to my Intellitouch VF system. I am able to read all modes and features (13th buffer of bytes with my system) from my Intellitouch but I cannot make the system respond to commands I send over the bus. I am using an UNO with software serial and I suspect maybe that’s the issue. I cannot get the MEGA to talk over Serial1 for some reason. How do you have that hooked up to the dfrobot io board? You need to wire it up right? Since the shield does not connect to 18 and 19 on the Mega.

    Thanks,
    Brent

    • Hi Brent, yes it needs to be jumpered down to the Mega. Because the DFRobot board pushes the serial in/out pins 0 & 1 and I wanted them to come into the Mega on 18 & 19 I just folded the headers under so they don’t seat in the Mega, then I jumpered out the top of the DFRobot board to 18 & 19. So the DFRobot board seats but the serial output is looped around bypassing the header pins completely. I didn’t use software serial so I don’t know if that’s a factor or not.

      Regarding the writeback to the panel, it’s hit and miss. I think in the code you’ll see I post the command several times. Since the bus is so chatty I think that was a problem I was facing early on. Since Pentair uses discreet on and off commands you can send it a few times and it usually works. Make sure to take the line high before you send otherwise you’re still “listening”.

      I’m going to post my newer code which though it still has Xively traces in it the call to post to Xively is remm’d out. I’ve found that there just is no way to make the Xively post from the Arduino reliably stay up much more than a day. Instead I now make an HTTP /GET call to a Python script that handles the post to Xively. Much more reliable, isn’t wire-tethered over i2c, doesn’t require level shifters for serial and just plain works. When I get a month of uptime I will strip out the Xively traces and just go with the Python script to do the lifting. BTW, the Python script is on a RasPi running Bottle which without doubt one of the simplest web servers I’ve seen.

      Jason

      • Brent

        Jason,

        Thanks for the quick reply! I will give both suggestions a try. Have you considered using an ESP8266 wifi module? That’s what I will be using for communication since I don’t want to drill a hole in the house to run ethernet outside and they are only like $5.00! I still have a lot to learn with the ESP8266 and I even think it might be able to handle reading the RS-485 bus directly bypassing the need for the Arduino altogether. That’s where I am stuck too, trying to get the ESP8266 to talk to the Arduino over serial. The Arduino IDE will even load sketches to it directly.

        • Brent,
          I just learned about those recently. I was considering the Particle Photon because it can be programmed via the web but I had a Mega just sitting idle so my buy-in was the DFRobot shield. I drilled the house into my nerd closet and buried a conduit to the Pentair panel before we put pavers down the side of the house. So now I have some CAT-3 & CAT-5 outside that I can get to if I add something weather-related. Maybe a pH monitor that I can read into the Arduino, too.

          Jason

    • Hi Brent, yes it needs to be jumpered down to the Mega. Because the DFRobot board pushes the serial in/out pins 0 & 1 and I wanted them to come into the Mega on 18 & 19 I just folded the headers under so they don’t seat in the Mega, then I jumpered out the top of the DFRobot board to 18 & 19. So the DFRobot board seats but the serial output is looped around bypassing the header pins completely. I didn’t use software serial so I don’t know if that’s a factor or not.

      Regarding the writeback to the panel, it’s hit and miss. I think in the code you’ll see I post the command several times. Since the bus is so chatty I think that was a problem I was facing early on. Since Pentair uses discreet on and off commands you can send it a few times and it usually works. Make sure to take the line high before you send otherwise you’re still “listening”.

      I’m going to post my newer code which though it still has Xively traces in it the call to post to Xively is remm’d out. I’ve found that there just is no way to make the Xively post from the Arduino reliably stay up much more than a day. Instead I now make an HTTP /GET call to a Python script that handles the post to Xively. Much more reliable. When I get a month of uptime I will strip out the Xively traces and just go with the Python script to do the lifting. BTW, the Python script is on a RasPi running Bottle which without doubt one of the simplest web servers I’ve seen.

      Jason

Leave a Comment

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