Simple 4 relay irrigation scheduler

you talk about what?

GG07 posted a picture of his GUI.

Full credit goes to @hagay. I used the code @hagay posted at entry 23. The only additions I made were merely cosmetic for example…

OTA

   #include <ESP8266mDNS.h>  // For OTA
   #include <WiFiUdp.h>  // For OTA
   #include <ArduinoOTA.h>  // For OTA
     .
     .
     .
void setup()
{
  ArduinoOTA.setHostname("Sprinkler");  // For OTA
  ArduinoOTA.begin();  // For OTA
}
  .
  .
  .
void loop()
{
  ArduinoOTA.handle();  // For OTA
}

LED Widget to indicate the GPIO pin is high and therefore the sprinkler is on. Also added a sneaky label name change (On/Off).

WidgetLED led1(V37); //defines Blynk LED widget virtual pin 37
const int btnPin = 2 ;

void RelayLed(){   
  byte val = digitalRead(btnPin);   // read button value
  if (val == HIGH) {           // check if the switch is HIGH
    Blynk.virtualWrite(V37, 255);  // turn app RelayLED on V37 ON
    Blynk.setProperty(V37, "label", "  ON");
  } 
  else {
        Blynk.virtualWrite(V37, 0);  // turn app RelayLED on V37 OFF
        Blynk.setProperty(V37, "label", " OFF");
  }  
}


void setup()
{
timer.setTimeout(100, [](){
   timer.setInterval(1000L, RelayLed);
  });
}

And finally I added Millis Uptime and wifi signal strength (ESP8266)…

void sendUptime()
{
   Blynk.virtualWrite(38, millis() / 1000);
   Blynk.virtualWrite(39, WiFi.RSSI());
}
.
.
.
void setup()
{
     timer.setTimeout(300, [](){
       timer.setInterval(5000L, sendUptime);
      });
}

I modified hagay’s code and added the features you have questions about. I started by loading different examples that Blynk has in their tutorial section. With trial and error I eventually figured out how to integrate some of the different features into my code.

I use etc widget to sync and the read processor time every 10 seconds . This is so I know that everything is working and communicating properly and that my sprinklers will go on and off when I they should.

The top sliding is on and off. I added this before haygay added his system on/off controls. This allowed me to shut the system off remotely when it rains.

The system status virtual led is just a visual verification that reads if the system is on or offf. It is a visual indication that let’s me know that the device received my command to shut off.

I have not had time to try hagay’s latest code. But if you use it, the PIN numbers will be different but should work similar to my modifications for on and off. Hagay’s documentation inside the code is pretty good.

Posting my code here may cause confusion. This is hagay’s forum. I will see if I can do a QRC version.

THIS IS THE CODE I USE: 98% Hagay 2% modification
It is based on an older version of Hagay’s code–not the lattest version

MODIFICATIONS:

  • GPIO settings unique to my device
  • Different WIFI LED setting so it would work with my device
  • Added an ON/OFF before haygay added the one in his newer version V50
  • added PROCESSOR TIME V101
  • added PROCESSOR Date V102
  • added LED indication of system on/off V55
/*************************************************************
  Download latest Blynk library here:
    https://github.com/blynkkk/blynk-library/releases/latest

  Blynk is a platform with iOS and Android apps to control
  Arduino, Raspberry Pi and the likes over the Internet.
  You can easily build graphic interfaces for all your
  projects by simply dragging and dropping widgets.

    Downloads, docs, tutorials: http://www.blynk.cc
    Sketch generator:           http://examples.blynk.cc
    Blynk community:            http://community.blynk.cc
    Follow us:                  http://www.fb.com/blynkapp
                                http://twitter.com/blynk_app

  Blynk library is licensed under MIT license
  This example code is in public domain.

 *************************************************************
  This example runs directly on ESP8266 chip.

  Note: This requires ESP8266 support package:
    https://github.com/esp8266/Arduino

  Please be sure to select the right ESP8266 module
  in the Tools -> Board menu!

  Change WiFi ssid, pass, and Blynk auth token to run :)
  Feel free to apply it to any other example. It's simple!
 *************************************************************/

/* Comment this out to disable prints and save space */
#define BLYNK_PRINT Serial
#define WIFI_LED 10

// define configuration (number of switches and number of timers)
#define SWITCH_CNT 4
#define TIME_CNT 4

#include <ESP8266WiFi.h>
#include <BlynkSimpleEsp8266.h>
#include <TimeLib.h>
#include <WidgetRTC.h>


// You should get Auth Token in the Blynk App.
// Go to the Project Settings (nut icon).
char auth[] = "XXXXXXXXXXXXXXXXXXXXXXXXXXX";

// Your WiFi credentials.
// Set password to "" for open networks.
char ssid[] = "XXXXXXXXXX";
char pass[] = "XXXXXXXXXXXXXX";


byte switch_pins[] = {14 , 12 , 16 , 13}; // number of gpio to be used as switch/relay control

bool switch_default[] = {LOW,LOW,LOW,LOW}; // switches that use reverse polarity should be set to HIGH here

////////////////////////////////////////////////////////////////////////////////////////
// This code can control up to 4 switches                                            //
// For each switch up to 4 schedule start and end times can be configured - (V0..V15) //
// A default duration can be defined per switch                           - (V16..V19)//
// If an end time is not defined at the app the default duration is used              //
// default duration is also used when manually activating the switch       - (V20..V23)//
// for each switch when start time is reached the switch turns on for the duration    //
//                                                                                    //
// maximum duration is used for safety (to limit activation time)          - (V24..V27)//
////////////////////////////////////////////////////////////////////////////////////////
int  start_time_sec[SWITCH_CNT][TIME_CNT];     // array of 4 start times (in seconds) for 4 switches [switch number][schedual timer number]
bool start_valid[SWITCH_CNT][TIME_CNT];        // is the start time valid ?
bool weekdays[SWITCH_CNT][TIME_CNT][8];        // array of 8 days (day 0 not used) for each schedual time
int  active_duration[SWITCH_CNT][TIME_CNT+1];  // duration per switch per time(in sec)
bool end_valid[SWITCH_CNT][TIME_CNT];          // is the end time valid ?
int  max_duration[SWITCH_CNT];                 // max duration per switch
int auto_off = 1;           // 1 is auto or on

