John R McAlpine V Mac
Published © GPL3+

Hot Tub Time Machine

Smart, internet connected hot tub control system. Increase energy efficiency and convenience through better control and logic.

AdvancedFull instructions providedOver 1 day2,114
Hot Tub Time Machine

Things used in this project

Hardware components

Photon
Particle Photon
×1
Amazon Echo
Amazon Alexa Amazon Echo
×1
solid state relay, 100A
×7

Software apps and online services

ControlEverything.com mobicle.io

Hand tools and fabrication machines

Soldering iron (generic)
Soldering iron (generic)

Story

Read more

Schematics

Hot Tub Time Machine schematics

Code

Hot Tub Time Machine Control System

Arduino
// This #include statement was automatically added by the Particle IDE.
#include "Adafruit_DHT/Adafruit_DHT.h"
#include "elapsedMillis/elapsedMillis.h"
#include "math.h"

 /*                                    +-----+
 *                          +----------| USB |----------+
 *                          |          +-----+       *  |
 *                          | [ ] VIN           3V3 [ ] |
 *                          | [ ] GND           RST [ ] |
 *                          | [ ] TX           VBAT [ ] |
 *                          | [ ] RX  [S]   [R] GND [ ] |
 *                          | [ ] WKP            D7 [*] |<- XRelay
 *                          | [ ] DAC +-------+  D6 [*] |<- HeaterPumpRelay
 *                          | [ ] A5  |   *   |  D5 [*] |<- AirRelay
 *                          | [ ] A4  |Photon |  D4 [*] |<- Pump3Relay
 *              DHT22 Sensor| [ ] A3  |       |  D3 [*] |<- Pump2Relay
 *           Current Trans->| [ ] A2  +-------+  D2 [*] |<- Pump1Relay 
 *10K Pullup+TempOutHeater->| [*] A1             D1 [*] |<- HeaterRelay
 *10K Pullup+TempInHeater ->| [*] A0             D0 [ ] |
 *                          |                           |
 *                           \    []         [______]  /
 *                            \_______________________/
 *
 *
 */


# define tempoutheaterpin A1
# define tempinheaterpin    A0
# define xrelaypin          D7
# define heaterpumprelaypin D6
# define airrelaypin        D5
# define pump3relaypin      D4
# define pump2relaypin      D3
# define pump1relaypin      D2
# define heaterrelaypin     D1
# define sensoroverheat     125

# define ctpin              A2
# define ctzero             2048


# define ctcal              0.069042969 //141.4A/2048counts
# define biasresistortempin     10000
# define biasresistortempout    9775

# define tempfilt           4

# define maxsettemp 104
# define minsettemp 38
# define heatercheckinterval 30000
# define houroftime         3600000

//--------------Begin DHT------------------
// Example testing sketch for various DHT humidity/temperature sensors
// Written by ladyada, public domain

#define DHTPIN A3     // what pin we're connected to

// Uncomment whatever type you're using!
//#define DHTTYPE DHT11		// DHT 11 
#define DHTTYPE DHT22		// DHT 22 (AM2302)
//#define DHTTYPE DHT21		// DHT 21 (AM2301)

// Connect pin 1 (on the left) of the sensor to +5V
// Connect pin 2 of the sensor to whatever your DHTPIN is
// Connect pin 4 (on the right) of the sensor to GROUND
// Connect a 10K resistor from pin 2 (data) to pin 1 (power) of the sensor

DHT dht(DHTPIN, DHTTYPE);
//---------------End DHT-------------------



unsigned int interval = 100;
unsigned int particleinterval = 100;
unsigned int particlefuncinterval = 10000;
unsigned int heatinterval = 5000;
unsigned int dhtinterval = 30000;
//unsigned long initialsleepinterval = 21600000;
unsigned long initialsleepinterval = houroftime*3;
unsigned long sleepdurationon = houroftime*3;
unsigned long sleepdurationoff = houroftime*9;

elapsedMillis timeElapsed; //declare global if you don't want it reset every time loop runs
elapsedMillis timeElapsed2; //declare global if you don't want it reset every time loop runs
elapsedMillis timeElapsed3;
elapsedMillis timeElapsed4;
elapsedMillis timeElapsed5;
elapsedMillis timesleeptimer;

elapsedMillis timeheatercheck;

int hotTubFunction(String command);

