#include <Wire.h>
#include <EEPROM.h>
#include <Button.h>
#include <RTClib.h>
#include <TimeLib.h>
#include <Timezone.h>
#include <LiquidCrystal_I2C.h>

// Info ============================
// 
// ON and OFF time variables are default at start
// If RTImportant is false, they will save in EEPROM and restored at reset
// These variables can be changed with settings menu if necessary
//
// This projects supports Daylight Saving Time, see the settings and GitHub page to setup
// WOKWI incorrectly simulates time, so in simulation DST may apply over Timezone settings
//
// This projects uses Bistable Latching Relay, which uses 2 coils to operate the latch
// This type of relay requires 2 pins to operate each coil and specified trigger duration
// Consistant HIGH voltage on the coil can spoil the relay
// This relay doesn't require power to keep its state
// Project can be modified to simple relay in updateRelay function
// 
// Controls:
//  Press settings button to enable or disable LCD backlight and update
//  LCD ENABLED: Hold settings button to enter settings mode
//  SETTINGS MODE: Press settings button to switch between ON/OFF H/M values
//  SETTINGS MODE: Press Up/Down buttons to adjust the value
//  SETTINGS MODE: Hold settings button to exit settings mode
//
// If in settings mode with no interactions, exit after SMTimeout seconds
// If in standart mode with no interactions, turn of LCD after LCDBLTimeout seconds
//

// Settings ========================

// ON and OFF time
char RONH = 20;  // ON Hours
char RONM = 0;   // ON Minutes
char ROFFH = 6;  // OFF Hours
char ROFFM = 30; // OFF Minutes

// If true - timings above will always apply on reset, false - load from EEPROM
bool RTImportant = false;

// Enable LCD and settings buttons on reset
bool LCDBL = true;

// Timing
const unsigned short LCDBLTimeout = 120; // Timeout for the setting above (s), 0 - disabled
const unsigned short SMTimeout = 30; // Settings mode timeout (s), 0 - disabled
const unsigned short SUF = 1000;  // LCD Update (ms)
const unsigned char CSF = 30; // Clock Sync (s)

// The trigger duration is 0.5-2 seconds, it is recommended not to exceed 1 minute
// https://www.aliexpress.com/item/1005006365628246.html
const unsigned short RTD = 700; // Relay trigger duration (ms) 

// Pins
const char RelayReset = 4;
const char RelaySet = 3;
const char SettingsBtn = 12;
const char UpBtn = 11;
const char DownBtn = 10;

// Daylight saving time (Ukraine, Kyiv)
// https://github.com/JChristensen/Timezone
TimeChangeRule TZ_DST = {"EEST", Last, Sun, Mar, 3, +180};  // DST Start
TimeChangeRule TZ_Standart = {"EET", Last, Sun, Oct, 4, +120};  // DST End, Standart Start

// RUN Data (DON'T MODIFY!)
RTC_DS3231 rtc;
Timezone TZ(TZ_DST, TZ_Standart);
LiquidCrystal_I2C lcd(0x27,16,2);

// https://github.com/davidepalladino/Button-Arduino/tree/2.x.x
Button sBtn(SettingsBtn, 1000);
Button uBtn(UpBtn);
Button dBtn(DownBtn);

const unsigned char RONH_EEPROM = 10;
const unsigned char RONM_EEPROM = 11;
const unsigned char ROFFH_EEPROM = 12;
const unsigned char ROFFM_EEPROM = 13;

// Funcs ============================

void validateTimer() {
  if (RONH < 0) {
    RONH = 0;
  }
  else if (RONH > 23) {
    RONH = 23;
  }
  if (RONM < 0) {
    RONM = 0;
  }
  else if (RONM > 59) {
    RONM = 59;
  }

  if (ROFFH < 0) {
    ROFFH = 0;
  }
  else if (ROFFH > 23) {
    ROFFH = 23;
  }
  if (ROFFM < 0) {
    ROFFM = 0;
  }
  else if (ROFFM > 59) {
    ROFFM = 59;
  }
}