// when activating a switch a timer is set for the configured duration
// when the duration ends the switch is turned off by the timer
// the id of the timer is saved using end_timer_id
// if switch is manually turned off the end_timer_id is used to stop the timer.

// end_timer_id is initialized to 32 (per entry) at the setup section
int end_timer_id[SWITCH_CNT];

// timer object declaration
BlynkTimer timer;

// this code use Real Time Clock widget in the blynk app to keep the clock updated from net
WidgetRTC rtc;


BLYNK_CONNECTED() {
  // Synchronize time on connection
  rtc.begin();
  Blynk.syncAll();
}
WidgetLED led1(V55);

bool ledStatus = auto_off;



// V55 LED Widget is ON or OFF
void LedWidget()
{ledStatus = auto_off;
  if (ledStatus) {
   led1.on();
  } else {
    led1.off();
    
  }
}

//////////////////////////////////////////////////////////
// get schedual parameters from App                     //
//////////////////////////////////////////////////////////
void set_time(BlynkParam param, byte switch_no, byte time_no){

     TimeInputParam t(param);
  // Process start time

  if (t.hasStartTime())
  {
    
    Serial.println(String("Start: ") +
                   t.getStartHour() + ":" +
                   t.getStartMinute() + ":" +
                   t.getStartSecond());
    Serial.println(String("Start in sec: ") + param[0].asLong() + String(" for switch") + switch_no + String(" time_no: ") + time_no);
    start_time_sec[switch_no][time_no]= param[0].asLong();
    start_valid[switch_no][time_no] = true;

  }
  else
  {
    // Do nothing
    Serial.println(String("No Start Time Given for switch: ") + switch_no + String(" time_no: ") + time_no);
    start_valid[switch_no][time_no] = false;
  }



  //////////////////////////////////////////////////////////////////////////////
  // check if end time is received convert and save it as day time in seconds //
  //////////////////////////////////////////////////////////////////////////////
  if (t.hasStopTime())
  {
    
    Serial.println(String("Stop: ") +
                   t.getStopHour() + ":" +
                   t.getStopMinute() + ":" +
                   t.getStopSecond());
    Serial.println(String("Stop in sec: ") + param[1].asLong() + String(" for switch") + switch_no + String(" time_no: ") + time_no);

    active_duration[switch_no][time_no] = (param[1].asLong()-start_time_sec[switch_no][time_no]);
    
    // if end time is smaller than start time this means end time is at next day
    if (active_duration[switch_no][time_no]<0) active_duration[switch_no][time_no]=86400+active_duration[switch_no][time_no];
    
    Serial.println(String("Stop duration: ") + active_duration[switch_no][time_no]);

    end_valid[switch_no][time_no] = true;


  }
  else // if end time is not defined //
  {
    // Do nothing
    Serial.println(String("No Stop Time Given for switch: ") + switch_no + String(" time_no: ") + time_no);
    end_valid[switch_no][time_no] = false;
  }

 
  // Process weekdays (1. Mon, 2. Tue, 3. Wed, ...)

  for (int i = 1; i <= 7; i++) {
    if (t.isWeekdaySelected(i)) {
      Serial.println(String("Day ") + i + " is selected");
      weekdays[switch_no][time_no][i] = true;
    }
    else {
      weekdays[switch_no][time_no][i] = false;
    }
    
  }

    Serial.println();

}
//this reads status of on/off mode and displays on screen
BLYNK_WRITE(V50) { int Vpin50 = param.asInt();
  auto_off = Vpin50;}


// V0..V3 for switch 0, V4..V7 for switch 1 ...
BLYNK_WRITE(V0)  { set_time(param, 0,0);  }
BLYNK_WRITE(V1)  { set_time(param, 0,1);  }
BLYNK_WRITE(V2)  { set_time(param, 0,2);  }
BLYNK_WRITE(V3)  { set_time(param, 0,3);  }

BLYNK_WRITE(V4)  { set_time(param, 1,0);  }
BLYNK_WRITE(V5)  { set_time(param, 1,1);  }
BLYNK_WRITE(V6)  { set_time(param, 1,2);  }
BLYNK_WRITE(V7)  { set_time(param, 1,3);  }

BLYNK_WRITE(V8)  { set_time(param, 2,0);  }
BLYNK_WRITE(V9)  { set_time(param, 2,1);  } 
BLYNK_WRITE(V10) { set_time(param, 2,2);  }
BLYNK_WRITE(V11) { set_time(param, 2,3);  }

BLYNK_WRITE(V12) { set_time(param, 3,0);  }
BLYNK_WRITE(V13) { set_time(param, 3,1);  }
BLYNK_WRITE(V14) { set_time(param, 3,2);  }
BLYNK_WRITE(V15) { set_time(param, 3,3);  }

// use a slider to define default activation duration (slider count in minute)
BLYNK_WRITE(V16) { active_duration[0][TIME_CNT] = param.asInt()*60; }
BLYNK_WRITE(V17) { active_duration[1][TIME_CNT] = param.asInt()*60; }
BLYNK_WRITE(V18) { active_duration[2][TIME_CNT] = param.asInt()*60; }
BLYNK_WRITE(V19) { active_duration[3][TIME_CNT] = param.asInt()*60; }


// use a slider to define default max duration (slider count in minute)   ***HARD CODE THIS*** NL
BLYNK_WRITE(V24) { max_duration[0] = param.asInt()*60; }
BLYNK_WRITE(V25) { max_duration[1] = param.asInt()*60; }
BLYNK_WRITE(V26) { max_duration[2] = param.asInt()*60; }
BLYNK_WRITE(V27) { max_duration[3] = param.asInt()*60; }

