Rich Noordam
Created February 15, 2017 © GPL3+

ESP32S Web Based Sprinkler System

Control your Sprinkler system through an easy to set up web based interface.

IntermediateWork in progress4 hours63
ESP32S Web Based Sprinkler System

Things used in this project

Hardware components

Shift Register- Serial to Parallel
Texas Instruments Shift Register- Serial to Parallel
×2
relay board
×1
Jumper wires (generic)
Jumper wires (generic)
×1
SparkFun ESP32 Thing
SparkFun ESP32 Thing
×1
Capacitor 1 µF
Capacitor 1 µF
×1
Resistor 10k ohm
Resistor 10k ohm
×1
Capacitor 10 µF
Capacitor 10 µF
×1
DS3231MPMB1 Peripheral Module
Maxim Integrated DS3231MPMB1 Peripheral Module
×1
Micro SD card deck
Bitcraze Micro SD card deck
×1
OLED Screen
×1

Software apps and online services

Arduino IDE
Arduino IDE

Story

Read more

Schematics

Version 3 Fritz File

Code

Version 3 Arduino Code

Arduino
Testing Code for Version 3.
#include <NTPClient.h>
#include <WiFi.h>
#include <WiFiUdp.h>
#include <ESP32WebServer.h>
#include <Wire.h> 
#include <RtcDS3231.h>

// Relays Zones and schedules
const int intNumOfSchedules = 4;
const int intNumOfZones = 16;
const int intDaysOfWeek = 7;
// WiFi details
const char *ssid     = "YOURSSID";
const char *password = "YOURPASS";
IPAddress local_IP(192, 168, 1, 170);
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet(255, 255, 255, 0);
IPAddress primaryDNS(209,18,47,62);
IPAddress secondaryDNS(209,18,47,61);
// NTP
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "us.pool.ntp.org", -7*3600, 60000);
// RTC variable for DS3231
RtcDS3231<TwoWire> Rtc(Wire);
// Local Web Server Variable
ESP32WebServer server(80);
//##############################################
// Scheduling Data Class and Variables
// Detail: Class used to handle Schedules
class Schedules {
  public: int weekDays[intDaysOfWeek];                  // 1 = Monday
  public: String startTime;             // startTime of zones
  public: int zoneDuration[intNumOfZones];  // int array with durations for each relay
  public: bool isComplete[intNumOfZones];   // has zone ran for day
  public: float waterUsage[intNumOfZones];  // amount of water used (flowmetered) for each relay

