Martin Smaha
Published © GPL3+

ESP32 8-Zone Weather Aware Sprinkler Control

Off The Shelf ESP32 relay board based controller uses the free OpenWeatherMap api to detect rain, wind, and temperature.

AdvancedFull instructions provided8 hours142
ESP32 8-Zone Weather Aware Sprinkler Control

Things used in this project

Hardware components

ESP32 8-Relay Board
Search Amazon to find boards
×1
AC to DC converter model A121203
Search Amazon to find modules
×1
SparkFun FTDI Basic Breakout - 3.3V
SparkFun Electronics SparkFun FTDI Basic Breakout - 3.3V
Or equlivalent USB to 3v serial adaptor
×1

Software apps and online services

Arduino IDE
Arduino IDE

Story

Read more

Schematics

Valve schematic

Code

SprinklerControl.h

C/C++
Include file to link all files.
/*
 * Project SprinklerControl
 * Description: Home Sprinkler System with weather modification
 * Author: Martin Smaha
 * Date: 9/5/26
 * File: Project Includes
 */

typedef struct {
  int Magic;//should be 0xAA55
  bool isOperational;//sprinker system is operational
  int A_minutes[8];//minutes per zone
  int B_minutes[8];
  char A_hhmm[7][8];//daily start times in 24hr notation
  char B_hhmm[7][8];
  bool isRain;//disable during Rain
  bool isWind;//disable during wind
  int wind_mph;
  int A_temp;
  int B_temp;
} TParameters;

extern TParameters Parameters;
extern char icon_url[],weather_string[],observation_time[],WeatherBuf[];
extern float temp_f,wind_mph,precip_today_in,high_temp;
extern bool sprinklers[],manual[],isRain;
extern int DOW,Minute,Hour,Second,StateA,StateB,MinutesA,MinutesB,ZoneA,ZoneB,SystemState;

extern void WebInit(void);
extern void WebTask(bool restart);
extern void WeatherInit(void);
extern void WeatherMinute(void);
extern void WeatherTask(void);
extern void RouterReboot(void);

WebServer.cpp

C/C++
Web browser interface
/*
 * Project SprinklerControl
 * Description: Home Sprinkler System with weather modification
 * Author: Martin Smaha
 * Date: 9/11/26
 * File: Web Server
 */
#include "arduino.h"
#include "SprinklerControl.h"
#include <EEPROM.h>
#include <WiFi.h>

static WiFiServer server = WiFiServer(8080);//web page port
static WiFiClient WebClient;
static char LANBuf[4096];
static int WebState,ContentLength,ContentStart,LANBufCount,next_hhmm,current_hhmm;
static const char *dowString[] = {"SUN","MON","TUE","WED","THU","FRI","SAT"};

void ParseWeb(void);
void WebServer(void);
void WebTask(void);
void ParseForm(void);
void ParseManual(void);

int MyReset(String extra) {
  return 0;
}
int WebReset(String extra) {
  WebState = 3;
  return 0;
}

void WebInit(void) {
  WebState = 0;
  LANBufCount = 0;
  ContentLength = 0;
  ContentStart = 0;
  server.begin();
}

// Web Header
void WebHeader(void) {
  WebClient.print("HTTP/1.1 200 OK\r\n");
  WebClient.print("Cache-Control: max-age=0, no-cache\r\n");
  WebClient.print("Pragma: no-cache\r\n");
  WebClient.print("Connection: close\r\n\r\n");
  WebClient.print("<html><head><title>Sprinkler System</title><style>div {border: 2px solid #a1a1a1;padding: 10px 20px;width: 300px;border-radius: 25px;}button {width: 100px;height: 40px;background: blue;color: white;}button:hover {color: black;}</style>");
  WebClient.print("<meta http-equiv=\"Cache-Control\" content=\"no-cache, no-store, must-revalidate\" />");
  WebClient.print("<meta http-equiv=\"Pragma\" content=\"no-cache\" />");
  WebClient.print("<meta http-equiv=\"Expires\" content=\"0\" />");
  WebClient.print("<meta content='text/html; charset=ISO-8859-1' http-equiv='Content-Type'>");
  WebClient.print("<meta name='viewport' content='width=device-width, initial-scale=1'></head>");
  WebClient.print("<body><div>");
}