/////////////////////////////////////////////////////////////////
// Handle switch events (from app or from scheduler )          //
/////////////////////////////////////////////////////////////////

// turn off switch after active duration ends
// duration number is not important here 
void turn_off_switch_no_0(){ turn_on_off(0,0,0); Blynk.virtualWrite(V20,0); Serial.println(String("timer turn off switch 0 ") );}
void turn_off_switch_no_1(){ turn_on_off(0,1,0); Blynk.virtualWrite(V21,0); Serial.println(String("timer turn off switch 1 ") );}
void turn_off_switch_no_2(){ turn_on_off(0,2,0); Blynk.virtualWrite(V22,0); Serial.println(String("timer turn off switch 2 ") );}
void turn_off_switch_no_3(){ turn_on_off(0,3,0); Blynk.virtualWrite(V23,0); Serial.println(String("timer turn off switch 3 ") );}

// handle switch state
void turn_on_off(int on_off, byte switch_no , byte time_no){
    long active_duration_ms ;
    char Time_print[16];
    
    if ((on_off==1) && (auto_off == 1)) //auto_off is a slider in app to shut off the program
    {
      // create time as string to print on activation butten
      sprintf(Time_print, "%02d:%02d", hour(), minute());

      // turn on the switch (or off if default is on)
      digitalWrite(switch_pins[switch_no],!switch_default[switch_no]);

      // if end time is valid use the active duration assigned to this time
      // (change to msec will be done later)
      if (end_valid[switch_no][time_no])
         active_duration_ms = ((long)active_duration[switch_no][time_no]);
      else // otherwise use the general time duration
         active_duration_ms = ((long)active_duration[switch_no][4]);

      
      // max duration smaller than two min is not valid
      if ( (max_duration[switch_no]< 120) | (max_duration[switch_no]>active_duration_ms) )
         active_duration_ms = active_duration_ms*1000;
      else
        active_duration_ms = ((long)max_duration[switch_no])*1000;
       

      // if new timer is set before another one ended then disable previous timer
      if (end_timer_id[switch_no]!=32) timer.deleteTimer(end_timer_id[switch_no]);

      // turn on switch and set timer 
      switch (switch_no) {
        case 0: 
          Blynk.setProperty(V20, "onLabel", String(Time_print));
          Blynk.virtualWrite(V20,1);
          end_timer_id[0]=timer.setTimeout(active_duration_ms, turn_off_switch_no_0);
          
         break;
        case 1: 
          Blynk.setProperty(V21, "onLabel", String(Time_print));
          Blynk.virtualWrite(V21,1);
          end_timer_id[1]=timer.setTimeout(active_duration_ms, turn_off_switch_no_1);
         break;
        case 2: 
          Blynk.setProperty(V22, "onLabel", String(Time_print));
          Blynk.virtualWrite(V22,1);
          end_timer_id[2]=timer.setTimeout(active_duration_ms, turn_off_switch_no_2);
         break;
        case 3: 
          Blynk.setProperty(V23, "onLabel", String(Time_print));
          Blynk.virtualWrite(V23,1);
          end_timer_id[3]=timer.setTimeout(active_duration_ms, turn_off_switch_no_3);
         break;    
      } 
      Serial.println(String("turn ON switch: ") + switch_no + String(" for duration: ") + active_duration_ms/60000 + String("min "));
    }
    else 
    {
      digitalWrite(switch_pins[switch_no],switch_default[switch_no]);
      timer.deleteTimer(end_timer_id[switch_no]);
      end_timer_id[switch_no]=32;
      Serial.println(String("turn OFF switch: ") + switch_no);
    }   
}

// set switch state from APP
BLYNK_WRITE(V20) { turn_on_off(param.asInt(),0,TIME_CNT); }
BLYNK_WRITE(V21) { turn_on_off(param.asInt(),1,TIME_CNT); }
BLYNK_WRITE(V22) { turn_on_off(param.asInt(),2,TIME_CNT); }
BLYNK_WRITE(V23) { turn_on_off(param.asInt(),3,TIME_CNT); }

/////////////////////////////////////////////////////////////////////////////////////////////
// the following function is called every 60 seconds by a timer                            //
//  the function checks if a start time is reached and if yes it will call                 //
//   the turn_on_off function (to turn on the switch and set timer for turning off switch) //
/////////////////////////////////////////////////////////////////////////////////////////////
void activetoday(){         // check if schedule #1 should run today

  if(Blynk.connected()) // set wifi led if no connection
  {
    digitalWrite(WIFI_LED,LOW); 
  }
  else
  {
    digitalWrite(WIFI_LED,HIGH); 
  }
  
  if(year() != 1970){
    unsigned int nowseconds = ((hour() * 3600) + (minute() * 60) + second());
    int dayadjustment = -1;  
    if(weekday() == 1){
      dayadjustment = 6; // needed for Sunday Time library is day 1 and Blynk is day 7
    }
   
 
     for (int switch_cnt = 0;  switch_cnt< SWITCH_CNT; switch_cnt++) {
         for (int timer_cnt = 0;  timer_cnt< TIME_CNT; timer_cnt++) {
              if (start_valid[switch_cnt][timer_cnt] == true) {
                if (weekdays[switch_cnt][timer_cnt][weekday() + dayadjustment]==true){
                  
                   if (nowseconds >= start_time_sec[switch_cnt][timer_cnt]){
                      
                      if(nowseconds < start_time_sec[switch_cnt][timer_cnt]+90){
                        turn_on_off(1,switch_cnt,timer_cnt);
                        Serial.println(String("turn ON switch: ") + switch_cnt);

                      }
                   }
                }
              }
         }
      
     }
 
  } 
}

/////////////////////////////////////