  // constructor
  public: Schedules(){}
  public: void Update(int WeekDays[], String StartTime, int ZoneDuration[]){
    for(int i=0; i<intDaysOfWeek; i++){
      weekDays[i] = WeekDays[i];
    }
    startTime = StartTime;
    for(int j=0; j<intNumOfZones; j++){
      zoneDuration[j] = ZoneDuration[j];
    }
    //memset(waterUsage,0,sizeof(waterUsage));
    //memset(isComplete,false,sizeof(isComplete));
  }
};
// *********************************************
// ** Relay Driver
// *********************************************
class RelayDriver
{
  public: int zone;      // zoneID to turn on.
  public: String StartTime; // 
  public: int duration;  // seconds
  public: int isActive;
  public: int isStop;
  public: long onTime;     // milliseconds of on-time
  // base byte array to send bytes to 
  private: byte dataArray[9]; // array  to handle the 74595 chip states
  // set pins for relay control
  private: int SER_Pin = 25;   //pin 14 on the 75HC595 data
  private: int RCLK_Pin = 26;  //pin 12 on the 75HC595 latch
  private: int SRCLK_Pin = 27; //pin 11 on the 75HC595 clk
  // Timing variable
  unsigned long previousMillis;   // will store the last time the relays were updated.
  // Constructor - creates a RelayDriver and initializes the member variables and state
  public: RelayDriver(){
    dataArray[0] = 0x00; //0b00000000 # All Off (sent twice shuts all off
    dataArray[1] = 0x01; //0b00000001 # Position 1 of chip on 
    dataArray[2] = 0x02; //0b00000010 # Position 2 of chip on 
    dataArray[3] = 0x04; //0b00000100 # Position 3 of chip on 
    dataArray[4] = 0x08; //0b00001000 # Position 4 of chip on 
    dataArray[5] = 0x10; //0b00010000 # Position 5 of chip on 
    dataArray[6] = 0x20; //0b00100000 # Position 6 of chip on 
    dataArray[7] = 0x40; //0b01000000 # Position 7 of chip on 
    dataArray[8] = 0x80; //0b10000000 # Position 8 of chip on 
    previousMillis = 0;
    isActive = 0;
    isStop = 0;
    onTime = 0;
    // set pins.    
    pinMode(RCLK_Pin, OUTPUT);
    pinMode(SRCLK_Pin, OUTPUT);
    pinMode(SER_Pin, OUTPUT);
    // write initial state (All Off)
    digitalWrite(RCLK_Pin, 0);
    shiftOut(SER_Pin, SRCLK_Pin, dataArray[0]);
    shiftOut(SER_Pin, SRCLK_Pin, dataArray[0]);
    digitalWrite(RCLK_Pin, 1);
  }
  public: void Update()  // base update to turn everything OFF if running or if timer > the onTime.
  {
    unsigned long currentMillis = millis();
    // is it time to turn off relay?  
//    Serial.println(onTime);
//    Serial.println(currentMillis - previousMillis);
    if (isActive){
      if(currentMillis - previousMillis >= onTime)
      {
        // write initial state (All Off)
        digitalWrite(RCLK_Pin, 0);
        shiftOut(SER_Pin, SRCLK_Pin, dataArray[0]);
        shiftOut(SER_Pin, SRCLK_Pin, dataArray[0]);
        digitalWrite(RCLK_Pin, 1);
        isActive=0;
        duration = 0;
      }
    } else {
      // shut all off always to be safe. (should be off)
      // keeps the 'static' at bay in circuit too, as it resets it often.
      digitalWrite(RCLK_Pin, 0);
      shiftOut(SER_Pin, SRCLK_Pin, dataArray[0]);
      shiftOut(SER_Pin, SRCLK_Pin, dataArray[0]);
      digitalWrite(RCLK_Pin, 1);
      isActive=0;
      duration = 0;
    }
  }
  public: void Update(String inCommand)  // base update to turn everything OFF if running or if timer > the onTime.
  {
    unsigned long currentMillis = millis();
    Serial.print("Command: ");
    Serial.println(inCommand);
    if (inCommand == "STOPALL"){
      // shut all off always to be safe. (should be off)
      // keeps the 'static' at bay in circuit too, as it resets it often.
      digitalWrite(RCLK_Pin, 0);
      shiftOut(SER_Pin, SRCLK_Pin, dataArray[0]);
      shiftOut(SER_Pin, SRCLK_Pin, dataArray[0]);
      digitalWrite(RCLK_Pin, 1);
      isActive=0;
      duration = 0;
      }
  }
  public: void Update(int InZone, int InrunTimeSec) // this update changes the state, and sets timers
  {
    // zone is 2 parts, chip 2 gets sent first.
    // example: all off send 0,0 twice
    // example: position 1, send 0, 1 of array
    // example: position 9, send 1, 0 of array
    Serial.print(" InZone:");
    Serial.print(InZone);
    Serial.print(" InrunTimeSec:");
    Serial.println(InrunTimeSec);
    unsigned long currentMillis = millis();
    if (isActive == 1){ 
      // check to see if stop button has been pushed.
      digitalWrite(RCLK_Pin, 0);
      shiftOut(SER_Pin, SRCLK_Pin, dataArray[0]);
      shiftOut(SER_Pin, SRCLK_Pin, dataArray[0]);
      duration = 0;
      isActive = 0;
      
    } else {
      // is off, turn on and set vals.
      onTime = InrunTimeSec * 1000; 
      duration = InrunTimeSec;
      isActive=1;
      previousMillis = currentMillis;
      zone = InZone;
      digitalWrite(RCLK_Pin, 0);
      if (zone <= 8){ // lower set
        shiftOut(SER_Pin, SRCLK_Pin, dataArray[0]);         // shift second bit out as zeros/OFF
        shiftOut(SER_Pin, SRCLK_Pin, dataArray[zone]);      // shift out zone we want on
      } else { // upper set
        int relayPos = zone - 8;
        shiftOut(SER_Pin, SRCLK_Pin, dataArray[relayPos]);  // shift out zone we want on
        shiftOut(SER_Pin, SRCLK_Pin, dataArray[0]);         // shift second bit out as zeros/OFF
      }
      digitalWrite(RCLK_Pin, 1);
    }
}
  // this does the changing of the 74595 chips.
  void shiftOut(int myDataPin, int myClockPin, byte myDataOut) {
      //internal function setup
    int i=0;
    int pinState;

    //clear everything out just in case to
    //prepare shift register for bit shifting
    digitalWrite(myDataPin, 0);
    digitalWrite(myClockPin, 0);

    //cycle down through byte
    for (i=7; i>=0; i--)  {
      digitalWrite(myClockPin, 0);
      //if the value passed to myDataOut and a bitmask result 
      // true then set pinState to 1.
      if ( myDataOut & (1<<i) ) {
        pinState= 1;
      }
      else {  
        pinState= 0;
      }
      //Sets the pin to HIGH or LOW depending on pinState
      digitalWrite(myDataPin, pinState);
      //register shifts bits on upstroke of clock pin  
      digitalWrite(myClockPin, 1);
      //zero the data pin after shift to prevent bleed through
      digitalWrite(myDataPin, 0);
    }
    //stop shifting
    digitalWrite(myClockPin, 0);
  }
};