void WebFooter(void) {
  WebClient.print("</div></body></html>");
}

// Web server
void ParseWeb(void) {
  if (WebClient.connected()) {
    char buf[256];
    int isRequest = 0;
    if (strstr(LANBuf,"GET /status.html") != NULL) {
        isRequest = 1;
    } else if (strstr(LANBuf,"GET /manual.html") != NULL) {
        isRequest = 2;
    } else if (strstr(LANBuf,"GET /schedule.html") != NULL) {
        isRequest = 3;
    } else if (strstr(LANBuf,"POST /schedule.cgi") != NULL) {
        ParseForm();
        isRequest = 3;
    } else if (strstr(LANBuf,"GET /manual.cgi") != NULL) {
        ParseManual();
        isRequest = 2;
    } else if (strstr(LANBuf,"GET /weather.html") != NULL) {
        isRequest = 4;
    }
    //send response
    if (isRequest == 0) { //standard http error message for page not found
      WebClient.print("HTTP/1.1 404 \r\n\r\n");
      WebClient.stop();
    } else if (isRequest == 1) { //status.html
      WebHeader();
      //Header
      WebClient.print("<H1>Sprinkler System</H1>");
      //Current Weather
      sprintf(buf,"<table><tr><td><a href=weather.html><b>Current Weather</b></a></td><td><img src=\"%s\"</td></tr>",icon_url);
      WebClient.print(buf);
      sprintf(buf,"<tr><td>Weather:</td><td>%s</td></tr>",weather_string);
      WebClient.print(buf);
      sprintf(buf,"<tr><td>Temperature:</td><td>%.1f F</td></tr><tr><td>Wind Speed:</td><td>%.1f mph</td></tr>",temp_f,wind_mph);//todays rain removed 8/21/18
      WebClient.print(buf);
      sprintf(buf,"<tr><td>Today's High:</td><td>%.1f F</td></tr>",high_temp);
      WebClient.print(buf);
      sprintf(buf,"<tr><td>Last Updated:</td><td>%s UTC</td></tr>",observation_time);
      WebClient.print(buf);
      sprintf(buf,"<tr><td>Current Time:</td><td>%02d:%02d:%02d Local</td></tr></table>",Hour,Minute,Second);
      WebClient.print(buf);
      //current running program
      if (StateA > 0) {
        sprintf(buf,"<hr><font>Program A is Running: Zone %d</font>",ZoneA+1);
        WebClient.print(buf);
      } else if (StateB > 0) {
        sprintf(buf,"<hr><font>Program B is Running: Zone %d</font>",ZoneB+1);
        WebClient.print(buf);
      } else {
        switch (SystemState) {
          case -1:
            WebClient.print("<hr>System is Initializing");
            break;
          case 0:
            WebClient.print("<hr>System is DISABLED");
            break;
          case 1:
            WebClient.print("<hr>System is Operational");
            if (isDigit(Parameters.A_hhmm[DOW][0])) {
              next_hhmm = atoi(Parameters.A_hhmm[DOW]);
              current_hhmm = Minute + (Hour * 100);
              if (next_hhmm > current_hhmm) {
                if (high_temp >= Parameters.A_temp) {
                  sprintf(buf,"<br>A Prog starts at: %s",Parameters.A_hhmm[DOW]);
                  WebClient.print(buf);
                } else {
                  WebClient.print("<br>A Prog DISABLED due to temperature");
                }
              }
            }
            if (isDigit(Parameters.B_hhmm[DOW][0])) {
              next_hhmm = atoi(Parameters.B_hhmm[DOW]);
              current_hhmm = Minute + (Hour * 100);
              if (next_hhmm > current_hhmm) {
                if (high_temp >= Parameters.B_temp) {
                  sprintf(buf,"<br>B Prog starts at: %s",Parameters.B_hhmm[DOW]);
                  WebClient.print(buf);
                } else {
                  WebClient.print("<br>B Prog DISABLED due to temperature");
                }
              }
            }
            break;
          case 2:
            WebClient.print("<hr>System is Wind DISABLED");
            break;
          case 3:
            WebClient.print("<hr>System is Rain DISABLED");
            break;
        }
      }
      //menu area
      WebClient.print("<hr>");
      WebClient.print("<table><tr><td><button onclick=location.href='manual.html'>MANUAL</button><td>");
      WebClient.print("<td><button  onclick=location.href='schedule.html'>SCHEDULE</button></td></tr></table>");
      //Footer
      WebFooter();
      WebClient.stop();
    } else if (isRequest == 2) { //manual.html
      WebHeader();
      WebClient.print("<H1>Manual Control</H1><table>");
      for (int i = 0; i < 8; i++) {
        if (manual[i]) {
          sprintf(buf,"<tr><td><button  onclick=location.href='manual.cgi?zone=%d'>ZONE %d</button></td><td><span style=background:#00ff00;> &nbsp &nbsp </span><font>&nbsp ON</font></td></tr>",i+1,i+1);
        } else {
          sprintf(buf,"<tr><td><button  onclick=location.href='manual.cgi?zone=%d'>ZONE %d</button></td><td><span style=background:#cccccc;> &nbsp &nbsp </span><font>&nbsp OFF</font></td></tr>",i+1,i+1);
        }
        WebClient.print(buf);
      }
      WebClient.print("</table>");
      //menu area
      WebClient.print("<hr>");
      WebClient.print("<table><tr><td><button onclick=location.href='status.html'>STATUS</button><td>");
      WebClient.print("<td><button  onclick=location.href='schedule.html'>SCHEDULE</button></td></tr></table>");
      WebFooter();
      WebClient.stop();
    } else if (isRequest == 3){ //schedule.html
      WebHeader();
      WebClient.print("<H1>Schedule</H1><form action=schedule.cgi method=post>");
      //enable operation
      if (Parameters.isOperational) {
        WebClient.print("<input type=checkbox name=isOperational value=1 checked>Enable Operation<br>");
      } else {
        WebClient.print("<input type=checkbox name=isOperational value=1 >Enable Operation<br>");
      }
      //zone times
      WebClient.print("<hr><table><tr><td><b>Zone</b></td><td><b>Prog A</b></td><td><b>Prog B</b></td></tr>");
      for (int i = 0; i < 8; i++) {
        sprintf(buf,"<tr><td>%d</td><td><input type=text size=3 value=%d name=A_minutes%d></td><td><input type=text size=3 value=%d name=B_minutes%d></td></tr>",i+1,Parameters.A_minutes[i],i+1,Parameters.B_minutes[i],i+1);
        WebClient.print(buf);
      }
      WebClient.print("</table>Enter zone duration in minutes<hr>");
      //start times
      WebClient.print("<table><tr><td><b>Day</b></td><td><b>Start A</b></td><td><b>Start B</b></td></tr>");
      for (int i = 0; i < 7; i++) {
        char startA[8],startB[8];
        if (isdigit(Parameters.A_hhmm[i][0]) || Parameters.A_hhmm[i][0] == 'o') {
          strcpy(startA,Parameters.A_hhmm[i]);
        } else {
          strcpy(startA,"off");
        }
        if (isdigit(Parameters.B_hhmm[i][0]) || Parameters.B_hhmm[i][0] == 'o') {
          strcpy(startB,Parameters.B_hhmm[i]);
        } else {
          strcpy(startB,"off");
        }
        sprintf(buf,"<tr><td>%s</td><td><input type=text size=5 value=%s name=A_hhmm%d></td><td><input type=text size=5 value=%s name=B_hhmm%d></td></tr>",dowString[i],startA,i+1,startB,i+1);
        WebClient.print(buf);
      }
      WebClient.print("</table>Enter time as 24hr HHMM or off<hr>");
      //weather options
      WebClient.print("<b>Weather Options</b><br>");
      if (Parameters.isRain) {
        WebClient.print("<input type=checkbox name=isRain value=1 checked>Disable start if it has rained<br>");
      } else {
        WebClient.print("<input type=checkbox name=isRain value=1 >Disable start if it has rained<br>");
      }
      if (Parameters.isWind) {
        WebClient.print("<input type=checkbox name=isWind value=1 checked>Disable start if it is windy<br>");
      } else {
        WebClient.print("<input type=checkbox name=isWind value=1 >Disable start if it is windy<br>");
      }
      sprintf(buf,"<input type=text size=3 value=%d name=wind_mph>Wind MPH to disable at<br>",Parameters.wind_mph);
      WebClient.print(buf);
      sprintf(buf,"<input type=text size=3 value=%d name=A_temp>Temperature to enable A program<br>",Parameters.A_temp);
      WebClient.print(buf);
      sprintf(buf,"<input type=text size=3 value=%d name=B_temp>Temperature to enable B program<br>",Parameters.B_temp);
      WebClient.print(buf);
      //submit button
      WebClient.print("<hr><input type=submit value='Submit Changes'></form>");
      //menu area
      WebClient.print("<hr>");
      WebClient.print("<table><tr><td><button onclick=location.href='status.html'>STATUS</button><td>");
      WebClient.print("<td><button  onclick=location.href='manual.html'>MANUAL</button></td></tr></table>");
      WebFooter();
      WebClient.stop();
    } else if (isRequest == 4) {//weather.html
      WebHeader();
      WebClient.print("<H1>Weather Response</H1>Data response from openweathermap</div>");
      WebClient.print("<pre style='white-space: pre-wrap;'>");
      WebClient.print(WeatherBuf);
      WebClient.print("</pre><button onclick=location.href='status.html'>STATUS</button></body></html>");
      WebClient.stop();
    }
  }
}