int minsafetytemp = 40; //farenheit minimum safety temp.  Turns on pump and heat to prevent freezing

int rssival = 0;
bool heaterenable = FALSE;
bool circenable = FALSE;
double sleep = 0;

double settemp = 0;
double temphtrin = 0;
double lasttemphtrin = 0;
double temphtrout = 0;
double lasttemphtrout = 0;
double watts = 0;
int maxcurrent = 0;
int instcurrent = 0;
double rmscurrent = 0;

double htelecbxtemp = 0;
double htelecbxhumd = 0;
   


char publishString[40];
char tubpublishstring[40];
char tubstatusstring[40];

SYSTEM_MODE(SEMI_AUTOMATIC); //allows for control of Spark.connect() Code runs even if no wifi connection.

// This routine runs only once upon reset
void setup() {
    //timeElapsed = 0;
    Serial1.begin(230400);   // open serial over USB
    //ip = {"IP:", WiFi.localIP()};
    //String ip = WiFi.localIP();

    Particle.function("hottub", hotTubFunction);
    Particle.variable("settemp", &settemp, DOUBLE);
    Particle.variable("temphtrin", &temphtrin, DOUBLE);  //variables update automatically. Max 4 per particle
    Particle.variable("temphtrout", &temphtrout, DOUBLE);  //variables update automatically. Max 4 per particle
    Particle.variable("tubstatus", tubstatusstring, STRING); //variables are viewable on mobicle
    Particle.variable("sleep", &sleep, DOUBLE);
    Particle.variable("watts", &watts, DOUBLE);
    //Particle.variable("htelecbxtemp", &htelecbxtemp, DOUBLE);
    //Particle.variable("htelecbxhumd", &htelecbxhumd, DOUBLE);
    

    pinMode(tempoutheaterpin, INPUT);
    pinMode(tempinheaterpin, INPUT);
    pinMode(ctpin, INPUT);
    
    pinMode(xrelaypin, OUTPUT);
    pinMode(heaterpumprelaypin, OUTPUT);
    pinMode(airrelaypin, OUTPUT);
    pinMode(pump3relaypin, OUTPUT);
    pinMode(pump2relaypin, OUTPUT);
    pinMode(pump1relaypin, OUTPUT);
    pinMode(heaterrelaypin, OUTPUT);
    
    digitalWrite(xrelaypin, LOW);
    digitalWrite(heaterpumprelaypin, LOW);
    digitalWrite(airrelaypin, LOW);
    digitalWrite(pump3relaypin, LOW);
    digitalWrite(pump2relaypin, LOW);
    digitalWrite(pump1relaypin, LOW);
    digitalWrite(heaterrelaypin, LOW);
    //WiFi.selectAntenna(ANT_AUTO);
    //WiFi.selectAntenna(ANT_INTERNAL);
    WiFi.selectAntenna(ANT_EXTERNAL);
    
    //Do DHTINIT
    pinMode(DHTPIN, INPUT); //sensor needs pullup resistor.
	dht.begin();
	//End DHTINIT
    
}