// Relay Driver Variable
RelayDriver relayDriver;
// Schedule Array
Schedules arraySchedule[intNumOfSchedules];
void WiFi_Setup(){
  if (!WiFi.config(local_IP, gateway, subnet, primaryDNS, secondaryDNS)) {
    Serial.println("WiFi Failed to configure settings.");
  }
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi connected!");
}

// ************************************
// ** RTC Update
// ************************************
void RTC_Update(){
  // Do udp NTP lookup, epoch time is unix time - subtract the 30 extra yrs (946684800UL) library expects 2000
  timeClient.update();
  unsigned long epochTime = timeClient.getEpochTime()-946684800UL;
  Rtc.SetDateTime(epochTime); 
}
bool RTC_Valid(){
  bool boolCheck = true; 
  if (!Rtc.IsDateTimeValid()){
    Serial.println("RTC lost confidence in the DateTime!  Updating DateTime");
    boolCheck = false;
    RTC_Update();    
  }
  if (!Rtc.GetIsRunning())
  {
    Serial.println("RTC was not actively running, starting now.  Updating DateTime");
    Rtc.SetIsRunning(true);
    boolCheck = false;
    RTC_Update();
  }
}

void dailyCompleteFlagReset(){
  RtcDateTime myCurrentTime = Rtc.GetDateTime();
  String strHour;
  strHour = String(myCurrentTime.Hour());
  int intHour;
  intHour = strHour.toInt();
  // after midnight reset.
  if (intHour == 0){
    for(int i=0; i < intNumOfSchedules; i++){
      for(int j=0; j< intNumOfZones; j++){
        arraySchedule[i].isComplete[j] = false;
      }
    }
  }
}

// ***********************************
// RTC Timecheck functions.  updates once a day unless finds error.
// ***********************************


unsigned long timeCheckLength = 3600000;
int checkHour;
// will store the last time the timestamp was checked.
unsigned long previousTimeCheckMillis;   