// Get web server requests
void WebTask(bool restart) {
  switch (WebState) {
    case 0: // connect to server
      if (!WebClient.connected()) {//look for connection
        if (restart){
          WebState = 3;
        } else {
          WebClient = server.available();
        }
      } else {//we are connected
        memset(LANBuf,0,sizeof(LANBuf));
        LANBufCount = 0;
        ContentLength = 0;
        ContentStart = 0;
        WebState = 1;
      }
      break;
    case 1: // get returned data
      if (WebClient.connected()) {
        if (WebClient.available()) { //get each data segment
          char buf[256];
          int n = WebClient.read((uint8_t*)buf,sizeof(buf));
          if ((LANBufCount + n) <= sizeof(LANBuf)) { //prevent array overflow
            memcpy(LANBuf+LANBufCount,buf,n);
            LANBufCount += n;
          }
          if (ContentLength == 0) {
            char *p = strstr(LANBuf,"Content-Length:");
            if (p) {
              ContentLength = atoi(p+15);
            }
          }
          if (ContentStart == 0) {
            char *p = strstr(LANBuf,"\r\n\r\n");
            if (p) {
              ContentStart = p + 4 - LANBuf;
            }
          }
        } else if (LANBufCount >= (ContentLength + ContentStart) && LANBufCount > 0) {
          WebState = 2;
        }
      } else {
        WebState = 0;//9-9-2020
      }
      break;
    case 2: // parse data
      ParseWeb();
      WebState = 0;
      LANBufCount = 0;
      ContentStart = 0;
      ContentLength = 0;
      break;
    case 3: // restart server
      server.stop();
      WebState = 4;
      break;
    case 4: // restart server
      server.begin();
      WebState = 0;
      break;
  }
}

