Simple 4 relay irrigation scheduler

Can you please share the qr code of the project?

Is there a problem with the QR code that has already been shared?

Pete.

Qr code invalid message

This is the sketch and qr code I tried.
https://community.blynk.cc/t/simple-4-relay-irrigation-scheduler/24047/23?u=matchless

Now i have tried a different sketch and code later in this thread and now it is working correctly.
Next challenge is te implement a moisture sensor wich switches a relais on and off

Hi,

I just join this community. I love this project and would like to implement it but using ESP32 instead of ESP8266. @hagay would it work on the fly with ESP32 or which part do I need to change. Would appreciate if you can point me to the right directions or some hints.

TIA.

Nik

You’d need to change these two lines for the ESP32 versions:

And you’d need to review/change the GPIO pins that are used to ensure that you’re using ones that are appropriate for the ESP32 (probably GPIO18 onwards).

This topic, and the topics/links that are provided as you read on give all the info you’ll need about GPIOs on the two board types:

Pete.

Thanks a lot. That should get me started.

Regards

Nik

I have clone the github code and upon trying to compile it (no changes made whatsoever yet),I got these error messages. Can’t figure out what are they. Please help.

Note: I am trying to compile it for ESP8266 board, have not try to change it to ESP32 yet.

    Arduino: 1.8.12 (Mac OS X), Board: "NodeMCU 0.9 (ESP-12 Module), 80 MHz, Flash, Legacy (new can return nullptr), All SSL ciphers (most compatible), 4MB (FS:2MB OTA:~1019KB), v2 Lower Memory, Disabled, None, Only Sketch, 115200"

In file included from /Users/nikfahmi/Library/Arduino15/packages/esp8266/hardware/esp8266/2.7.1/tools/sdk/libc/xtensa-lx106-elf/include/sys/stdio.h:6:0,
                 from /Users/nikfahmi/Library/Arduino15/packages/esp8266/hardware/esp8266/2.7.1/tools/sdk/libc/xtensa-lx106-elf/include/stdio.h:63,
                 from /Users/nikfahmi/Library/Arduino15/packages/esp8266/hardware/esp8266/2.7.1/cores/esp8266/Arduino.h:32,
                 from sketch/ESP8266_scheduler.ino.cpp:1:
/Users/nikfahmi/Library/Arduino15/packages/esp8266/hardware/esp8266/2.7.1/tools/sdk/libc/xtensa-lx106-elf/include/sys/pgmspace.h:25:130: error: 'const char HTTP_HEAD []' redeclared as different kind of symbol
   #define PROGMEM __attribute__((section( "\".irom.text." __FILE__ "." __STRINGIZE(__LINE__) "."  __STRINGIZE(__COUNTER__) "\"")))
                                                                                                                                  ^
/Users/nikfahmi/Documents/Arduino/libraries/WhareHauoraWiFiManager/WiFiManager.h:25:24: note: in expansion of macro 'PROGMEM'
 const char HTTP_HEAD[] PROGMEM            = "<!DOCTYPE html><html lang=\"en\"><head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1, user-scalable=no\"/><title>{v}</title>";
                        ^
In file included from /Users/nikfahmi/Library/Arduino15/packages/esp8266/hardware/esp8266/2.7.1/libraries/ESP8266HTTPUpdateServer/src/ESP8266HTTPUpdateServer.h:4:0,
                 from /Users/nikfahmi/Documents/GitHub/ESP8266_scheduler/ESP8266_scheduler.ino:70:
/Users/nikfahmi/Library/Arduino15/packages/esp8266/hardware/esp8266/2.7.1/libraries/ESP8266WebServer/src/ESP8266WebServer.h:34:39: error: previous declaration of 'HTTPMethod HTTP_HEAD'
 enum HTTPMethod { HTTP_ANY, HTTP_GET, HTTP_HEAD, HTTP_POST, HTTP_PUT, HTTP_PATCH, HTTP_DELETE, HTTP_OPTIONS };
                                       ^
exit status 1
Error compiling for board NodeMCU 0.9 (ESP-12 Module).

This report would have more information with
"Show verbose output during compilation"
option enabled in File -> Preferences.

The code from here:


compiles fine for me with ESP Core 2.7.1 (which is what you are using) and WiFiManager 0.15.0