void timeCheck(){
  unsigned long currentTimeCheckMillis = millis();
  if(currentTimeCheckMillis - previousTimeCheckMillis >= timeCheckLength){
    checkHour = checkHour + 1;
    Serial.print("Time Check, once per hour. Hour: ");
    Serial.println(checkHour);
    if (checkHour < 23){
      previousTimeCheckMillis = currentTimeCheckMillis;
      //if (!RTC_Valid()){
      //  Serial.println("Time Invalid Update.");
      //  RTC_Update();
      //} else {
      //  Serial.println("Time is Valid, No Update.");
      //}
    } else {
      // run an update once a day.
      Serial.println("Once a day RTC Update");
      RTC_Update();
      checkHour = 0;
      // reset daily schedule flags after midnight
      dailyCompleteFlagReset();
    }
  }//  else {
//    if (!RTC_Valid()){
//      Serial.println("Time Invalid Update.");
//      RTC_Update();
//    }
//  }
}
// RELAY CONTROL
void controlRelay(int zone, int secToRun){
  if (zone != 0){
    relayDriver.Update(zone, secToRun);
  }  
}
void controlRelay(String UpdateString){
  relayDriver.Update(UpdateString);  
}
void controlRelay(){
  relayDriver.Update();  
}
// *********************************
// END RELAY CONTROL
// *********************************

//############################################
//# Web Server handler Coding
//############################################
void handleRoot() {
  // turn led on when request is made
  // Purpose: Show Current Status, and provide links to Zone, and Scheduler 
  String myForm = "<html><head><script src='https://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js'></script><script src='http://code.jquery.com/ui/1.9.2/jquery-ui.js'></script>";
  myForm += "<link rel='stylesheet' href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css' integrity='sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u' crossorigin='anonymous'></head>";
  myForm += "<body></p><FORM action='http://192.168.1.170/Zone'><div style='width:750px'><p>Zone 1:<INPUT type='radio' name='zone' value='1'> Zone 2:<INPUT type='radio' name='zone' value='2'>Zone 3:<INPUT type='radio' name='zone' value='3'>";
  myForm += "Zone 4:<INPUT type='radio' name='zone' value='4'>Zone 5:<INPUT type='radio' name='zone' value='5'>Zone 6:<INPUT type='radio' name='zone' value='6'>Zone 7:<INPUT type='radio' name='zone' value='7'>";
  myForm += "Zone 8:<INPUT type='radio' name='zone' value='8'></p><p>Zone 9:<INPUT type='radio' name='zone' value='9'>Zone 10:<INPUT type='radio' name='zone' value='10'>Zone 11:<INPUT type='radio' name='zone' value='11'>";
  myForm += "Zone 12:<INPUT type='radio' name='zone' value='12'>Zone 13:<INPUT type='radio' name='zone' value='13'>Zone 14:<INPUT type='radio' name='zone' value='14'>Zone 15:<INPUT type='radio' name='zone' value='15'>";
  myForm += "Zone 16:<INPUT type='radio' name='zone' value='16'></p></div><div>Time to Run (sec): <input type='text' name='secToRun'></div><P><INPUT type='submit' value='Submit'></p></FORM></body></html>";
  server.send(200, "text/html", myForm);
}