// Get web form response
void ParseForm(void) {
  char buf[32], *p;
  p = strstr(LANBuf,"isOperational=");
  if (p) {
    Parameters.isOperational = true;
  } else {
    Parameters.isOperational = false;
  }
  for (int i = 0; i < 8; i++) {
    sprintf(buf,"A_minutes%d=",i+1);
    p = strstr(LANBuf,buf);
    if (p) {
      p += strlen(buf);
      Parameters.A_minutes[i] = atoi(p);
    }
  }
  for (int i = 0; i < 8; i++) {
    sprintf(buf,"B_minutes%d=",i+1);
    p = strstr(LANBuf,buf);
    if (p) {
      p += strlen(buf);
      Parameters.B_minutes[i] = atoi(p);
    }
  }
  for (int i = 0; i < 7; i++) {
    sprintf(buf,"A_hhmm%d=",i+1);
    p = strstr(LANBuf,buf);
    if (p) {
      p += strlen(buf);
      char *end = strchr(p,'&');
      int n = 4;
      if (end) {
        n = min(end - p,4);
      }
      memset(Parameters.A_hhmm[i],0,6);
      strncpy(Parameters.A_hhmm[i],p,n);
    }
  }
  for (int i = 0; i < 7; i++) {
    sprintf(buf,"B_hhmm%d=",i+1);
    p = strstr(LANBuf,buf);
    if (p) {
      p += strlen(buf);
      char *end = strchr(p,'&');
      int n = 4;
      if (end) {
        n = min(end - p,4);
      }
      memset(Parameters.B_hhmm[i],0,6);
      strncpy(Parameters.B_hhmm[i],p,n);
    }
  }
  p = strstr(LANBuf,"isRain=");
  if (p) {
    Parameters.isRain = true;
  } else {
    Parameters.isRain = false;
  }
  p = strstr(LANBuf,"isWind=");
  if (p) {
    Parameters.isWind = true;
  } else {
    Parameters.isWind = false;
  }
  p = strstr(LANBuf,"wind_mph=");
  if (p) {
    p += 9;
    Parameters.wind_mph = atoi(p);
  }
  p = strstr(LANBuf,"A_temp=");
  if (p) {
    p += 7;
    Parameters.A_temp = atoi(p);
  }
  p = strstr(LANBuf,"B_temp=");
  if (p) {
    p += 7;
    Parameters.B_temp = atoi(p);
  }
  EEPROM.put(0, Parameters);
  EEPROM.commit();
}