// Digital clock display of the time
void clockDisplay()
{
  // You can call hour(), minute(), ... at any time
  // Please see Time library examples for details

  String currentTime = String(hour()) + ":" + minute() + ":" + second();
  String currentDate = String(day()) + " " + month() + " " + year();
  Serial.print("Current time: ");
  Serial.print(currentTime);
  Serial.print(" ");
  Serial.print(currentDate);
  Serial.println();

  // Send DATE and DATE to Blynk APP
  Blynk.virtualWrite(V101, currentTime);
  // Send date to the App--Not used but is available--this will show processor date if it is important to you
  Blynk.virtualWrite(V102, currentDate);
}


void setup()
{
  
  Serial.begin(9600);  // Debug console
  
  // reset output pins used and system variables
  for (int i = 0; i<SWITCH_CNT ; i++){
    pinMode(switch_pins[i], OUTPUT); // reset output pins
    digitalWrite(switch_pins[i],switch_default[i]); // set switch to default
    end_timer_id[i] = 32;            // reset end timer id's

    for (int j = 0; j<TIME_CNT ; j++)
    {
      end_valid[i][j] = false;       // reset end valid array
    }
    
  }

  
  pinMode(WIFI_LED, OUTPUT);

  
  Blynk.begin(auth, ssid, pass);
    rtc.begin();  // Begin synchronizing time
    timer.setInterval(10000L, clockDisplay);
    timer.setInterval(60000L, activetoday);  // check every 60s if ON / OFF trigger time has been reached
    setSyncInterval(10 * 60); // Sync interval in seconds (10 minutes)

}

void loop()
{
  Blynk.run();
  timer.run();
  LedWidget();
}
2 Likes

So I finally finished re write of the code.
The new code has the same features but number of schedulers and timers can be set using define (no limit on number of schedulers and timers). Keeping SCHEDULER_CNT == 4 and TIMER_CNT == 4 you are able to use the same app as before. If you change one of these defines you will need to reassign the Virtual Pins at the app.
Added system sleep mode that deactivate tasks (gpios) without deactivating schedulers (disable mode). During sleep all schedulers are active (only task is deactivated). When out of sleep if a scheduler is active the task will be turned ON.
Also added a hierarchy on the gpio activation to enable you to schedule tasks other than gpio activation.

Please let me know if you find any bugs.

/* Blynk SCHEDULER 
 *    This code implements a parametric scheduler to activate a task 
 *      at specific times and specific days . 
 *    The system operates at resolution of minuets. 
 *    Each scheduler can be assigned one task (i.e. turn on/off a gpio ...).
 *    The user can parametrize the number of schedulers. 
 *    The user can parametrize the number of timers assigned to each scheduler. 
 *    Each timer can be configured (from the app) to activate/deactivate 
 *      the task at specific start/end time and days of week 
 *    Each task can also be activated immediately and turned off after a 
 *      configurable default time per scheduler (from the app).  
 *      If end time is not given the task will be deactivated 
 *      after the scheduler default time. 
 *    Each scheduler can be configured (from app) with a max duration.  
 *      If when timer activates a task the max duration is smaller than
 *      the configured task duration then the task is activated for max duration.
 *      If max duration is smaller than 2min it is ignored.
 *    
 *    The system can be disabled for a configurable (from app) number of days. 
 *      When in sleep mode all active tasks and all schedulers are turned off. 
 *      If number of days is 0 all current active tasks will be turned off
 *      If number of days is 1 all tasks and all schedulers will be 
 *      turned off until midnight of today.
 *      If number of days is 2 all tasks and all schedulers will be 
 *      turned off until midnight of next day. ...
 *    The system can also be put in sleep (from app) .  
 *      When in sleep mode all active tasks are turned off. Schedulers continue working
 *      but will not activate tasks. Once out of sleep mode tasks are turned on if they 
 *      ware expected to be active without the sleep mode.
 *      if number of days is 0 the sleep button has no effect.
 *      if number of days is 1 the sleep mode continue until midnight.
 *      if number of days is 2 the sleep mode continue until midnight next day....
 *      
 */

/* Comment this out to disable prints and save space */
#define BLYNK_PRINT Serial

#include <ESP8266WiFi.h>
//#include <ESP8266mDNS.h>
//#include <WiFiUdp.h>
//#include <ArduinoOTA.h>
#include <BlynkSimpleEsp8266.h>
#include <TimeLib.h>
#include <WidgetRTC.h>

/////////////////////////////////////////////////////////////
// system configuration section
// The following section contains code that should be changed 
//  by user to match the exact system needs.
////////////////////////////////////////////////////////////
#define WIFI_LED 16

// define parameters for number of schedulers and number of timers
#define SCHEDULER_CNT 4
#define TIME_CNT 4

// This system uses gpio on of as tasks.
// The number of used gpio per task is given by task_gpio_pins array
//byte task_gpio_pins[] = {12 , 5 , 4 , 15}; // number of gpio to be used as task control
byte task_gpio_pins[] = {2 , 5 , 14 , 15}; // number of gpio to be used as task control

// The default value of the gpio for task off is given by task_gpio_default.
//  (used for gpio negative polarity)
bool task_gpio_default[] = {LOW,HIGH,LOW,HIGH}; 


// You should get Auth Token in the Blynk App.
// Go to the Project Settings (nut icon).
char auth[] = "xxxxxxxxxxxxxxxxxxxxxxxxxxxx";

// Your WiFi credentials.
// Set password to "" for open networks.
char ssid[] = "xxxxxx";
char pass[] = "xxxxxxxxxxxxxx";

//////////////////////////////////////////////////////////////////////////////////
// task activation function
// this is an abstraction layer for activating task
// currently only simple activation of gpio but user can change this
//////////////////////////////////////////////////////////////////////////////////
void task_on_off(bool task_state, char scheduler_num){
  bool gpio_val = task_state^task_gpio_default[scheduler_num];
  digitalWrite(task_gpio_pins[scheduler_num],gpio_val);
  if (task_state)
     Serial.println(String("Turn On Task: ") + (int)scheduler_num);
  else
     Serial.println(String("Turn Off Task: ") + (int)scheduler_num);
     

}
////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////
// end system configuration section                                               //
////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////