void handleInput(){
  int zone=0;
  int secToRun=0;
  int isStop=0;
  Serial.println("****handling input start****");
  String inputMessage = "";
  for (uint8_t i=0; i<server.args(); i++){
    //Serial.println(server.argName(i) + ":" + server.arg(i));
    //  inputMessage = inputMessage + server.argName(i) + ":" + server.arg(i) + "<BR>";
    if (server.argName(i)=="zone"){
      zone = server.arg(i).toInt();
      // for stop button.
      if (zone > intNumOfZones){
        zone = 0;
      }
    }
    if (server.argName(i)=="secToRun"){
      secToRun = server.arg(i).toInt();
    }
    if (server.argName(i)=="hdnStop"){
      isStop = server.arg(i).toInt();
    }
  }  
  String myForm = "<html><head><script src='https://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js'></script><script src='http://code.jquery.com/ui/1.9.2/jquery-ui.js'></script>";
  myForm += "<link rel='stylesheet' href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css' integrity='sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u' crossorigin='anonymous'><script>function stopButton(){ document.getElementById('hdnStop').value=1; document.getElementById('myForm').submit();  }</script></head>";
  myForm += "<body><FORM id='myForm' action='http://192.168.1.170/Zone'><span id='timer'></span>&nbsp;<input type='hidden' id='hdnStop' value='" + String(isStop) + "'/><script>var count='" + String(secToRun) + "';var counter=setInterval(timer, 1000); function timer(){  count=count-1;  if (count <= 0) { clearInterval(counter); document.getElementById('timer').innerHTML='Zone " + zone + " : Expired'; document.getElementById('submitButton').disabled=''; return; } document.getElementById('timer').innerHTML='Zone " + zone + " : ' + count + ' secs';document.getElementById('stopButton').disabled='';document.getElementById('submitButton').disabled='disabled'; }</script></p><hr align='left' width='500px'>";
  myForm += "<div style='width:750px'><p>Zone 1:<INPUT type='radio' name='zone' value='1'>Zone 2:<INPUT type='radio' name='zone' value='2'>Zone 3:<INPUT type='radio' name='zone' value='3'>";
  myForm += "Zone 4:<INPUT type='radio' name='zone' value='4'>Zone 5:<INPUT type='radio' name='zone' value='5'>Zone 6:<INPUT type='radio' name='zone' value='6'>Zone 7:<INPUT type='radio' name='zone' value='7'>";
  myForm += "Zone 8:<INPUT type='radio' name='zone' value='8'></p><p>Zone 9:<INPUT type='radio' name='zone' value='9'>Zone 10:<INPUT type='radio' name='zone' value='10'>Zone 11:<INPUT type='radio' name='zone' value='11'>";
  myForm += "Zone 12:<INPUT type='radio' name='zone' value='12'>Zone 13:<INPUT type='radio' name='zone' value='13'>Zone 14:<INPUT type='radio' name='zone' value='14'>Zone 15:<INPUT type='radio' name='zone' value='15'>";
  myForm += "Zone 16:<INPUT type='radio' name='zone' value='16'></p></div><div>Time to Run (sec): <input type='text' name='secToRun'></div><P><hr align='left' width='500px'><INPUT id='submitButton' style='background:green;' type='submit' value='Start Zone'><INPUT style='background:red;' id='stopButton' type='submit' value='Stop Zone' onclick='stopButton()'></p></FORM></body></html>";
  //inputMessage = inputMessage + myForm;  
  server.send(200, "text/html", myForm);
  // turn on
  if (zone == 0){
    controlRelay("STOPALL");
  } else {
    controlRelay(zone, secToRun);
  }
  Serial.println("****handling input end****");
}
// handle when URL isn't found
void handleNotFound(){
  String message = "File Not Found\n\n";
  message += "URI: ";
  message += server.uri();
  message += "\nMethod: ";
  message += (server.method() == HTTP_GET)?"GET":"POST";
  message += "\nArguments: ";
  message += server.args();
  message += "\n";
  for (uint8_t i=0; i<server.args(); i++){
    message += " " + server.argName(i) + ": " + server.arg(i) + "\n";
  }
  server.send(404, "text/plain", message);
}
void WebServer_setup(){
  server.on("/", handleRoot);
  server.on("/Zone", [](){
    Serial.println("HandleZone");
    handleInput();
  });
  server.onNotFound(handleNotFound);
  server.begin();
  Serial.println("HTTP server started");
}
//#########################
//# End Web Server Stuff
//#########################
//#########################
//# Scheduler stuff
//#########################
void setup_Scheduler(){
  int weekdays[intDaysOfWeek]; // RTC library starts dow on sunday = 0;
  weekdays[0] = 0;  weekdays[1] = 1;  weekdays[2] = 2;  weekdays[3] = 3;  weekdays[4] = 4;  weekdays[5] = 5;  weekdays[6] = 6;
  int duration[intNumOfZones];
  duration[0]= 300;  duration[1]= 300;  duration[2]= 300;  duration[3]= 600;  duration[4]= 600;  duration[5]= 600;  duration[6]= 600;
  duration[7]= 900;  duration[8]= 600;  duration[9]= 600;  duration[10]= 600;  duration[11]= 600;  duration[12]= 600;  duration[13]= 600;
  duration[14]= 0;  duration[15]= 0;
  arraySchedule[0].Update(weekdays, "04:00", duration);
  //duration[0]= 30;  duration[1]= 0;  duration[2]= 0;  duration[3]= 3;  duration[4]= 3;  duration[5]= 3;  duration[6]= 3;
  //duration[7]= 30;  duration[8]= 3;  duration[9]= 3;  duration[10]= 3;  duration[11]= 3;  duration[12]= 3;  duration[13]= 3;
  //duration[14]= 0;  duration[15]= 0;
  //arraySchedule[1].Update(weekdays, "09:00", duration);
  //duration[0]= 30;  duration[1]= 3;  duration[2]= 3;  duration[3]= 3;  duration[4]= 0;  duration[5]= 3;  duration[6]= 3;
  //duration[7]= 3;  duration[8]= 3;  duration[9]= 3;  duration[10]= 3;  duration[11]= 3;  duration[12]= 3;  duration[13]= 3;
  //duration[14]= 0;  duration[15]= 0;
  //arraySchedule[2].Update(weekdays, "14:00", duration);
  //duration[0]= 30;  duration[1]= 0;  duration[2]= 0;  duration[3]= 0;  duration[4]= 0;  duration[5]= 0;  duration[6]= 3;
  //duration[7]= 3;  duration[8]= 3;  duration[9]= 3;  duration[10]= 0;  duration[11]= 0;  duration[12]= 0;  duration[13]= 0;
  //duration[14]= 0;  duration[15]= 0;
  //arraySchedule[3].Update(weekdays, "18:00", duration);
}