//look for zone control command
void ParseManual(void) {
  char *p = strstr(LANBuf,"?zone=");
  if (p) {
    int x = atoi(p+6) - 1;
    if (x >= 0 && x < 8) {
      for (int i = 0; i < 8; i++) {//check all 8 zones
        if (i == x) {
          manual[i] = !manual[i];
        } else {
          manual[i] = false;
        }
      }
    }
  }
}

ESP32_Sprinkler.ino

C/C++
Main Logic
#include <EEPROM.h>
#include <WiFi.h>
#include "time.h"
#include "SprinklerControl.h"
#include <ESPmDNS.h>
#include <ArduinoOTA.h>

#define D0 32
#define D1 33
#define D2 25
#define D3 26
#define D4 27
#define D5 14
#define D6 12
#define D7 13

TParameters Parameters;
bool sprinklers[8],manual[8],restart,isRebooted;
int lastMinute,DOW,Minute,Hour,Second,StateA,StateB,MinutesA,MinutesB,ZoneA,ZoneB,SystemState,WiFiRSSI,ManualMinutes;
int numReboots;
bool isDST;
struct tm timeinfo;

bool checkDST(int day, int month, int dow)
{ //note: dow is 0-6 = sun-sat
    //January, february, and december are out.
    if (month < 3 || month > 11) { return false; }
    //April to October are in
    if (month > 3 && month < 11) { return true; }
    int previousSunday = day - dow;
    //In march, we are DST if our previous sunday was on or after the 8th.
    if (month == 3) { return previousSunday >= 8; }
    //In november we must be before the first sunday to be dst.
    //That means the previous sunday must be before the 1st.
    return previousSunday <= 0;
}

// Replace with your network credentials
const char* ssid     = "YOUR_SSID";
const char* password = "YOUR_PASSWORD";

// NTP Server configurations
const char* ntpServer = "pool.ntp.org";
const long  gmtOffset_sec = -28800;   // Change this to your timezone offset in seconds (e.g., EST = -5 hours = -18000)
const int   daylightOffset_sec = 3600; // Daylight savings offset in seconds (1 hour = 3600, or 0 if not used)
const int   standardOffset_sec = 0;

void printLocalTime() {
  // Print using standard C formatting string (strftime)
  Serial.println(&timeinfo, "%A, %B %d %Y %H:%M:%S");
}

void syncTime(){
  if (isDST){
    configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
  }else{
    configTime(gmtOffset_sec, standardOffset_sec, ntpServer);
  }
  Serial.print("Syncing internal RTC with NTP...");
  while(!getLocalTime(&timeinfo)){
     delay(500);
     Serial.print(".");
  }
  Serial.println(" SYNCED!");
  printLocalTime();
}