void ensureEEPROM() {
  if (EEPROM.read(RONH_EEPROM) == 255) {
    EEPROM.write(RONH_EEPROM, RONH);
  }
  if (EEPROM.read(RONM_EEPROM) == 255) {
    EEPROM.write(RONM_EEPROM, RONM);
  }
  if (EEPROM.read(ROFFH_EEPROM) == 255) {
    EEPROM.write(ROFFH_EEPROM, ROFFH);
  }
  if (EEPROM.read(ROFFM_EEPROM) == 255) {
    EEPROM.write(ROFFM_EEPROM, ROFFM);
  }
}

bool checkFrqDelay(unsigned long frequency, unsigned long* timer) {
  unsigned long rt = millis();
  unsigned long dif = rt - *timer;
  if (dif >= frequency || dif <= 0) {
    *timer = rt;
    return true;
  }
  return false;
}

unsigned long rtcSync = 0;
void syncClock() {
  if (!checkFrqDelay(CSF*1000, &rtcSync))
    return;

  time_t rtcEpoch = rtc.now().unixtime();
  time_t innerEpoch = now();

  setTime(rtcEpoch);
  
  Serial.print("RTC: SYNC, ~");
  Serial.print(rtcEpoch-innerEpoch);
  Serial.print("s lost.\n");
}

bool inPeriod(time_t t) {
  if (RONH <= ROFFH) {  // STR 12:30, END 17:30
    return ((hour(t) == RONH && minute(t) >= RONM) || hour(t) > RONH)
      && (hour(t) == ROFFH && minute(t) < ROFFM);
  }
  else {  // STR 20:30, END 6:30
    return ((hour(t) == RONH && minute(t) >= RONM) || hour(t) > RONH) 
      || ((hour(t) == ROFFH && minute(t) < ROFFM) || hour(t) < ROFFH);
  }
}

bool relayState = false;
void updateRelay() {
  time_t t = TZ.toLocal(now());

  // Latching Bistable Self-locking Relay
  if (inPeriod(t)) {
    if (!relayState) {
      digitalWrite(RelaySet, HIGH);
      delay(RTD);
      digitalWrite(RelaySet, LOW);
      relayState = true;
      Serial.println("RELAY: Set.");
    }
  }
  else {
    if (relayState) {
      digitalWrite(RelayReset, HIGH);
      delay(RTD);
      digitalWrite(RelayReset, LOW);
      relayState = false;
      Serial.println("RELAY: Reset.");
    }
  }
}

String padZero(int number, int padding) {
  String result = String(number);
  while (result.length() < padding) {
    result = "0" + result;
  }
  return result;
}

unsigned long lcdUpd = 0;
void LCDTime() {
  time_t t = TZ.toLocal(now());

  lcd.print("Time:   ");
  lcd.print(padZero(hour(t), 2));
  lcd.print(":");
  lcd.print(padZero(minute(t), 2));
  lcd.print(":");
  lcd.print(padZero(second(t), 2));
}

void LCDDefault() {
  if (!checkFrqDelay(SUF, &lcdUpd))
    return;

  lcd.clear();
  LCDTime();
  lcd.setCursor(0, 1);
  lcd.print("Relay ");
  if (!relayState) {
    lcd.print("ON:  ");
    lcd.print(padZero(RONH ,2));
    lcd.print(":");
    lcd.print(padZero(RONM ,2));
  }
  else {
    lcd.print("OFF: ");
    lcd.print(padZero(ROFFH ,2));
    lcd.print(":");
    lcd.print(padZero(ROFFM ,2));
  }
}

char SMPart = 0;
bool SMPartD = false;

void LCDPrintPart(short part) {
  bool forceDisplay = false;
  if (part != SMPart) {
    forceDisplay = true;
  }
  else {
    SMPartD = !SMPartD;
  }

  if (SMPartD || forceDisplay) {
    switch (part) {
      case 0:
        lcd.print(padZero(RONH, 2));
        break;
      case 1:
        lcd.print(padZero(RONM, 2));
        break;
      case 2:
        lcd.print(padZero(ROFFH, 2));
        break;
      case 3:
        lcd.print(padZero(ROFFM, 2));
        break;
    }
  }
  else {
    lcd.print("  ");
  }  
}

void LCDSettings() {
  if (!checkFrqDelay(350, &lcdUpd))
    return;
  
  lcd.clear();
  lcd.print("Relay  ON: ");
  LCDPrintPart(0);
  lcd.print(":");
  LCDPrintPart(1);
  lcd.setCursor(0, 1);
  lcd.print("Relay OFF: ");
  LCDPrintPart(2);
  lcd.print(":");
  LCDPrintPart(3);
}