String getValue(String data, char separator, int index)
{
  int found = 0;
  int strIndex[] = {0, -1};
  int maxIndex = data.length()-1;

  for(int i=0; i<=maxIndex && found<=index; i++){
    if(data.charAt(i)==separator || i==maxIndex){
        found++;
        strIndex[0] = strIndex[1]+1;
        strIndex[1] = (i == maxIndex) ? i+1 : i;
    }
  }
  return found>index ? data.substring(strIndex[0], strIndex[1]) : "";
}

// Function to take RtcDateTime and make it screen printable.
String ReturnDate(RtcDateTime inDate){
  char str[15];   //declare a string as an array of chars  
  sprintf(str, "%04u/%02u/%02u %02u:%02u:%02u",     //%d allows to print an integer to the string
    inDate.Year(),                      //get year method
    inDate.Month(),                 //get month method
    inDate.Day(),                      //get day method
    inDate.Hour(),                   //get hour method
    inDate.Minute(),              //get minute method
    inDate.Second()               //get second method
  );
  return String(str);
}

int dayOfWeek(RtcDateTime currDateTime){
  int intReturn= (int)currDateTime.DayOfWeek();
  return intReturn;
}
void checkScheduler(){
  if (relayDriver.isActive ==0){
    RtcDateTime myCurrentTime = Rtc.GetDateTime();
    //Serial.print("Current DateTime: ");
    //Serial.println(myCurrentTime);
    //Serial.print("Day Of Week");
    //Serial.println(dayOfWeek(myCurrentTime));
    //printDateTime(myCurrentTime);
    
    // if active, don't do anything.
    for (int arraySchedules=0;  arraySchedules < intNumOfSchedules; arraySchedules++){
    // handle each schedule
      for(int i=0; i < intDaysOfWeek; i++){
      // handle each day of week.
        if (arraySchedule[arraySchedules].weekDays[i]==dayOfWeek(myCurrentTime)){
          // if there is something scheduled for this day of the week with the current schedule.
          int intHour, intMinute, intSeconds=0;
          intHour = getValue(arraySchedule[arraySchedules].startTime,':',0).toInt();
          intMinute = getValue(arraySchedule[arraySchedules].startTime,':',1).toInt();
          // set time values to check for schedule vs curr time.
          RtcDateTime scheduleCheckTime = RtcDateTime(myCurrentTime.Year(), myCurrentTime.Month(), myCurrentTime.Day(), (uint16_t)intHour, (uint16_t)intMinute, (uint16_t)intSeconds);
          //Serial.print("CurrentHour: ");
          //Serial.print(intHour);
          //Serial.print ("  ");
          //Serial.print("CurrentMinute: ");
          //Serial.println(intMinute);
          if (scheduleCheckTime <= myCurrentTime){
          // make sure current time > than start time.
          //if (intHour <= (int)myCurrentTime.Hour() & intMinute <= (int)myCurrentTime.Minute()){
            //Serial.println("Zones to turn on:");
            // determine which zone to turn on next.
            for(int k=0; k < intNumOfZones; k++){
              //Serial.println(String(arraySchedules) + ": Zone:" + String(k) + ": Complete:" + String(!arraySchedule[0].isComplete[k]) + ":Duration:" + arraySchedule[0].zoneDuration[k]);
              // if zone isn't complete, turn it on, and mark it complete.
              if (!arraySchedule[arraySchedules].isComplete[k]){
                if (arraySchedule[arraySchedules].zoneDuration[k] > 0 & relayDriver.isActive != 1){
                  //Serial.println("Schedule: Relay to turn on: " + String(k+1));
                  Serial.println("Schedule: " + String(arraySchedules) + " : Relay to turn on: " + String(k+1) + " for: " + String(arraySchedule[arraySchedules].zoneDuration[k]) + " seconds.");
                  printDateTime(myCurrentTime);
                  // once this is called then Set start time for web page date, and duration, then run zone.
                  relayDriver.Update(k+1, arraySchedule[arraySchedules].zoneDuration[k]);
                  arraySchedule[arraySchedules].isComplete[k] = true;
                } else {
                  if (relayDriver.isActive != 1 & !arraySchedule[arraySchedules].isComplete[k]){
                    Serial.println("Schedule: " + String(arraySchedules) + " : Relay to turn on: " + String(k+1) + " skipped as duration = 0.");
                    arraySchedule[arraySchedules].isComplete[k] = true;
                  }
               }
              }
            }
          }
        }
      }
    } 
  } // else active and don't turn on.
}
// ********************************************
//  End Scheduling.
// ********************************************