void setup() {
  Serial.begin(115200);

  // Connect to Wi-Fi
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  while (WiFi.waitForConnectResult() != WL_CONNECTED) {
    Serial.println("Connection Failed! Rebooting...");
    delay(5000);
    ESP.restart();
  }
  // OTA
  ArduinoOTA.setHostname("ESP32-Sprinkler-OTA");
  ArduinoOTA.begin();

  // setup digital IO for zone control
  pinMode(D0, OUTPUT);
  pinMode(D1, OUTPUT);
  pinMode(D2, OUTPUT);
  pinMode(D3, OUTPUT);
  pinMode(D4, OUTPUT);
  pinMode(D5, OUTPUT);
  pinMode(D6, OUTPUT);
  pinMode(D7, OUTPUT);
  
  digitalWrite(D0, LOW); //low is off
  digitalWrite(D1, LOW);
  digitalWrite(D2, LOW);
  digitalWrite(D3, LOW);
  digitalWrite(D4, LOW);
  digitalWrite(D5, LOW);
  digitalWrite(D6, LOW);
  digitalWrite(D7, LOW);
 
  memset(&sprinklers[0],0,sizeof(sprinklers));
  memset(&manual[0],0,sizeof(manual));
  StateA = 0;
  StateB = 0;
  SystemState = -1;
  numReboots = 0;
  isRebooted = false;
  EEPROM.begin(512);
  EEPROM.get(0, Parameters);

  if (Parameters.Magic != 0xA5A5) {
    //default values - use web interface to set schedule
    Parameters.Magic = 0xA5A5;
    Parameters.isOperational = false;
    for (int i = 0; i < 8; i++) {
      Parameters.A_minutes[i] = 0;
      Parameters.B_minutes[i] = 0;
    }
    for (int i = 0; i < 7; i++) {
      strcpy(Parameters.A_hhmm[i],"off");
      strcpy(Parameters.B_hhmm[i],"off");
    }
    Parameters.isRain = true;//disable during Rain
    Parameters.isWind = true;//disable during wind
    Parameters.wind_mph = 10;
    Parameters.A_temp = 70;
    Parameters.B_temp = 80;
  }
  // start listening for clients
  WebInit();
  WeatherInit();
  lastMinute = timeinfo.tm_min;
  restart = true;
  ManualMinutes = 0;
  isDST = false;
  syncTime();
  isDST = checkDST(timeinfo.tm_mday,timeinfo.tm_mon,timeinfo.tm_wday);
  if (isDST) syncTime();//again to compensate for DST if present

}//end setup

void SprinklersOff(void) {
  memset(sprinklers,0,sizeof(sprinklers));
}

void UpdateZones(void) {
  digitalWrite(D0, (sprinklers[0] || manual[0])?HIGH:LOW); //low is off
  digitalWrite(D1, (sprinklers[1] || manual[1])?HIGH:LOW);
  digitalWrite(D2, (sprinklers[2] || manual[2])?HIGH:LOW);
  digitalWrite(D3, (sprinklers[3] || manual[3])?HIGH:LOW);
  digitalWrite(D4, (sprinklers[4] || manual[4])?HIGH:LOW);
  digitalWrite(D5, (sprinklers[5] || manual[5])?HIGH:LOW);
  digitalWrite(D6, (sprinklers[6] || manual[6])?HIGH:LOW);
  digitalWrite(D7, (sprinklers[7] || manual[7])?HIGH:LOW);
}

void ScheduleTask(void) {
  if (Parameters.isOperational) {
    SystemState = 1;
    if (Parameters.isWind && wind_mph >= Parameters.wind_mph) {
      SystemState = 2;
      SprinklersOff();
      StateA = 0;
      StateB = 0;
    } else if (Parameters.isRain && isRain) {
      SystemState = 3;
      SprinklersOff();
      StateA = 0;
      StateB = 0;
    } else {
      ProgramTaskA();
      ProgramTaskB();
    }
  } else {
    SystemState = 0;
    StateA = 0;
    StateB = 0;
  }
}

void ProgramTaskA(void) {//entered once each minute when seconds are 0
  switch (StateA) {
    case 0: //look for start
      if (high_temp >= Parameters.A_temp && isDigit(Parameters.A_hhmm[DOW][0])) {
        int t = atoi(Parameters.A_hhmm[DOW]);//get 4 digit time
        int hr = t/100;
        int mn = t%100;
        if (Hour == hr && Minute == mn){
          MinutesA = 0;
          ZoneA = -1;
          StateA = 1;
          StartZoneA();
        }
      }
      break;
    case 1: //run for Duration
      if (MinutesA >= Parameters.A_minutes[ZoneA]) {
        MinutesA = 0;
        SprinklersOff();
        StartZoneA();//next zone
      }
      break;
  }
}