//////////////////////////////////////////////////////////////////////////////////
// The following defines will automatically assign virtual pins                 //
//  The first SCHEDULER_CNT*TIME_CNT virtual pins are assigned for time inputs  //
//   where virtual pins 0..TIME_CNT-1 are for scheduler 0                       //
//                pins TIME_CNT..2*TIME_CNT-1 for scheduler 1 ...               //
//  The following SCHEDULER_CNT pins are used for default duration sliders      //
//  The following SCHEDULER_CNT pins are used for immediate activation button   //
//  The following SCHEDULER_CNT pinst are used for maximum duration slider      //
//  Pin ( SCHEDULER_CNT*(TIME_CNT+3) ) +3 used for system sleep button          //
//  Pin ( SCHEDULER_CNT*(TIME_CNT+3) ) +4 used for system disable button        //
//  Pin ( SCHEDULER_CNT*(TIME_CNT+3) ) +5 used for slider of number of days     //
//    to disable system                                                         //
// User must ensure that number of used Virtual pins will not exceed 127        //
//  So the following rule must be kept when selecting SCHEDULER_CNT and TIME_CNT//
//                                                                              //
//                SCHEDULER_CNT*(TIME_CNT+3) < 123                              //
//                                                                              //
//////////////////////////////////////////////////////////////////////////////////
// define time inputs Virtual Pin (Time Input Widget) for timer 0 of scheduler 0 
#define VP_SWCH0_TIME0 V0

// define activation duration Virtual Pin (Slider Widget) for Scheduler 0 (V16)
#define VP_ACTV_DUR_SCHD0 (VP_SWCH0_TIME0+(TIME_CNT*SCHEDULER_CNT))

// define immidiate activation Virtual Pin (Button Widget) for Scheduler 0 (V20)
#define VP_IMMD_ACTV_SCHD0 (VP_ACTV_DUR_SCHD0+SCHEDULER_CNT)

// define max duration Virtual Pin (Slider Widget) Scheduler 0 (V24)
#define VP_MAX_DUR_SCHD0 (VP_IMMD_ACTV_SCHD0+SCHEDULER_CNT)

// define disable system input and number of days to disable input
#define VP_SLEEP_SYS (VP_MAX_DUR_SCHD0+SCHEDULER_CNT+3)
#define VP_DISBL_SYS (VP_MAX_DUR_SCHD0+SCHEDULER_CNT+4)
#define VP_DISBL_DAYS (VP_DISBL_SYS+1)

// maximum possible time in seconds using uint32_t (this is at year 2106)
#define TIME_MAX 0xFFFFFFFF


////////////////////////////////////////////////////////////////////////////////////////
// time input arrays
////////////////////////////////////////////////////////////////////////////////////////
int     start_time_sec[SCHEDULER_CNT][TIME_CNT];       // Array of TIME_CNT timers per scheduler containing 
                                                       //  minuet in day for scheduler activation                                                 
bool    weekdays[SCHEDULER_CNT][TIME_CNT][7];          // Array of 7 days (Sunday is day 0 ) for each time

 
int32_t active_duration_sec[SCHEDULER_CNT][TIME_CNT+1];// duration per time input (in sec) additional "timer" 
                                                       //  is used to save the default active duration

int32_t max_duration_sec[SCHEDULER_CNT];               // max duration per scheduler (in sec)


bool    system_sleep_mode = false;                     // current sleep mode  

uint32_t active_end_time_sec[SCHEDULER_CNT];           // current end time (in sec from 1970) per active scheduler
                                                       //  if scheduler is not active (no active task) this == TIME_MAX

uint32_t max_end_time_sec[SCHEDULER_CNT];              // current end time due to max active time time (in sec from 1970) 
                                                       //  per active scheduler 
                                                       // If scheduler is not active or max time =< 2 min this == TIME_MAX      

// saves id of main timer
uint8_t  main_timer_id;

// system disable timer
uint8_t  disable_timer_id = 32;                     // When system is disabled or at sleep a timer is set to wake 
                                                    //   the system disable_timer_id saves this timer ID

// system disable days 
int32_t  system_disable_days_sec;                  // VP_DISBL_DAYS defines days to disable/sleep the system (including today) given in sec

// timer object declaration
BlynkTimer SystemTimer;                            // for calling activetoday and for turning off sleep/disable modes

// this code use Real Time Clock widget in the blynk app to keep the clock updated from net
WidgetRTC rtc;

BLYNK_CONNECTED() {
  // Synchronize time on connection
  rtc.begin();
  Blynk.syncAll();
}