void loop() {
    if (Particle.connected() == false) {
        Particle.connect();
    }
            instcurrent = analogRead(ctpin)-ctzero;
            maxcurrent = max(maxcurrent,instcurrent);
            
            //analog1raw = analogRead(A0);
            //analog1 = (analog1 *63 +analog1raw *3.3/4095)/64;
            double temphtrdelta;
            
            if (timeElapsed> interval) //sets up interval timer and fires on interval 50ms
            {  
            rmscurrent = 0.707*maxcurrent*ctcal;
            maxcurrent = 0;
            
            watts = 240*rmscurrent;
            //watts = instcurrent;
            
            int tempincounts = analogRead(tempinheaterpin);
            int tempoutcounts = analogRead(tempoutheaterpin);
            
            temphtrin = (lasttemphtrin*tempfilt+thermistor(tempincounts, biasresistortempin))/(tempfilt+1);
            lasttemphtrin = temphtrin;
            
            temphtrout = (lasttemphtrout*tempfilt+thermistor(tempoutcounts, biasresistortempout))/(tempfilt+1);
            lasttemphtrout = temphtrout;
            
            temphtrdelta = temphtrout - temphtrin;
            
            //temphtrin = thermistor(tempincounts, biasresistortempin);
            //temphtrout = thermistor(tempoutcounts, biasresistortempout);            

                        //strcpy(tubpublishstring, "Open4Biz");
                        //Particle.publish("TUBEVENT",tubpublishstring);
                        //strcpy(tubstatusstring, "Open"); //update particle variable.
                        
                timeElapsed = 0;    //resets interval timer.
            }
/*            
            if (timeElapsed2>particleinterval)  //Run code every .1 seconds
            { 
            Particle.process(); //Process wifi events 
            timeElapsed2 = 0;
            }
*/          
            if (timeElapsed3> particlefuncinterval) //Run code every 10 seconds
            {
                rssival = WiFi.RSSI();
                sprintf(publishString,"%d",rssival);
                Particle.publish("RSSI",publishString);
                timeElapsed3 = 0; //reset interval timer
            }

            if (timeElapsed4 > heatinterval) //Run code every 10 seconds
            {
                if ((temphtrin < minsafetytemp) || (temphtrout < minsafetytemp)) { //if temps is too low, trigger heater and pump on for freeze safety
                    heaterenable == TRUE;
                    circenable == TRUE;
                } else {
                    heaterenable == FALSE;
                }
                
                
                if (heaterenable == TRUE){ //if the heater is enabled, proceed
                    if ((temphtrin < settemp)&&(digitalRead(heaterrelaypin) == 0)){
                        timeheatercheck = 0; //catch the edge when the heater is enabled but the output is off and reset the timeheatercheck
                    }
                    if (temphtrin < settemp){ //if the temperature is lower than the set point, turn on the heater
                        digitalWrite(heaterpumprelaypin, HIGH);  //for safety, make sure circ pump is enabled
                        digitalWrite(heaterrelaypin, HIGH);
                    } else {    //if temp to too high, keep circ pump on and turn off heater
                        digitalWrite(heaterpumprelaypin, HIGH);  //for safety make sure circ pump is enabled
                        digitalWrite(heaterrelaypin, LOW);
                         timeheatercheck = 0; 
                    
                    
                    if (timeheatercheck > heatercheckinterval){
                        if ((temphtrdelta < 1.5) || (temphtrin >sensoroverheat) || (temphtrout >sensoroverheat)) {
                            //trigger error and shutdown heater/pump
                            heaterenable == FALSE;
                            circenable == FALSE;
                            strcpy(tubstatusstring, "Heater Error"); 
                        }
                    }
                }
                    
                } else {    //if heater is disabled, turn off heater.
                        digitalWrite(heaterrelaypin, LOW);
                }
                
                timeElapsed4 = 0; //reset interval timer
            }
            
            if (timeElapsed5> dhtinterval) 
            {
                //getDht();
                timeElapsed5 = 0; //reset interval timer
                if (heaterenable == TRUE) {
                    sprintf(publishString,"*H%.1f*",temphtrin);
                } else if (circenable == TRUE){
                    sprintf(publishString,"*%.1f*",temphtrin);
                } else {
                     sprintf(publishString,"%.1f",temphtrin);
                }
                Particle.publish("TUBTEMP",publishString);
            }
            
            //handle sleep events
            unsigned int sleepswitch = (unsigned int)sleep;
            switch (sleepswitch) {
                case 3:  // pump off wait to turn on
                if (timesleeptimer > sleepdurationoff) {
                    timesleeptimer = 0;
                    circenable = TRUE;
                    sleep = 2;
                }
                break;
            
                case 2:  // pump on wait to turn off
                if (timesleeptimer > sleepdurationon) {
                    timesleeptimer = 0;
                    circenable = FALSE;
                    sleep = 3;
                }
                break;
            
                case 1: 
                if (timesleeptimer > initialsleepinterval){
                    timesleeptimer = 0;
                    sleep = 2;
                }
                break;
            }

            
            //handle circenable state
            if (circenable == TRUE){
                digitalWrite(heaterpumprelaypin, HIGH); //turn on pump if circ pump is enabled
            } else {
                digitalWrite(heaterpumprelaypin, LOW);  //turn off pump is circ pump is disabled
            }            
            
            //Heater safety override
            int heatstatus = digitalRead(heaterrelaypin);
            if(heatstatus == HIGH){
                digitalWrite(heaterpumprelaypin, HIGH);    
            }
            //override heater pump to on if heater is on
        
}
    