void StartZoneA(void) {
  for (ZoneA++; ZoneA < 8; ZoneA++) {
    if (Parameters.A_minutes[ZoneA] > 0) {
      sprinklers[ZoneA] = true;
      break;
    }
  }
  if (ZoneA >= 8) {
    StateA = 0;//all done
  }
}
void ProgramTaskB(void) {//entered once each minute when seconds are 0
  switch (StateB) {
    case 0: //look for start
      if (high_temp >= Parameters.B_temp && isDigit(Parameters.B_hhmm[DOW][0])) {
        int t = atoi(Parameters.B_hhmm[DOW]);//get 4 digit time
        int hr = t/100;
        int mn = t%100;
        if (Hour == hr && Minute == mn){
          MinutesB = 0;
          ZoneB = -1;
          StateB = 1;
          StartZoneB();
        }
      }
      break;
    case 1: //run for Duration
      if (MinutesB >= Parameters.B_minutes[ZoneB]) {
        MinutesB = 0;
        SprinklersOff();
        StartZoneB();//next zone
      }
      break;
  }
}

void StartZoneB(void) {
  for (ZoneB++; ZoneB < 8; ZoneB++) {
    if (Parameters.B_minutes[ZoneB] > 0) {
      sprinklers[ZoneB] = true;
      break;
    }
  }
  if (ZoneB >= 8) {
    StateB = 0;//all done
  }
}

void loop() {
  ArduinoOTA.handle();
  WiFiRSSI = WiFi.RSSI();
  getLocalTime(&timeinfo);
  Hour = timeinfo.tm_hour;
  Minute = timeinfo.tm_min;
  Second = timeinfo.tm_sec;
  DOW = timeinfo.tm_wday;
  restart = false;

  if (Minute != lastMinute) {
    restart = true;
    WeatherMinute();
    MinutesA++;
    MinutesB++;
    if (manual[0] || manual[1] || manual[2] || manual[3] || manual[4] || manual[5] || manual[6] || manual[7]){
      ManualMinutes++;
      if (ManualMinutes >= 5) {
        memset(manual,0,sizeof(manual));
      }
    } else {
      ManualMinutes = 0;
    }
    //automatically handle DST-PST switching at 2am
    if (Hour == 2) {
      bool maybeDST = checkDST(timeinfo.tm_mday,timeinfo.tm_mon,timeinfo.tm_wday);
      if (isDST != maybeDST){
        isDST = maybeDST;
        syncTime();//do again to correct for change
      }
    }
	  //reset temperature at 0602 each day
    if (Hour == 6 && Minute < 2) {
      isRebooted = false;
      high_temp = temp_f;//reset daily peak temperature
      isRain = false;//reset rain flag
      if (WiFi.isConnected()) syncTime();//and resync with time service
    }
  }
  lastMinute = Minute;
  WebTask(restart);
  ScheduleTask();
  UpdateZones();
  WeatherTask();
}

WeatherClient.cpp

C/C++
OpenWeatherMap interface
/*
 * Project SprinklerControl
 * Description: Home Sprinkler System with weather modification
 * Author: Martin Smaha
 * Date: 9/11/26
 * File: Weather Client
 */
#include "SprinklerControl.h"
#include "arduino.h"
#include <WiFi.h>
#include "time.h"

static WiFiClient WeatherClient;
char WeatherBuf[1024],icon_url[128],weather_string[256],observation_time[256];
float temp_f,wind_mph,precip_today_in,high_temp;
int WeatherState,WeatherTimer,WeatherBufCount,TZOffset;
bool isRain;

void WeatherTask(void);
void ParseWeather(void);
void ParseFloat(const char* key, float* value);
void ParseInt(const char* key, int* value);
void ParseLongLong(const char* key, long long* value);
void ParseString(const char* key, char* value);

int WReset(String extra) {
  WeatherState = 3;
  return 0;
}