//////////////////////////////////////////////////////////////////////////////////////
// End of Sleep and Disable Modes                                                   //
//  (They will probably not be used together so if one ends the other must end too) //
//////////////////////////////////////////////////////////////////////////////////////
void sleep_disable_mode_off()
{
   system_sleep_mode = false;       
   // iterate over all schedulers and re reactivate tasks if needed (sleep mode).
   for (int scheduler_cnt = 0;  scheduler_cnt< SCHEDULER_CNT; scheduler_cnt++) 
      if(active_end_time_sec[scheduler_cnt]!=TIME_MAX) task_on_off(HIGH, scheduler_cnt); 

   // enable main timer (only if in disable mode)    
   SystemTimer.enable(main_timer_id); 
    
   // set disable and sleep buttons to off
   Blynk.virtualWrite(VP_DISBL_SYS,0);
   Blynk.virtualWrite(VP_SLEEP_SYS,0);

   // set disable timer id to 32 to prevent disabling system timer by mistake
   SystemTimer.deleteTimer(disable_timer_id);
   disable_timer_id = 32;
   
   Serial.println(String("Sleep/Disable mode off"));
}
//////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////
//////                 BLYNK_WRITE_DEFAULT                                 ///////
//////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////    
// BLYNK_WRITE_DEFAULT enables defining configurable number of schedulers and 
//  time inputs without changing the code (only change defines)
// BLYNK_WRITE_DEFAULT takes any write to virtual pin and parse it using request.pin 
BLYNK_WRITE_DEFAULT() { 
  uint8_t pin = request.pin;      // Which exactly pin is handled?
 

  
      
  ///////////////////////////////////////////////////////////////////////
  // DISABLE DAYS SLIDER INPUT                                         //
  ///////////////////////////////////////////////////////////////////////
  if (pin == VP_DISBL_DAYS) system_disable_days_sec = param.asInt()*86400; // V33

  ///////////////////////////////////////////////////////////////////////
  // SYSTEM DISABLE/SLEEP BUTTONS INPUT                                //
  ///////////////////////////////////////////////////////////////////////
  else if((pin == VP_DISBL_SYS )|(pin == VP_SLEEP_SYS)){                   // V31 & V32
       if (param.asInt()!=0){ 
        
          String currentDate = String(day()) + "\\" + month() + "\\" + year();
          
          if (pin == VP_SLEEP_SYS){
            system_sleep_mode = true;
            // turn off all tasks (do not disable the schedulers )
            for (int task_cnt = 0;  task_cnt< SCHEDULER_CNT; task_cnt++) 
              task_on_off(LOW, task_cnt);

            // print current data on VP_SLEEP_SYS button
            Blynk.setProperty(VP_SLEEP_SYS, "onLabel", currentDate);
          }
          else{
             // disable main time tick 
             SystemTimer.disable(main_timer_id);   
             // turn off all active schedulers  
             Serial.println(String("Disable all"));
             for (int scheduler_cnt = 0;  scheduler_cnt< SCHEDULER_CNT; scheduler_cnt++) 
               scheduler_turn_off(scheduler_cnt);
             // print current data on VP_SLEEP_SYS button
             Blynk.setProperty(VP_DISBL_SYS, "onLabel", currentDate);              
          }
          // if disable days is 0 then set nowseconds to -1 to prevent timer with negative duration  
          int32_t nowseconds = -1; 
          if (system_disable_days_sec!=0)
             nowseconds = elapsedSecsToday(now());
             
          // create a timer to wake the system up after system_disable_days_sec are over
          SystemTimer.deleteTimer(disable_timer_id); // make sure no disable timer is active
          disable_timer_id = SystemTimer.setTimeout(1000*(system_disable_days_sec-nowseconds),sleep_disable_mode_off);
        }
        else {// if system is manually out of seep mode delete disable timer and enable schedulers
          sleep_disable_mode_off();
        }
  }
  ////////////////////////////////////////////////////////////////////////////////////
  // MAX DURATION SLIDER INPUT                                                      //
  ////////////////////////////////////////////////////////////////////////////////////
  else if (pin >= VP_MAX_DUR_SCHD0) {                                       //V24..V27
     if (pin < VP_MAX_DUR_SCHD0+SCHEDULER_CNT){ 
      if(param.asInt()>2)
         max_duration_sec[pin-VP_MAX_DUR_SCHD0] = param.asInt()*60;
      else
         max_duration_sec[pin-VP_MAX_DUR_SCHD0] = 864000; // set to 10 days is equivalent to not setting max duration
     }
  }
  ///////////////////////////////////////////////////////////////////////////////////
  // SCHEDULER IMMEDIATE TASK ACTIVATION BUTTON INPUT                              //
  ///////////////////////////////////////////////////////////////////////////////////
  else if (pin >= VP_IMMD_ACTV_SCHD0) {                                     //V20..V23
    if ( pin <(VP_IMMD_ACTV_SCHD0+SCHEDULER_CNT) ) {
      if (param.asInt()==0)
        scheduler_turn_off(pin-VP_IMMD_ACTV_SCHD0);
      else           
        scheduler_turn_on(pin-VP_IMMD_ACTV_SCHD0,TIME_CNT); // immediate turn on using duration from slider
    }
  }
  ///////////////////////////////////////////////////////////////////////////////////
  // DEFAULT ACTIVE DURATION SLIDER INPUT                                          //
  ///////////////////////////////////////////////////////////////////////////////////
  else if (pin >= VP_ACTV_DUR_SCHD0) {                                       //V16..V19
    // default duration is written in extra "timer"
    if (pin <VP_ACTV_DUR_SCHD0+SCHEDULER_CNT) active_duration_sec[pin-VP_ACTV_DUR_SCHD0][TIME_CNT] = param.asInt()*60;
  }
  ///////////////////////////////////////////////////////////////////////////////////
  // TIME INPUT                                                                    //
  ///////////////////////////////////////////////////////////////////////////////////
  else if (pin >= VP_SWCH0_TIME0) {                                          // V0..V15
    int scheduler_num = (pin-VP_SWCH0_TIME0)/TIME_CNT;
    int time_num = (pin-VP_SWCH0_TIME0)%TIME_CNT;
    
    if ((scheduler_num<SCHEDULER_CNT) &( time_num<TIME_CNT)) {
       TimeInputParam t(param); // convert to time input parameter
       
       // Process start time
       start_time_sec[scheduler_num][time_num]= -1;  // start_time_sec of -1 indicate no start time is set (will never match current time)
  
       if (t.hasStartTime())
       {
         Serial.println(String("Start: ") + t.getStartHour() + ":" + t.getStartMinute() + ":" + t.getStartSecond());
         Serial.println(String("Start in sec: ") + param[0].asLong() + String(" for scheduler") + scheduler_num + String(" time_no: ") + time_num);
    
         start_time_sec[scheduler_num][time_num]=param[0].asLong();     
       }
       //////////////////////////////////////////////////////////////////////////////
       // check if end time is received convert and save it as day time in seconds //
       //////////////////////////////////////////////////////////////////////////////
       active_duration_sec[scheduler_num][time_num] = -1;
  
       if (t.hasStopTime())
       {
         Serial.println(String("Stop: ") + t.getStopHour() + ":" + t.getStopMinute() + ":" + t.getStopSecond());
         Serial.println(String("Stop duration: ") + (param[1].asLong() -param[0].asLong()));

         active_duration_sec[scheduler_num][time_num] = (param[1].asLong()-param[0].asLong());
   
         // if end time is smaller than start time this means end time is at next day
         if (active_duration_sec[scheduler_num][time_num]<=0) 
           active_duration_sec[scheduler_num][time_num] = 86400+active_duration_sec[scheduler_num][time_num];    
       }
    
       /////////////////////////////////////////////////////////////////////////////
       // Process weekdays (1. Mon, 2. Tue, 3. Wed, ...)                          //
       /////////////////////////////////////////////////////////////////////////////
       for (int i = 1; i <= 7; i++) {
         weekdays[scheduler_num][time_num][(i%7)] = t.isWeekdaySelected(i);
    
         if (t.isWeekdaySelected(i))  
           Serial.println(String("Day ") + i + " is selected"); 
       }
    
       Serial.println();
    }    
  }
} 
//////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////
//////             END  OF  BLYNK_WRITE_DEFAULT                            ///////
//////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////   