// SETUP
void setup(){
  Serial.begin(115200);
  // Wi Fi Setup
  WiFi_Setup();
  // Start NTP Time Client
  Serial.println("NTP Time Lookup");
  timeClient.begin();
  delay(2000);
  timeClient.update();
  Serial.println("RTC Start");
  Rtc.Begin();
  Serial.println("RTC Update Time");
  RTC_Update();
  // RTC check values
  previousTimeCheckMillis = 0;
  checkHour =0;
  // Web Server Setup
  WebServer_setup();
  // Scheduler Setup
  setup_Scheduler();
}


void loop() {
  // Handle Time Check to keep RTC 
  // updated with NTP when out of DateTime
  timeCheck();
  // Web Server Check
  server.handleClient();
  // Relay Driver Control
  controlRelay();
  // Check Scheduler 
  checkScheduler();
}
// Utility function
#define countof(a) (sizeof(a) / sizeof(a[0]))
void printDateTime(const RtcDateTime& dt)
{
    char datestring[20];
    snprintf_P(datestring, 
            countof(datestring),
            PSTR("%02u/%02u/%04u %02u:%02u:%02u"),
            dt.Month(),
            dt.Day(),
            dt.Year(),
            dt.Hour(),
            dt.Minute(),
            dt.Second() );
    Serial.println(datestring);
}

Credits

Rich Noordam
10 projects • 34 followers
Many interests, computing obviously being one of them. MS SQL Server Database Administration, Reporting, Data Science, and more.
Thanks to Jeremy Anderson.

Comments