// Run =============================

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

  pinMode(RelaySet, OUTPUT);
  pinMode(RelayReset, OUTPUT);

  rtc.begin();
  syncClock();

  validateTimer();

  if (!RTImportant) {
    ensureEEPROM();
    EEPROM.get(RONH_EEPROM, RONH);
    EEPROM.get(RONM_EEPROM, RONM);
    EEPROM.get(ROFFH_EEPROM, ROFFH);
    EEPROM.get(ROFFM_EEPROM, ROFFM);
  }

  lcd.init();
  if (LCDBL) {
    lcd.backlight();
  }
}

bool SM = 0;
unsigned long LCDBLTime = millis();

void loop() {
  syncClock();
  updateRelay();

  short settingBtn = sBtn.checkPress();

  // LCD & Buttons Control 
  if (settingBtn == 1) {
    LCDBLTime = millis();

    if (LCDBL) {
      lcd.noBacklight();
      LCDBL = false;

      Serial.println("LCD: Disable.");
    }
    else {
      lcd.backlight();
      LCDBL = true;

      Serial.println("LCD: Enable.");
    }

    delay(150);
  }

  // Disable LCD update and setting buttons if LCD is OFF
  if (!LCDBL) {
    return;
  }

  // Enter Settings Mode
  if (settingBtn == -1) {
    LCDBLTime = millis();

    if (SM == false) {
      SM = true;
      SMPart = 3;
    }
  }

  if (SM == false) {
    // Display default menu
    LCDDefault();
  }
  else {
    Serial.println("SETTINGS: Enter settings mode.");

    unsigned long settingsTime = millis();
    while(millis() - settingsTime < (unsigned long)SMTimeout*1000 && SMTimeout > 0) {
      short upBtn = uBtn.checkPress();
      short downBtn = dBtn.checkPress();
      settingBtn = sBtn.checkPress();

      // Change selected part (ON/OFF H/M)
      if (settingBtn == 1) {
        settingsTime = LCDBLTime = millis();
        
        if (++SMPart >= 4) {
          SMPart = 0;
        }

        delay(150);
      }
      else if (settingBtn == -1) {
        LCDBLTime = millis();

        // Exit Settings on hold
        break;
      }

      if (upBtn == 1) {
        settingsTime = LCDBLTime = millis();

        switch (SMPart) {
          case 0:
            if (++RONH >= 24) {
              RONH = 0;
            }
            break;
          case 1:
            if (++RONM >= 60) {
              RONM = 0;
            }
            break;
          case 2:
            if (++ROFFH >= 24) {
              ROFFH = 0;
            }
            break;
          case 3:
            if (++ROFFM >= 60) {
              ROFFM = 0;
            }
            break;
        }

        delay(150);
      }
      else if (downBtn == 1) {
        settingsTime = LCDBLTime = millis();

        switch (SMPart) {
          case 0:
            if (--RONH < 0) {
              RONH = 23;
            }
            break;
          case 1:
            if (--RONM < 0) {
              RONM = 59;
            }
            break;
          case 2:
            if (--ROFFH < 0) {
              ROFFH = 23;
            }
            break;
          case 3:
            if (--ROFFM < 0) {
              ROFFM = 59;
            }
            break;
        }

        delay(150);
      }

      LCDSettings();
    }

    SM = false;
    Serial.println("SETTINGS: Changes applied.");

    if (!RTImportant) {
      EEPROM.update(RONH_EEPROM, RONH);
      EEPROM.update(RONM_EEPROM, RONM);
      EEPROM.update(ROFFH_EEPROM, ROFFH);
      EEPROM.update(ROFFM_EEPROM, ROFFM);

      Serial.println("SETTINGS: Saved to EEPROM.");
    }
  }

  // Display & Settings Timeout
  if (LCDBL && LCDBLTimeout > 0) {
    if (millis() - LCDBLTime >= (unsigned long)LCDBLTimeout*1000) {
      lcd.noBacklight();
      LCDBL = false;

      Serial.println("LCD: Timeout.");
    }
  }
}
GND5VSDASCLSQWRTCDS1307+
latching-bistable-relayBreakout