/////////////////////////////////////////////////////////////////
// Handle Scheduler events (from app or from timers )          //
/////////////////////////////////////////////////////////////////
// handle scheduler ON

void scheduler_turn_on(byte scheduler_num , byte time_no){

   uint32_t now_abs_time = now();
   
    // turn on the task 
    if (!system_sleep_mode) task_on_off(HIGH, scheduler_num);

    // if stop time is not defined use the duration from slider
    if(active_duration_sec[scheduler_num][time_no]>=0)
       active_end_time_sec[scheduler_num] = now_abs_time+active_duration_sec[scheduler_num][time_no];   
    else
       active_end_time_sec[scheduler_num] = now_abs_time+active_duration_sec[scheduler_num][TIME_CNT];  
 
    // max_end_time_sec is changed only if it was not previously set
    if (max_end_time_sec[scheduler_num]==TIME_MAX)
        max_end_time_sec[scheduler_num]=now_abs_time+max_duration_sec[scheduler_num];
          
    // create time as string to print on activation button
    char Time_print[10];
    sprintf(Time_print, "%02d:%02d", hour(), minute());
    
    // set button on and write the activation time on the button 
    Blynk.setProperty(VP_IMMD_ACTV_SCHD0+scheduler_num, "onLabel", String(Time_print));
    Blynk.virtualWrite(VP_IMMD_ACTV_SCHD0+scheduler_num,1);

    Serial.println(String("turn ON scheduler: ") + scheduler_num + String(" Until : ") + max_end_time_sec[scheduler_num]);
}

// handle scheduler OFF
void scheduler_turn_off(char scheduler_num ){
 
    // set task off
    task_on_off(LOW, scheduler_num);

    // delete associated timer
    active_end_time_sec[scheduler_num] = TIME_MAX;
    max_end_time_sec[scheduler_num] = TIME_MAX;
    
    Serial.println(String("turn OFF scheduler: ") + scheduler_num);

    // reset button
    Blynk.virtualWrite(scheduler_num + VP_IMMD_ACTV_SCHD0,0); 
}

/////////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////////
//// the following function is called every 10 seconds by a timer                            ////
////  the function checks if a start time is reached and if yes it will call                 ////
////   the scheduler_turn_on function (to turn on the scheduler and                          ////
////                                   set timer for turning off scheduler)                  ////
/////////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////////
uint32_t  last_abs_min = 0;     // saves the last minute that activetoday was started at
                                // this is later used to ensure activetoday will not be started twice at the same minute 

                                   
void activetoday(){         // check if schedule #1 should run today

  uint32_t now_abs_sec = now(); // time in seconds from 1970

  // set wifi led if no connection
  if(Blynk.connected()) 
    digitalWrite(WIFI_LED,HIGH);
  else
    digitalWrite(WIFI_LED,LOW);

   if(year() != 1970){ // if RTC was not started do nothing 
     uint32_t now_sec = elapsedSecsToday(now_abs_sec);
     uint32_t now_abs_min = now_abs_sec/60;

     for (uint8_t scheduler_cnt = 0;  scheduler_cnt< SCHEDULER_CNT; scheduler_cnt++) {
        ////////////////////////////////////////////
        // scheduler itteration 
        ////////////////////////////////////////////
      
        for (uint8_t timer_cnt = 0;  timer_cnt< TIME_CNT; timer_cnt++) {
          uint8_t tmp_day = 0x01<<weekday();
          ///////////////////////////////////////
          // Timer itteration
          //////////////////////////////////////
           if (now_abs_min != last_abs_min) {

             if ((weekdays[scheduler_cnt][timer_cnt][weekday()-1]==true) &
                 ((now_sec >= start_time_sec[scheduler_cnt][timer_cnt]) & (now_sec < (start_time_sec[scheduler_cnt][timer_cnt])+60))                  ){
                     /////////////////////////////////////
                     // activation
                     ////////////////////////////////////
                      scheduler_turn_on(scheduler_cnt,timer_cnt);
              }
            
          }
        }

       ///////////////////////////////////
       // De activation (per scheduler)
       //////////////////////////////////
       if ((now_abs_sec >= active_end_time_sec[scheduler_cnt])|
           (now_abs_sec >= max_end_time_sec[scheduler_cnt])   ){
                    scheduler_turn_off(scheduler_cnt);
       }  
     }

    last_abs_min = now_abs_min; // save last used min
   }
}

