#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.");
}
}
}