What version of WiFiManager do you have installed?

Pete.

Thanks so much Pete. Apparently I have installed the wrong WifiManager. The code now compiled ok after installing the right version you mentioned.

Thanks again.

Nik

Well, I got the code compiled ok and managed to upload it to my Nodemcu V3. I thought everything was ok and today I tried the Blynk app… scan the QR code into Blynk app, paid some energy to get it going but then I hit a snag.

The app was working for a few minutes then it said it is offline. Checked the serial monitor and found this msg…

deactivate all schedulers
     ets Jan  8 2013,rst cause:2, boot mode:(3,7)

    load 0x4010f000, len 3456, room 16 
    tail 0
    chksum 0x84
    csum 0x84
    va5432625
    ~ld
    dZmXy3nPvZHIU9NCHrjLC1hfFgu7XxlL
    blynk-cloud.com
    8442
    *WM: Adding parameter
    *WM: 
    *WM: Adding parameter
    *WM: blynk-token
    *WM: Adding parameter
    *WM: blynk-server
    *WM: Adding parameter
    *WM: blynk-port
    *WM: 
    *WM: AutoConnect
    *WM: Connecting as wifi client...
    *WM: Status:
    *WM: 0
    *WM: No saved credentials
    *WM: Connection result: 
    *WM: 0
    Entered config mode
    192.168.4.1
    AutoConnectAP

It seems the Nodemcu reset and it wiped the autoconn data to login to my router. I went in again into AutoConnectAP to save my ssid and password, it will work again for a minute then it start to happen all over again.

What is wrong with this… is my NodeMCU faulty? Or it is because I haven’t add any relay yet to it as I’m still waiting for the relays to arrive from China.

Still trying to troubleshoot the above. Is gpio used correct {14,24,24,24}?

[Unformatted text removed by moderator]

Hi all.
I just got the code working on a node mcu and breadboard. Works great! Many thanks!
Is there way i can convert the basic code so one schedule will run sequential after the previous has finished?
i know i can use separate star times for each schedule but it is more practical to schedule one timer and have the rest run sequential.
The problem is that i cant have all three zones of my garden “on” at the same time.
Thanks

You need to know when each schedule ends .
Maybe you can use a flag to tell “first schedule stop” and so on ?
Or you can use a nested timer ?

This my code to turn off all lights with one button with 400 ms sequences

BLYNK_WRITE(V10)// ALL OFF
{
  timer.setTimeout(400L, []() {
    if (BTN1 == true) {
      mySwitch.send(SWITCH_1_OFF, PACKET_LENGTH);
      Blynk.virtualWrite(V1, LOW);
      BTN1 = false;
    }
    timer.setTimeout(400L, []() {
      if (BTN2 == true) {
        mySwitch.send(SWITCH_2_OFF, PACKET_LENGTH);
        Blynk.virtualWrite(V2, LOW);
        BTN2 = false;
      }
      timer.setTimeout(400L, []() {
        if (BTN3 == true) {
          mySwitch.send(SWITCH_3_OFF, PACKET_LENGTH);
          Blynk.virtualWrite(V3, LOW);
          BTN3 = false;
        }
        timer.setTimeout(400L, []() {
          if (BTN4 == true) {
            mySwitch.send(SWITCH_4_OFF, PACKET_LENGTH);
            Blynk.virtualWrite(V4, LOW);
            BTN4 = false;
          }
          timer.setTimeout(400L, []() {
            if (BTN5 == true) {
              mySwitch.send(SWITCH_5_OFF, PACKET_LENGTH);
              Blynk.virtualWrite(V5, LOW);
              BTN4 = false;
            }
            timer.setTimeout(400L, []() {
              if (BTN6 == true) {
                mySwitch.send(SWITCH_6_OFF, PACKET_LENGTH);
                Blynk.virtualWrite(V6, LOW);
                BTN4 = false;
              }
              timer.setTimeout(400L, []() {
                if (BTN7 == true) {
                  mySwitch.send(SWITCH_7_OFF, PACKET_LENGTH);
                  Blynk.virtualWrite(V7, LOW);
                  BTN4 = false;
                }
                timer.setTimeout(400L, []() {
                  if (BTN8 == true) {
                    mySwitch.send(SWITCH_8_OFF, PACKET_LENGTH);
                    Blynk.virtualWrite(V8, LOW);
                    BTN4 = false;
                  }
                  Blynk.virtualWrite(V10, LOW);
                });  // END Timer Function
              });  // END Timer Function
            });  // END Timer Function
          });  // END Timer Function
        });  // END Timer Function
      });  // END Timer Function
    });  // END Timer Function
  });  // END Timer Function
}

