-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathPoolController_mqtt.ino
549 lines (404 loc) · 18.4 KB
/
PoolController_mqtt.ino
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
/*Greg's Pool Controller
This controller is for my swimming pool.
This is about the 5th version/rewrite of the code. It’s the first project I started on about 2009...and actually what introduced me to the wonderful world of Arduino.
It’s a mess, I’ve borrowed code from all edges of the internet (thanks everyone!), its likely full of bugs and highly un-optimised....but it works well enough ;-)
Operational Functions:
Solar pool heating - this measures the roof and pool water temperature and if it’s swimming season and its hot enough then it will switch on the pumps and heat the pool
The controller can also monitor the pH level and run a peristaltic pump to add HCL to the pool.
It has a level of intelligence on when to operate the pumps and chlorinator so as to save power. During summer months the filter needs to operate for approx. 8hrs a day
but in winter, just 4hrs is enough. So during summer, if the pool runs for 2hrs to heat the pool during the day, then it will wait until off peak power time begins, and run the remaining 6hrs then.
During winter, it will only run the pumps during off peak hours.
There is a button on the controller to allow the pump to be stopped/started if required - to backwash for instance.
This version talks MQTT allowing it to be controlled from Openhab. It reports back the status, temperatures, pH level, pump runtimes as well as being able to be controlled via MQTT.
Control functions are pump start/stop (like the button allows), set the desired water temperature, how long in minutes the acid feeder should run each hour (as needed to keep pH in check)
Whilst the Arduino has an accurate DS1307 RTC, it will also pull the time from Openhab - this function was initially only so as to adjust for when daylight savings started/stopped...but I sync it more frequently now.
There is a 20x4 LCD display to show Time, Status, Roof/Water temps, pH as well as runtimes for the filter pump and acid pump.
For communications to MQTT, I’m now using the excellent MySensors system ( mysensors.org ), I was using an ethernet Arduino and an old Wi-Fi router in bridge mode, but the bridge was unreliable.
MySensors has been rock solid on this for 18months or so. There are references to Vera - where I was using MySensors to interface it to Vera, but no more...now MQTT and Openhab.
Hardware:
Arduino Nano running on Optiboot - at some point I was reallllly tight for flash!
This sits on a Nano IO shield - makes radio and I/O connections really easy.
There is a RTC and LCD on the I2C bus.
Manual mode button
It’s got a piezo beeper to alert when it’s in manual mode - so I don’t forget.
It’s got 5 SSR relays - filter pump, chlorinator, solar pump, acid pump and one for the pool lights.
*/
/*
Todo:
- Interupt function for the pH tuning
- Different Beep Types - Error - frequent beeping ( lost temp sensor)
- ph calibration values - set via controller/service port to get voltages etc.
*/
#define TESTBED 1 //Set to 1 for benchtest hardware - different id, parameters and Temp sensor addresses.
#define PHCAL 0 // for calibration routine - not currently working.
#if TESTBED == 1
#define MY_DEBUG // Enable debug prints to serial monitor for mySensors library.
#define MY_NODE_ID 4 //Set the MySensors NODE id... static cause i use Openhab/MQTT.
#define RADIO_CH 96 // Sets MYSensors to use Radio Channel 96
#endif
#if TESTBED == 0
#define MY_DEBUG // Enable debug prints to serial monitor for mySensors library.
#define MY_NODE_ID 6 //Set the MySensors NODE id... static cause i use Openhab/MQTT.
#define RADIO_CH 96 // Sets MYSensors to use Radio Channel 96
#endif
//Set the error/tx/rx pins to 20 - i dont need them.
#define MY_DEFAULT_TX_LED_PIN 20
#define MY_DEFAULT_RX_LED_PIN 20
#define MY_DEFAULT_ERR_LED_PIN 20
// Enable and select radio type attached
#define MY_RADIO_NRF24
#include <SPI.h>
#include <MySensor.h>
#include <DallasTemperature.h>
#include <OneWire.h>
#include <DS1307RTC.h> //for RTC
#include <bv4208.h> //for LCD
#include <I2c_bv.h> //for LCD
#include <Time.h> //for RTC
#include <Wire.h> //for LCD and RTC
// Connections to Arduino
//SDA = A4
//SCL = A5
//Radio 9,10,13,11,12,2 , A0=14 A1=15 A2=16 A3=17 A4=18 A5=19 A6=20
#define ONE_WIRE_BUS 4 // Dallas Temp Sensors on D4
#define phPin 16 // analog pH probe sensor on A2
#define beepPin 17 // Piezo Beeper - A3
#define solarpumpPin 5 // Solar Pump SSR
#define acidpumpPin 6 // Acid Pump SSR
#define filterpumpPin 7 // Filter Pump SSR
#define chlorinatorPin 8 // Chlorinator SSR
#define remotePin 3 //this is D3 which Interrupt 1 for the Overide/Manual button
#define lightPin 14 //pin A0 for the Pool Light SSR
#define SPAREPin 15 //SPARE pin A1 WIRED TO CONNECTOR PIN7
//set parameters for Overide Remote
boolean remoteIsOn = FALSE;
int remoteMode = 1;
//time object and hardcode metric
//boolean metric = true; // not sure i need this now with mQTT
tmElements_t tm;
//Set LCD Address
BV4208 lcd(0x21);
// Setup a oneWire instance to communicate with any OneWire devices (not just Maxim/Dallas temperature ICs)
OneWire oneWire(ONE_WIRE_BUS);
// Pass our oneWire reference to Dallas Temperature.
DallasTemperature dallasSensors(&oneWire);
//set Testing vs Productions variables and settings
#if TESTBED == 1
char sketch_name[] = "Pool Controller-TESTBED";
char sketch_ver[] = "2016.02.22b1";
int desiredPoolTemp = 31; //set initial desired/max pool temperature - this can be overwritten by the controller
int differential = 2; // Set the differential to switch on the pumps. In production this should be about 15, on testbed 2
int acidpumpOnTime = 5; //default minutes on the hour to run the acid feeder for - this can be overwritten by the controller
int veraRequestFreq = 30; //how many cycles to ask vera for data
#define SUMMERMAXPUMPRUNTIME 25000 // 28800 seconds = 8 hours, 36000 = 10 HOURS
#define WINTERMAXPUMPRUNTIME 14400 // 4 Hours only during winter. - Winter could also be enabled when pooltemp drops to 18deg
// Set Temp Sensor addresses - i use the ds18b example sketch to identify the addresses.
DeviceAddress roofTsensor = {
0x28, 0x61, 0x17, 0x1B, 0x04, 0x00, 0x00, 0xF5 };
DeviceAddress poolTsensor = {
0x28, 0xEB, 0x68, 0xE3, 0x02, 0x00, 0x00, 0x6E };
#endif
#if TESTBED == 0
char sketch_name[] = "Pool Controller";
char sketch_ver[] = "2016.03.01r1";
int desiredPoolTemp = 27; //set initial desired/max pool temperature - this can be overwritten by the controller
int differential = 15; // Set the differential to switch on the pumps. In Production this should be about 15, on testbed 0
int acidpumpOnTime = 05; //how many minutes on the hour to run the acid feeder for.
int veraRequestFreq = 60; //how many cycles to ask vera for data
#define SUMMERMAXPUMPRUNTIME 28800 // 28800 seconds = 8 hours, 36000 = 10 HOURS
#define WINTERMAXPUMPRUNTIME 14400 // 4 Hours only during winter. - Winter could also be enabled when pooltemp drops to 18deg
// Set Temp Sensor addresses
DeviceAddress roofTsensor = {
0x28, 0x76, 0x30, 0xE3, 0x02, 0x00, 0x00, 0x3E };
DeviceAddress poolTsensor = {
0x28, 0xBE, 0xFF, 0x0C, 0x02, 0x00, 0x00, 0x9E };
#endif
boolean filterpumpRunning = false;
boolean solarpumpRunning = false;
boolean veraRemote = false;
boolean timeReceived = false;
int maxPumpRunTime;
int veraLoopCount = 0; //counter for Vera update period
int currentMonth;
int currentHour;
int currentMinute;
int currentDay;
int currentSec;
float idealpH = 7.30; // set the ideal pH level for the pool
float acidpumpRunTime = 0;
int SolarStartcount = 0;
int SolarStartDelay = 6; // delay for solar pump startup
int ItsWinter = 0;
unsigned long yesterdaysPRT;
unsigned long pumpRunTime = 0;
unsigned long loopTime;
unsigned long loopstartmillis = 0;
long lastPumpstart = 0L;
long lastPumpstop = 50000L; // give it 50 secs for initial starting.
long huristicdelay = 60000L; //~60 sec delay between pump restarts
float lastroofTemp = 0;
float lastpoolTemp = 0;
float lastpHval = 0;
int lastpTime = 0;
int lastacidTime = 0;
int pTime ;
int bTime = 0;
int beepState = LOW;
long pBeepMillis = 0;
//pH Settings - change when calibrating
//would be good to calibrate from vera
float volt4 = 3.341;
float volt7 = 2.304;
float calibrationTempC = 31.1;
float avgMeasuredPH = 0;
// Array of Status codes 0-9
char* strStatus[]= {
"OFFPEAK","MAX PRT","POOLMAXTEMP","HEATING", "DELAY", "NO SUN", "READY", "ERR-SHIT","REMOTE ON","WINTER"};
int statuscode; // a number for each of the above 0-9
int lastStatusCode; //used for sending status to Vera
//Define MySensors child devices
#define Roof_CHILD_ID 1 // Id of the sensor child
#define Pool_CHILD_ID 2 // Id of the sensor child
#define pH_CHILD_ID 3 // Id of the sensor child
#define prt_CHILD_ID 4 //id of the PumpRunTime child
#define hcl_CHILD_ID 5 //id of the Acid Pump Runtime child
#define Pump_CHILD_ID 6 //id of the Pump Overide switch child
#define Light_CHILD_ID 7 //id of the Pool Light switch child
#define Controller_CHILD_ID 8 //id for Controller - for sending status.
// Initialise messages
MyMessage roofTempmsg(Roof_CHILD_ID, V_TEMP);
MyMessage poolTempmsg(Pool_CHILD_ID, V_TEMP);
MyMessage desiredPoolTempmsg(Pool_CHILD_ID, V_VAR1);
MyMessage pHValuemsg(pH_CHILD_ID, V_TEMP);
MyMessage desiredpHmsg(pH_CHILD_ID, V_VAR1);
MyMessage prtValuemsg(prt_CHILD_ID, V_TEMP);
MyMessage hclValuemsg(hcl_CHILD_ID, V_TEMP);
MyMessage PumpCntrlmsg(Pump_CHILD_ID, V_STATUS);
MyMessage LightCntrlmsg(Light_CHILD_ID, V_STATUS);
MyMessage ControlModemsg(Controller_CHILD_ID, V_TEXT);
unsigned long lastConnectionTime = 0; // last time you connected to the server, in milliseconds
const unsigned long postingInterval = 30000; // 30s delay between updates to Vera
void presentation() {
// Send the sketch version information to the gateway and Controller
sendSketchInfo(sketch_name, sketch_ver);
//Describe the device type...
present(Roof_CHILD_ID, S_TEMP);
present(Pool_CHILD_ID, S_TEMP);
present(pH_CHILD_ID, S_TEMP);
present(prt_CHILD_ID, S_TEMP);
present(hcl_CHILD_ID, S_TEMP);
present(Pump_CHILD_ID, S_LIGHT);
present(Light_CHILD_ID, S_LIGHT);
present(Controller_CHILD_ID, S_INFO);
}
void setup(void)
{
//Set the RTC as the timekeeper - vera just for sync.
setSyncProvider(RTC.get);
//request data from vera
//gw.request(Pool_CHILD_ID,V_VAR1); // ask vera for pool set temp
requestTime(); //get time in case rtc isnt set. Openhab needs a rule to look for this msg and send it. The vera plugin sent the time automatically.
//Setup LCD
lcd.bl(0); // turn BL on
lcd.cls(); // clear screen
//Set Pin Modes
pinMode (acidpumpPin, OUTPUT);
pinMode (solarpumpPin, OUTPUT);
pinMode (filterpumpPin, OUTPUT);
pinMode (chlorinatorPin, OUTPUT);
pinMode (beepPin, OUTPUT);
pinMode(lightPin, OUTPUT);
pinMode(remotePin,INPUT);
digitalWrite(remotePin,HIGH);
//Create Interrupt routine for Remote on D3
attachInterrupt(1,isrRemote, LOW);
// Start up the Dallas Temp library and set the resolution to 9 bit
dallasSensors.begin();
dallasSensors.setResolution(poolTsensor, 12);
dallasSensors.setResolution(roofTsensor, 12);
// Get temperature values
dallasSensors.requestTemperatures();
float init_poolTemp = dallasSensors.getTempC(poolTsensor);
Serial.println(init_poolTemp);
//End of setup
}
void loop(void)
{
loopstartmillis = millis(); //start a counter for the pump runtimes
veraLoopCount ++; //this counter for requesting data only periodically
//Send data to controller periodically in case it was missed/or they are out of sync
if (veraLoopCount == veraRequestFreq){
send(PumpCntrlmsg.set(filterpumpRunning == true ? 1 : 0)); // Send pump status to Vera
send(desiredpHmsg.set(acidpumpOnTime)); // Send back the current set HCL pump desired runtime
send(desiredPoolTempmsg.set(desiredPoolTemp)); // Send back the current set HCL pump desired runtime
// send(LightCntrlmsg.set(dsssss); //send the current status of the light
veraLoopCount = 0; //reset the counter
}
//Send Status to MQTT
if (lastStatusCode != statuscode){
send(ControlModemsg.set(strStatus[statuscode])); // Send pump status to Vera
lastStatusCode = statuscode;
}
//Sync the time with Vera - typically for DST changes.
if ((currentDay == 0) && (currentHour == 3) && (currentMinute == 01) && (currentSec == 5)) { // Sync time on Sunday at 3:01:05 from Vera - DST times change on Sundays 2/3AM
requestTime();
}
// Get temperature values
dallasSensors.requestTemperatures();
float poolTemp = dallasSensors.getTempC(poolTsensor); //should only really do this when the pump is runnign as the pipes can be different temp to the pool. Would need to set inital value and turn on pump if its set..
float roofTemp = dallasSensors.getTempC(roofTsensor);
//Send metrics to MQTT
if (millis() - lastConnectionTime > postingInterval) {
Serial.println("Publishing data...");
sendData(roofTemp, poolTemp, avgMeasuredPH, pumpRunTime, acidpumpRunTime);
}
//Print Data to LCD
lcd.bl(0); //Ensure Backlight is turned on!
digitalClockDisplay();
printRoofTemp(roofTemp);
printPoolTemp(poolTemp);
printStatus(statuscode);
printAcidtime(acidpumpRunTime);
printPtime(pumpRunTime);
printPHInfo(avgMeasuredPH);
//remote overide control section
//if switch in on then Isremoteonoff=true
//if remote is on toggle a state remotemode
if (IsRemoteOnOff()) {
// Serial.println("Remote Mode");
if (remoteMode == 1) {
}
else if (filterpumpRunning == true) {
//Serial.println("Filter Pump is running... will stop it");
stopFilterPump();
stopSolarPump();
// reset any timers
lastPumpstart = 0;
lastPumpstop = 0;
remoteMode = 1;
}
else if (filterpumpRunning == false) {
//Serial.println("Filter Pump is Stopped... will Start it");
startFilterPump();
lastPumpstart = 0;
lastPumpstop = 0;
remoteMode = 1;
}
doBeep(); //beep beeper to indicate remote is active
}
else {
//Serial.println("Remote is Off");
remoteMode = 0 ;
}
//Check if its Winter or Summer
if (IsItWinter()) {
ItsWinter = 1;
maxPumpRunTime = WINTERMAXPUMPRUNTIME;
}
else {
ItsWinter = 0;
maxPumpRunTime = SUMMERMAXPUMPRUNTIME;
}
// we want to reset the pump runtime counter at 7 am everyday
// we will dedicate the entire 0 minute of 7 am to the reset to ensure we catch it.
if ((currentHour == 7) && (currentMinute == 0)) {
yesterdaysPRT = pumpRunTime;
pumpRunTime = 0;
delay(5000); // rethink about this delay, maybe longer?
return; // don't know if this will break out of the loop()
}
//log amount of acid delivered....
//Begin pH stuff
int x;
int sampleSize = 500;
float avgRoomTempMeasuredPH =0;
float avgTemp = 0;
float avgPHVolts =0;
float avgVoltsPerPH =0;
float phTemp = 0;
float tempAdjusted4 = getTempAdjusted4();
for(x=0;x< sampleSize;x++)
{
float measuredPH = measurePH();
float phTemp = poolTemp;
float roomTempPH = doPHTempCompensation(measuredPH, phTemp);
float phVolt = measurePHVolts();
avgMeasuredPH += measuredPH;
avgRoomTempMeasuredPH += roomTempPH;
avgTemp += phTemp;
avgPHVolts += phVolt;
}
avgMeasuredPH /= sampleSize;
avgRoomTempMeasuredPH /= sampleSize;
avgTemp /= sampleSize;
avgPHVolts /= sampleSize;
// *****Begin pH monitoring section ****
//get the ph and start adding if needed.
//Run the acid pump for a count of X - reset the count every 30mins
//need to add a timer so only runs for 5mins or so each hour.
if ((avgMeasuredPH > idealpH) && ( currentMinute < acidpumpOnTime )) { //Turn on acid pump if needed and first few minutes of the hour.
startAcidPump();
} else {
stopAcidPump(); //Turn off the acid pump if the ph is OK or not the right time.
}
//perhaps fire an alarm if PH really OUT!
// *****Begin Heating and Pump control logic****
if ((pumpRunTime >= maxPumpRunTime) && (remoteMode == 0)){
statuscode = 1; //MaxPumpTime
stopFilterPump();
stopSolarPump(); //this may not be necessary but better to leave here in case pool is heating in OP hours!
return; //not sure what this return does???
}
else if ((IsOffPeak()) && (pumpRunTime < maxPumpRunTime) && (remoteMode == 0)) { // If its Off Peak then run the pump for the remaining required time
startFilterPump();
statuscode = 0; //Off Peak
}
else if ((poolTemp >= (desiredPoolTemp + 0.5)) && (remoteMode == 0)) { // This condition is when the pool has reached MAX temperature. No pumps running during peak hours
statuscode = 4; // heuristic - temporary state until timers expire
if (millis() - lastPumpstart > huristicdelay) { //stop with huristics
stopSolarPump();
stopFilterPump();
statuscode = 2; //Pool Max Temp
}
}
else if ((roofTemp > poolTemp + differential) && (poolTemp < desiredPoolTemp) && (ItsWinter == 0) && (remoteMode == 0)) { // This condition is when the pool will be heated - both pumps running.
statuscode = 4; // heuristic - temporary state until timers expire
if (millis() - lastPumpstop > huristicdelay) { //start with huristics
startFilterPump();
startSolarPump();
statuscode = 3; // HEATING
}
}
else if ((roofTemp <= poolTemp + differential) && (remoteMode == 0)) { //This condition is for when there is not enough solar heat to switch on the pumps.
if (pumpRunTime == 0 ){ //bodge to set initial statuscode
statuscode = 6; // "READY"
}
else if (remoteMode == 0) {
statuscode = 4; // heuristic - temporary state until timers expire
if (millis() - lastPumpstart > huristicdelay) { //stop with huristics
stopFilterPump();
stopSolarPump();
statuscode = 5; //NO SUN
}
}
}
else if (remoteMode == 1){
statuscode = 8; //Remote ON
}
else if (ItsWinter == 1) {
statuscode = 9; // WINTER
}
else {
// if we get to here for whatever reason, we should just stop the pumps
//this will occur if poolTemp>maxpooltemp and roofTemp>pool+differential
// stopFilterPump();
// stopSolarPump();
// statuscode = 7; // ERR-SHIT
statuscode = 4; // heuristic
}
//Generate the Pump Runtime for how long the pump needs to run each day.
if (filterpumpRunning == true){
loopTime = (millis() - loopstartmillis)/1000; //calculate how long the loop took to run
pumpRunTime =(pumpRunTime+loopTime); //calculate the new pumpRunTime value
}
} //end of sketch