/////////////////////////////////////
// BLYNK 
void setup() {
  // Debug console
  Serial.begin(115200);  

  // reset tasks and system variables
  for (int i = 0; i<SCHEDULER_CNT ; i++){
    pinMode(task_gpio_pins[i], OUTPUT);  // set output pins
    task_on_off(LOW,i );                 // turn tasks off
     
    for (int j = 0; j<TIME_CNT ; j++) { // reset timer parameters
      active_duration_sec[i][j] = -1;     // duration of -1 indicate no stop time is set 
      start_time_sec[i][j]= -1;           // start_time_sec of -1 indicate no start time is set
    }
    
    active_duration_sec[i][TIME_CNT] = 0; // this is the duration from the slider
    active_end_time_sec[i] = TIME_MAX;    // TIME_MAX indicate scheduler is not active
    max_end_time_sec[i] = TIME_MAX;       // TIME_MAX indicate scheduler is not active
  }
  
  pinMode(WIFI_LED, OUTPUT);
  Blynk.begin(auth, ssid, pass);
  
  // active today is called every 10 secondes 
  // if scheduler is manualy activated the scheduler may be active for up to extra 10 seconds
  main_timer_id = SystemTimer.setInterval(10000L, activetoday);  
  setSyncInterval(10 * 60); // Sync interval in seconds (10 minutes)


  // Port defaults to 8266
//  ArduinoOTA.setPort(8266);

 // Hostname defaults to esp8266-[ChipID]
//  ArduinoOTA.setHostname("myesp8266");

 // No authentication by default
// ArduinoOTA.setPassword((const char *)"Hay6t7r)");

// ArduinoOTA.onStart([]() {
// Serial.println("Start");
// });
// ArduinoOTA.onEnd([]() {
// Serial.println("\nEnd");
// });
// ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
// Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
// });
// ArduinoOTA.onError([](ota_error_t error) {
// Serial.printf("Error[%u]: ", error);
// if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed");
// else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed");
// else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed");
// else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed");
// else if (error == OTA_END_ERROR) Serial.println("End Failed");
// });
// ArduinoOTA.begin();

 
}

void loop() {
//  ArduinoOTA.handle();
  Blynk.run();
  SystemTimer.run();
}

wow can u share a qr app code that would work with this. sorry im new to this thanks.

This is my latest app (very similar to previous one).

3 Likes

hagay—I was able to get your latest code to work on my hardware. There was some occurances that caused me some confusion.

The code pretty much worked with my existing build.

If I did not have the max time slider in my project, when I selected the manual buttons my relays would come on for 10s and go off regardless of my slider settings. Just putting in the max sliders resolved this.

When system disable an temp hold were both selected, I could still turn relays on in manual. That may be intentional. Just not expecting that.

I do have to admit, your new code kind of leaves me in the dust. If this was the first iteration I came across I don’t think I would be able to construct a Blynk interface and make the needed modifications to adapt the code to my hardware. I did learn quite a bit by comparing the two Versions of your code side by side. I do appreciate the efficiency and scalability of your code. THANK YOU FOR SHARING!!!

In the end I’ve opted to stay with the more simple version of your code. I’m now a bit distracted with modifying widget attributes (inspired by your usage of same) to make the GUI more useful by conveying more information without having to add more widgets. minimizing the number of widgets keeps the GUI cleaner

Below: Playing with assigning color and labels. Blynk’s app color selections are limited. I find the green color they use kind of annoying.

Not my final itteration. I’m thinking that if timer fires a relay the color of button would be green. If it is manually started the button color would be yellow.

Here’s my twist on @hagay project…long press manual on and off buttons, long press timer on and off buttons, numeric input widget for manual timer activation and styled button indicating current status of system ie. timers on or off, activation time etc color change for solenoid activation.

You can add your own colour by using the appropriate hex code with your code.

Hello hagay, I’m just modify and testing your code at entry 57, with my relays. It’s very nice, it does exactly
what I need.
What do you think about introducing something to get a daily/weekly history of past actions?
I was thinking about “Superchart” widget… just to make sure that my irrigation started regularly.

Terminal widget applied to hagay’s code. Much like serial print, terminal print sends messages to bunk app. I set ‘‘tis up to do time stamps so I can monitor my system.

Terminal window scrolls. Very easy to integrate into Hagay”s code. Secondary benefit is the widget header is written to by code so I used it to display cpu time and date. This gives me the connection health status with out taking up a lot of screen space.

Side note: the terminal display also revealed what appears to be random events wher all 4 relays get set to offf. I think this is happening if the ESP 8266 chip gets disconnected from the server and then reconnects? The only other time I see this is when my device first boots up.

When a relay turns on using the timer input widgets, it prints the calculated duration based on start time and end time. Hagay already built this into the code for serial print messages. So, all I had to do was copy and paste the pieces to into my terminal print message for it to deliver the desired message to my Blynk app.

2 Likes

I think I’m experiencing something similar whereby the system resets for an unknown reason. I thought it may have something to do with my mods to @hagay code. Haven’t had much time to look into it yet.

Thanks Arizona for the feedback.
My intention with the new code was to have a “future proof” code for all my next projects (not sure it is but at least I am enjoying :crazy_face: it). I can understand why you decided to remain with the old code.
For the max duration issue I forgot to reset the max_duration_sec array so I think if you add :
max_duration_sec[i] = 864000;
befor :
active_duration_sec[i][TIME_CNT] = 0;
it should solve it.

From what I know you can add the superchart widget to your app without any change to the code and just assign it with the required gpio.
The only problem with this is that it will not track the device status if the device loose connection.

I had some similar reset behaviour when I had too many serial printouts.
I think the blynk use a watchdog timer and when I had too many printouts it did not return to the void loop in time.
Setting serial speed to 115200 and reducing printouts solved it for me.
It may be that you have too many terminal printouts at once and see the same behaviour.

Ok thanks for the update. Very much appreciated…I’ll adjust and give feedback.

Hagay—once again thank you. I have both serial prints running and terminal prints. On my device I had to set serial print to 9600 else I only get random ASCII characters. The terminal print uses WiFi but I suspect your assessment is accurate and I am not returning to the loop in time —specially since I added in more prints and slowed down my serial print speed. I will investigate further.