/*******************************************************************************
 * Function Name  : hotTubFunction
 * Description    : controls the hot tub
 * Input          : 
 * Output         : 
 * Return         : 
                
 *******************************************************************************/
int hotTubFunction(String controlstring)
{
    controlstring = controlstring.toUpperCase();    //force all control chars to uppercase
    strcpy(tubstatusstring, controlstring); //update particle variable.
    
    if(controlstring.startsWith("TEMP"))
	    {
		 sleep = 0;
		 controlstring =  controlstring.substring(4, controlstring.length());
		 settemp = controlstring.toFloat();
		 settemp = min(settemp, maxsettemp);
		 settemp = max(settemp, minsettemp);
		 heaterenable = TRUE;
		 circenable = TRUE;
		 timeheatercheck = 0;
		 return controlstring.toInt();
		 //settemp = 98.0;
        }
	else if (controlstring.startsWith("P1"))
		{
	        sleep = 0;
	        if (controlstring.endsWith("ON")){
	            digitalWrite(pump1relaypin, HIGH);
	        }
	        else if (controlstring.endsWith("T")){
	            int setpin = !digitalRead(pump1relaypin);
	            digitalWrite(pump1relaypin, setpin);
	            return setpin;
            }
	        else {
	            digitalWrite(pump1relaypin, LOW);
	        } 
	        return 1;
		}
	else if (controlstring.startsWith("P2"))
		{
		    sleep = 0;
		    if (controlstring.endsWith("ON")){
	            digitalWrite(pump2relaypin, HIGH);
	        }
	        else if (controlstring.endsWith("T")){
	            int setpin = !digitalRead(pump2relaypin);
	            digitalWrite(pump2relaypin, setpin);
	            return setpin;
            }
	        else {
	            digitalWrite(pump2relaypin, LOW);
	        }
	        return 2;
        }
	else if (controlstring.startsWith("P3"))
		{
		    sleep = 0;
		    if (controlstring.endsWith("ON")){
	            digitalWrite(pump3relaypin, HIGH);
	        }
	        else if (controlstring.endsWith("T")){
	            int setpin = !digitalRead(pump3relaypin);
	            digitalWrite(pump3relaypin, setpin);
	            return setpin;
            }
	        else {
	            digitalWrite(pump3relaypin, LOW);
	        }
	        return 3;
		}
	else if (controlstring.startsWith("AIR"))
		{
		    sleep = 0;
		    if (controlstring.endsWith("ON")){
	            digitalWrite(airrelaypin, HIGH);
	        }
	        else if (controlstring.endsWith("T")){
	            int setpin = !digitalRead(airrelaypin);
	            digitalWrite(airrelaypin, setpin);
	            return setpin;
            }
	        else {
	            digitalWrite(airrelaypin, LOW);
	        }
	        return 4;
		}
	else if (controlstring.startsWith("CIRC"))
		{
		    sleep = 0;
		    if (controlstring.endsWith("ON")){
                circenable = TRUE;
            }
            else if (controlstring.endsWith("T")){
	            circenable = !circenable;
	            heaterenable = FALSE;
	            return circenable;
            }

	        else {
	            circenable = FALSE;
	            heaterenable = FALSE;
	        }
	        return 5;
		}
	else if (controlstring.startsWith("O3"))
		{
		    sleep = 0;
		    if (controlstring.endsWith("ON")){
	            digitalWrite(xrelaypin, HIGH);
	        }
	        else if (controlstring.endsWith("T")){
	            int setpin = !digitalRead(xrelaypin);
	            digitalWrite(xrelaypin, setpin);
	            return setpin;
            }
	        else {
	            digitalWrite(xrelaypin, LOW);
	        }
	        return 6;
		}
	
	else if (controlstring.startsWith("HEAT"))
		{
		    sleep = 0;
		    if (controlstring.endsWith("ON")){
    	        heaterenable = TRUE;
    	        circenable = TRUE;
    	        timeheatercheck = 0;
	        }
	        else if (controlstring.endsWith("T")){
	            heaterenable = !heaterenable;
	            circenable = heaterenable;
	            timeheatercheck = 0;
	            return heaterenable;
            }

	        else {
                heaterenable = FALSE;
	        }
	        return 7;
		}
	
	else if (controlstring.startsWith("FUN"))
		{
            sleep = 0;
            controlstring =  controlstring.substring(3, controlstring.length());
		    float tempsettemp = controlstring.toFloat();
		    if (tempsettemp > 0){
		        settemp = tempsettemp;
		    }
		    settemp = min(settemp, maxsettemp);
		    settemp = max(settemp, minsettemp);
		    
            heaterenable = TRUE;
            timeheatercheck = 0;
            circenable = TRUE;
		    digitalWrite(xrelaypin, HIGH);
            digitalWrite(airrelaypin, HIGH);
            digitalWrite(pump3relaypin, HIGH);
            digitalWrite(pump2relaypin, HIGH);
            digitalWrite(pump1relaypin, LOW);
    		return controlstring.toInt();

		}		

	else if (controlstring.startsWith("SLEEP"))
		{
            heaterenable = FALSE;
            settemp = 0;
            circenable = TRUE;
            sleep = 1;
            timesleeptimer = 0;
            digitalWrite(heaterpumprelaypin, HIGH);
		    digitalWrite(xrelaypin, LOW);
            digitalWrite(airrelaypin, LOW);
            digitalWrite(pump3relaypin, LOW);
            digitalWrite(pump2relaypin, LOW);
            digitalWrite(pump1relaypin, LOW);
            digitalWrite(heaterrelaypin, LOW);
            return 9;
		}

	else if (controlstring.startsWith("OFF"))   //command shuts off all outputs.
		{
            heaterenable = FALSE;
            circenable = FALSE;
		    sleep = 0;
		    digitalWrite(xrelaypin, LOW);
            digitalWrite(heaterpumprelaypin, LOW);
            digitalWrite(airrelaypin, LOW);
            digitalWrite(pump3relaypin, LOW);
            digitalWrite(pump2relaypin, LOW);
            digitalWrite(pump1relaypin, LOW);
            digitalWrite(heaterrelaypin, LOW);
		}

	return 99;
	
}