void WeatherInit(void) {
  WeatherState = 3;
  WeatherTimer = 10;
  isRain = false;
}

void WeatherMinute(void) {
  WeatherTimer++;
}

// Parse Weather into the values we need
void ParseWeather(void) {
  char buf[32];
  ParseFloat("temp_max",&temp_f);//choose temp or temp_min or temp_max
  ParseFloat("speed",&wind_mph);
  precip_today_in = 0.0;
  memset(weather_string,0,sizeof(weather_string));
  ParseString("main",weather_string);
  memset(icon_url,0,sizeof(icon_url));
  memset(buf,0,sizeof(buf));
  ParseString("icon",buf);
  sprintf(icon_url,"http://openweathermap.org/img/w/%s.png",buf);
  memset(observation_time,0,sizeof(observation_time));
  time_t unix_UTC = 0;
  long long utc = 0;
  ParseLongLong("dt",&utc);
  unix_UTC = time_t(utc);
  struct tm *timeinfo = gmtime(&unix_UTC);
  sprintf(observation_time,"%02d:%02d:%02d",timeinfo->tm_hour,timeinfo->tm_min,timeinfo->tm_sec);
  if (temp_f > high_temp) {
    high_temp = temp_f;
  }
  if ((strcmp(weather_string,"Rain") == 0) || precip_today_in > 0.02) {
    isRain = true;
  }
}

// ParseFloat is a simple single level finder
void ParseFloat(const char* key, float* value) {
  char* p = strstr(WeatherBuf,key);
  if (p) {
    p = strstr(p,":");
    if (p) {
      *value = atof(p+1);
    }
  }
}
// ParseInt is a simple single level finder
void ParseInt(const char* key, int* value) {
  char* p = strstr(WeatherBuf,key);
  if (p) {
    p = strstr(p,":");
    if (p) {
      *value = atoi(p+1);
    }
  }
}
// ParseLongLong is a simple single level finder
void ParseLongLong(const char* key, long long* value) {
  char* p = strstr(WeatherBuf,key);
  if (p) {
    p = strstr(p,":");
    if (p) {
      *value = atoll(p+1);
    }
  }
}
// ParseString is a simple single level finder
void ParseString(const char* key, char* value) {
  char keyword[32];
  memset(keyword,0,32);
  sprintf(keyword,"\"%s\"",key);
  char* p = strstr(WeatherBuf,keyword);
  if (p) {
    p = strstr(p,":");
    if (p) {
      p = strstr(p+1,"\"");
      if (p) {
        p++;
        char *p2 = strstr(p,"\"");
        if (p2) {
          strncpy(value,p,p2-p);
        }
      }
    }
  }
}

// Get current weather conditions every 10 minutes
void WeatherTask(void) {
  switch (WeatherState) {
    case 0: // connect to server
      if (WeatherClient.connect("api.openweathermap.org", 80)) {
        WeatherClient.println("GET /data/2.5/weather?id=YOUR_CITY&units=imperial&APPID=YOUR_API_KEY"); //by station example
        WeatherClient.println("Host: api.openweathermap.org");
        WeatherClient.println("Content-Length: 0");
        WeatherClient.println();
        memset(WeatherBuf,0,sizeof(WeatherBuf));
        WeatherBufCount = 0;
        WeatherState = 1;
      }
      break;
    case 1: // get returned data
      if (WeatherClient.connected()) {
        if (WeatherClient.available()) { //get each data segment
          WeatherClient.read((uint8_t*)WeatherBuf,sizeof(WeatherBuf)-1);
        }
        if (WeatherTimer >= 2) {//timeout if response is taking too long
          WeatherClient.stop();//force closed
          WeatherState = 3;
        }
      } else {
        WeatherState = 2;
      }
      break;
    case 2: // parse data
      if (strstr(WeatherBuf,"404 Not Found")) {
        WeatherTimer = 8;//try again sooner
      } else {
        ParseWeather();
      }
      WeatherState = 3;
      break;
    case 3: // wait 15 minutes and update
      if (WeatherTimer >= 15) {
        WeatherTimer = 0;
        WeatherState = 0;
      }
      break;
  }
}

Credits

Martin Smaha
3 projects • 10 followers
Designing hardware and software since 1975.

Comments