Thanks!
I will go through it and have a try.
another question.
If the wifi drops, will the timer follow the last schedule or it will stop?

1 Like

hi vortex_kl did you solve this issue?
I have the same problem. It is running fine for may be one minute and then drops settings…
Thx in advance!

Hi.
I didn’t. I just program each zone so it’s starts after the previous ends

With honor to the Hagay code (circa 2018), I have attempted to adapt this succinct coding to a port extender (MCP23017) on a LOLIN D1 mini and an OLED 64X48 display. With my limited code skills I managed to create an operating version towards schedule controlling 12 relays and displaying the time and relay number on the OLED.

However, while finalizing testing it seems that as I change “defines” SCHEDULER _CNT & TIME_CNT the relays beyond 4 (or mcp_pin_PA4…) in the port extender, the devices fail to operate. (compiles fine)

Your good coaching would be sincerely appreciated as I have battled this for several days.

Code follows:

/* 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 <Blynk.h>
#include <ESP8266WiFi.h>
#include <BlynkSimpleEsp8266.h>
#include <TimeLib.h>
#include <WidgetRTC.h>
#include <Wire.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_MCP23017.h>

Adafruit_MCP23017 mcp;

// Define friendly names for the pins...

byte mcp_pin_PA0 = 0;    
byte mcp_pin_PA1 = 1;
byte mcp_pin_PA2 = 2;
byte mcp_pin_PA3 = 3;
byte mcp_pin_PA4 = 4;
byte mcp_pin_PA5 = 5;
byte mcp_pin_PA6 = 6;
byte mcp_pin_PA7 = 7;
byte mcp_pin_PB0 = 8;
byte mcp_pin_PB1 = 9;
byte mcp_pin_PB2 = 10;
byte mcp_pin_PB3 = 11;
byte mcp_pin_PB4 = 12;
byte mcp_pin_PB5 = 13;
byte mcp_pin_PB6 = 14;
byte mcp_pin_PB7 = 15;

/////////////////////////////////////////////////////////////
// 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[] = {0 , 1 , 2 , 3 , 4 , 5, 6 , 7 , 8 , 9 , 10 , 11 }; // number of MCP23017 "gpio" to be used as switch/relay control
byte task_gpio_pins[] = {mcp_pin_PA0 , mcp_pin_PA1 , mcp_pin_PA2 , mcp_pin_PA3 , mcp_pin_PA4 , mcp_pin_PA5, mcp_pin_PA6 , mcp_pin_PA7 , mcp_pin_PB0 , mcp_pin_PB1, mcp_pin_PB2 , mcp_pin_PB3 }; // number of MCP23017 "gpio" to be used as switch/relay 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[] = {HIGH, HIGH, HIGH, HIGH , HIGH, HIGH, HIGH, HIGH , HIGH, HIGH, HIGH, HIGH}; // switches that use reverse polarity should be set to HIGH here

char relay  ;      // GLOBAL VARIABLE TO DISPLAY ON OLED, I ATTEMPT TO CHANGE VALUE AND ASSIGN TO "RELAY" IN THE "handle switch state" loop

char auth[] = "REDACTED";   //LOLIN D1 Mini
char ssid[] = "REDACTED";
char pass[] = "REDACTED";

#define SCREEN_WIDTH 64 // OLED display width, in pixels
#define SCREEN_HEIGHT 48 // OLED display height, in pixels

// Declaration for an SSD1306 display connected to I2C (SDA, SCL pins)
#define OLED_RESET 0  // Default OLED Shield
Adafruit_SSD1306 display(OLED_RESET);

//////////////////////////////////////////////////////////////////////////////////
// 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];
  mcp.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]);
    relay = (scheduler_num + 1);
}

// 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);
    relay = 0;
    // 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())
      mcp.digitalWrite(WIFI_LED, HIGH);
    else
      mcp.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
    }
  }

  void OLED_SSD1306()
  {
    // clear display
    display.clearDisplay();

    //display
    display.setTextSize(1);
    display.setCursor(0, 0);
    display.print("TIME");
    display.setTextSize(1);
    display.setCursor(0, 10);
    display.print(String(hour()) + ":" + minute() + ":" + second());
    display.print(" ");
    display.setTextSize(1);
    display.setCursor(0, 30);
    display.print("RELAY");
    display.setTextSize(2);
    display.setCursor(34, 25);
    display.printf("%2d", relay);
    display.display();
  }

  /////////////////////////////////////
  // BLYNK
  void setup() {

    //  Wire.begin(0,2); //SDA, SCL  (reserved for applications of ESP8266-01, et. al.)

    // Debug console
    Serial.begin(115200);

    // Initialize the MCP23017
    mcp.begin(7);      // use hardware default address 7

    for (int i = 0; i <= 12; i++) {
      mcp.pinMode(i, OUTPUT);
    }

    // reset tasks and system variables
    for (int i = 0; i < SCHEDULER_CNT ; i++) {
      mcp.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
      }

      max_duration_sec[i] = 864000;
      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)

    display.begin(SSD1306_SWITCHCAPVCC, 0x3C);  // initialize with the I2C addr 0x3C (for the 64x48)
    delay(2000);
    display.clearDisplay();
    display.setTextColor(WHITE);

  }

  void loop() {

    Blynk.run();
    SystemTimer.run();
    OLED_SSD1306();
  }

I’d suggest that you add some serial print messages to see exactly what values you are seeing for the pin local variable within the BLYNK_WRITE_DEFAULT function and which of the if statements is being evaluated as true within this function.

Some other strategically places serial print statements would also help provide you with valuable insights.

Pete.

Many thanks to Pete & Hagay for the coaching and succinct code examples. After some persistent study, I found the initial code attempts pretty close, however I MAY have discovered an anomaly in the MCP23017 (at least the one I have), wherein I need to initialize ALL of the mcp_pins (16) such that mcp_pin_SB7 would function in the selected state for the timed intervals (it was switching “state” randomly).

Anyway, please find following a reasonably faithful- 12 relay setup that seems to operate all timers, sleep modes, disable modes, and manual on/off properly. Probably easily modified to operate other devices using all 16 available port extender OUTPUTS. (as written, no provisions for INPUTS but easily changed in “setup” et. al.)

Also, please note in header, my modifications to “WidgetTimeInput.h” library.

Thanks again code wizards, my working sketch follows:

/* MODIFIED C:\Users\Redacted\Documents\Arduino\libraries\Blynk\src WidgetTimeInput.h
  replace “-1” of the code “mWeekdays = -1;” by “0”. You can find the code in line “33” of file “WidgetTimeInput.h” (Version: Aug 2016, respectively 14 Sep 2016).*/