double thermistor(int RawADC, float biasresistor) {
 double Temp;
 //Temp = log(10000.0*((4096.0/RawADC-1))); 
 Temp =log(biasresistor/(4096.0/RawADC-1)); // for pull-up configuration
 Temp = 1 / (0.001129148 + (0.000234125 + (0.0000000876741 * Temp * Temp ))* Temp );
 Temp = Temp - 273.15;            // Convert Kelvin to Celcius
 Temp = (Temp * 9.0)/ 5.0 + 32.0; // Convert Celcius to Fahrenheit
 return Temp;
}



/*******************************************************************************
 * Function Name  : getDht
 * Description    : gets temp and humidity
 * Input          : 
 * Output         : 
 * Return         : void
                    
 *******************************************************************************/
void getDht() {
// Wait at least 2 seconds between measurements.
	

// Reading temperature or humidity takes about 250 milliseconds!
// Sensor readings may also be up to 2 seconds 'old' (its a 
// very slow sensor)
	float h = dht.getHumidity();
// Read temperature as Celsius
	float t = dht.getTempCelcius();
// Read temperature as Farenheit
	float f = dht.getTempFarenheit();
  
   htelecbxtemp = f; //updates spark variable with current temp.
   htelecbxhumd = h;
   
// Check if any reads failed and exit early (to try again).
	if (isnan(h) || isnan(t) || isnan(f)) {
		Serial.println("Failed to read from DHT sensor!");
		return;
	}

// Compute heat index
// Must send in temp in Fahrenheit!
	float hi = dht.getHeatIndex();
	float dp = dht.getDewPoint();
	float k = dht.getTempKelvin();

	Serial.print("Humid: "); 
	Serial.print(h);
	Serial.print("% - ");
	Serial.print("Temp: "); 
	Serial.print(t);
	Serial.print("*C ");
	Serial.print(f);
	Serial.print("*F ");
	Serial.print(k);
	Serial.print("*K - ");
	Serial.print("DewP: ");
	Serial.print(dp);
	Serial.print("*C - ");
	Serial.print("HeatI: ");
	Serial.print(hi);
	Serial.println("*C");
	Serial.println(Time.timeStr());
}

Credits

John R McAlpine V Mac

John R McAlpine V Mac

17 projects • 87 followers
www.MACSBOOST.com Assistant Teaching Professor at UNC Charlotte MEGR3171 Instrumentation, Motorsports Research

Comments