#include <Wire.h>
#include <EEPROM.h>
#include <Button.h>
#include <RTClib.h>
#include <TimeLib.h>
#include <Timezone.h>
#include <LiquidCrystal_I2C.h>
#include <LowPower.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 voltage on the coil can spoil the relay
// This relay doesn't require power to keep its state
// R/S pins should be GROUNDED, not powered, use transistors
// I used ULN2803APG cuz I had a few, which works in simmilar way
// Project can be modified for 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
//  Hold force button to cycle between force modes
//
// Force mode is made for rare occasions, it's saved in EEPROM every time it changes
//
// 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 ========================

// Pins
#define RelaySet 7
#define RelayReset 8
#define SettingsBtn 12
#define UpBtn 11
#define DownBtn 10
#define ForceBtn 9
#define WakeupBtn 2

// const char RelaySet = 7;
// const char RelayReset = 8;
// const char SettingsBtn = 12;
// const char UpBtn = 11;
// const char DownBtn = 10;
// const char ForceBtn = 9;

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

// 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 = 550; // Relay trigger duration (ms)

// Force state: 0 - disabled, 1 - relay always ON, 2 - relay always OFF
char FSTATE = 0;

// 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);
Button fBtn(ForceBtn, 1000);

// EEPROM registers
const unsigned char RONH_EEPROM = 10;
const unsigned char RONM_EEPROM = 11;
const unsigned char ROFFH_EEPROM = 12;
const unsigned char ROFFM_EEPROM = 13;
const unsigned char FORCE_EEPROM = 14;

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

void validateValues() {
  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;
  }

  if (FSTATE > 2) {
    FSTATE = 2;
  }
  else if (FSTATE < 0) {
    FSTATE = 0;
  }
}

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);
  }
  if (EEPROM.read(FORCE_EEPROM) == 255) {
    EEPROM.write(FORCE_EEPROM, FSTATE);
  }
}

void loadTimerFromEEPROM() {
  EEPROM.get(RONH_EEPROM, RONH);
  EEPROM.get(RONM_EEPROM, RONM);
  EEPROM.get(ROFFH_EEPROM, ROFFH);
  EEPROM.get(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;
}

time_t time() {
  return TZ.toLocal(rtc.now().unixtime());
}

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;

void setRelay() {
  if (!relayState) {
    digitalWrite(RelaySet, HIGH);
    delay(RTD);
    digitalWrite(RelaySet, LOW);
    relayState = true;
    Serial.println("RELAY: ON.");
  }
}

void resetRelay() {
  if (relayState) {
    digitalWrite(RelayReset, HIGH);
    delay(RTD);
    digitalWrite(RelayReset, LOW);
    relayState = false;
    Serial.println("RELAY: OFF.");
  }
}

void updateRelay() {
  switch (FSTATE) {
    case 1:
      setRelay();
      break;
    case 2:
      resetRelay();
      break;
    case 0:
    default:
      if (inPeriod(time())) {
        setRelay();
      }
      else {
        resetRelay();
      }
  }
}

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 = time();

  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);
  switch (FSTATE) {
    case 1:
      lcd.print("FORCE Relay:  ON");
      break;
    case 2:
      lcd.print("FORCE Relay: OFF");
      break;
    case 0:
    default:
      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;
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);
}

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

void wakeUp() {
  //
}

void goToSleep() {
  Serial.println("ECO: Sleep");
  delay(200);
  LCDBL = false;
  lcd.clear();
  attachInterrupt(digitalPinToInterrupt(WakeupBtn), wakeUp, HIGH);
  LowPower.powerDown(SLEEP_FOREVER, ADC_OFF, BOD_OFF);
  
  detachInterrupt(0);
  LCDBL = true;
  LCDBLTime = millis();
  lcd.backlight();
  Serial.println("ECO: Wakeup");
}

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

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

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

  rtc.begin();

  ensureEEPROM();
  if (!RTImportant) {
    loadTimerFromEEPROM();
  }
  EEPROM.get(FORCE_EEPROM, FSTATE);
  validateValues();

  // Restore relay state
  relayState = inPeriod(time());

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

void loop() {
  updateRelay();

  // LCD & Buttons Control 
  short settingBtn = sBtn.checkPress();
  short forceBtn = fBtn.checkPress();
  
  if (settingBtn == 1) {
    LCDBLTime = millis();

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

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

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

    delay(150);
  }

  if (forceBtn == -1) {
    if (++FSTATE > 2)
      FSTATE = 0;

    EEPROM.update(FORCE_EEPROM, FSTATE);

    switch (FSTATE) {
      case 0:
        Serial.println("FORCE: Changed to DISABLED");
        break;
      case 1:
        Serial.println("FORCE: Changed to ALWAYS ON");
        break;
      case 2:
        Serial.println("FORCE: Changed to ALWAYS OFF");
        break;
    }
  }

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

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

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

  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.clear();
      lcd.noBacklight();
      LCDBL = false;

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