/* 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 <Blynk.h>
#include <ESP8266WiFi.h>
#include <BlynkSimpleEsp8266.h>
#include <TimeLib.h>
#include <WidgetRTC.h>
#include <Wire.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_MCP23017.h>

Adafruit_MCP23017 mcp;

// Define friendly names for the pins...

byte mcp_pin_PA0 = 0;    
byte mcp_pin_PA1 = 1;
byte mcp_pin_PA2 = 2;
byte mcp_pin_PA3 = 3;
byte mcp_pin_PA4 = 4;
byte mcp_pin_PA5 = 5;
byte mcp_pin_PA6 = 6;
byte mcp_pin_PA7 = 7;
byte mcp_pin_PB0 = 8;
byte mcp_pin_PB1 = 9;
byte mcp_pin_PB2 = 10;
byte mcp_pin_PB3 = 11;
byte mcp_pin_PB4 = 12;
byte mcp_pin_PB5 = 13;
byte mcp_pin_PB6 = 14;
byte mcp_pin_PB7 = 15;

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

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

// 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[] = {0 , 1 , 2 , 3 , 4 , 5, 6 , 7 , 8 , 9 , 10 , 11 }; // number of MCP23017 "gpio" to be used as switch/relay control
byte task_gpio_pins[] = {mcp_pin_PA0 , mcp_pin_PA1 , mcp_pin_PA2 , mcp_pin_PA3 , mcp_pin_PA4 , mcp_pin_PA5, mcp_pin_PA6 , mcp_pin_PA7 , mcp_pin_PB0 , mcp_pin_PB1, mcp_pin_PB2 , mcp_pin_PB3 }; // number of MCP23017 "gpio" to be used as switch/relay 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[] = {HIGH, HIGH, HIGH, HIGH , HIGH, HIGH, HIGH, HIGH , HIGH, HIGH, HIGH, HIGH}; // switches that use reverse polarity should be set to HIGH here

char relay  ;      // GLOBAL VARIABLE TO DISPLAY ON OLED, I ATTEMPT TO CHANGE VALUE AND ASSIGN TO "RELAY" IN THE "handle switch state" loop

char auth[] = "Redacted";   //LOLIN D1 Mini
char ssid[] = "Redacted";
char pass[] = "Redacted";

#define SCREEN_WIDTH 64 // OLED display width, in pixels
#define SCREEN_HEIGHT 48 // OLED display height, in pixels

// Declaration for an SSD1306 display connected to I2C (SDA, SCL pins)
#define OLED_RESET 0  // Default OLED Shield
Adafruit_SSD1306 display(OLED_RESET);

//////////////////////////////////////////////////////////////////////////////////
// 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];
  mcp.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 ...                   V0-V23 //
//  The following SCHEDULER_CNT pins are used for default duration sliders        V24-V35 //
//  The following SCHEDULER_CNT pins are used for immediate activation           V36-V47 //
//  The following SCHEDULER_CNT pinst are used for maximum duration slider    V48-V59//
//  Pin ( SCHEDULER_CNT*(TIME_CNT+3) ) +3 used for system sleep button       V63//
//  Pin ( SCHEDULER_CNT*(TIME_CNT+3) ) +4 used for system disable button    V64//
//  Pin ( SCHEDULER_CNT*(TIME_CNT+3) ) +5 used for slider of number days    V65//
//    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
//  minutes 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]);
    relay = (scheduler_num + 1);
}

// 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);
    relay = 0;
    // 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())
      mcp.digitalWrite(WIFI_LED, LOW);
    else
      mcp.digitalWrite(WIFI_LED, HIGH);

    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 iteration
        ////////////////////////////////////////////

        for (uint8_t timer_cnt = 0;  timer_cnt < TIME_CNT; timer_cnt++) {
          uint8_t tmp_day = 0x01 << weekday();
          ///////////////////////////////////////
          // Timer iteration
          //////////////////////////////////////
          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
    }
  }

  void OLED_SSD1306()
  {
    // clear display
    display.clearDisplay();

    //display
    display.setTextSize(1);
    display.setCursor(0, 0);
    display.print("TIME");
    display.setTextSize(1);
    display.setCursor(0, 10);
    display.print(String(hour()) + ":" + minute() + ":" + second());
    display.print(" ");
    display.setTextSize(1);
    display.setCursor(0, 30);
    display.print("RELAY");
    display.setTextSize(2);
    display.setCursor(34, 25);
    display.printf("%2d", relay);
    display.display();
  }

  /////////////////////////////////////
  // BLYNK
  void setup() {

    //  Wire.begin(0,2); //SDA, SCL  (reserved for applications of ESP8266-01, et. al.)

    // Debug console
    Serial.begin(115200);

    // Initialize the MCP23017
    mcp.begin(7);      // use hardware default address 7

    for (int i = 0; i <= 16; i++) {
      mcp.pinMode(i, OUTPUT);
    }

    for (int i = 0; i <= 16; i++) {
      mcp.pullUp(i, HIGH);  // turn on a 100K pullup internally
    }
    
    for (int i = 0; i <= 16; i++) {
      mcp.digitalWrite(i, HIGH);
    }

    // reset tasks and system variables
    for (int i = 0; i < SCHEDULER_CNT ; i++) {
      mcp.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
      }

      max_duration_sec[i] = 864000;
      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
    }

    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)

    display.begin(SSD1306_SWITCHCAPVCC, 0x3C);  // initialize with the I2C addr 0x3C (for the 64x48)
    delay(2000);
    display.clearDisplay();
    display.setTextColor(WHITE);

  }

  void loop() {

    Blynk.run();
    SystemTimer.run();
    OLED_SSD1306();
  }