Michael Cartwright
Published © LGPL

Sunset Switch - Overkill

Over engineered sunset switch for exterior LED lights. Gets time of day and sunset time updates from web. Ode to the Wemos D1 Mini..

IntermediateFull instructions provided4 hours497
Sunset Switch - Overkill

Things used in this project

Hardware components

Adafruit MAX9814
×1
Arduino Compatible 5V Relay Board
×1
Wemos LOLIN D1 mini
×1
Linear Regulator (7805)
Linear Regulator (7805)
×1
0.96" OLED 64x128 Display Module
ElectroPeak 0.96" OLED 64x128 Display Module
×1

Software apps and online services

Arduino IDE
Arduino IDE

Story

Read more

Schematics

Sunset Switch Schematic

Code

Sunset Switch Arduino Sketch

Arduino
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>


#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ESP8266WebServer.h>
#include <ESP8266mDNS.h>

#include <ArduinoJson.h>

// Comment this out once you've finished debugging
#define SERIAL_MONITOR

#define STASSID "##########"
#define STAPSK  "##########"

#define SWITCH_PIN 0 // D3 on Wemos D1 Mini
#define KNOCK_PIN 17 // A0 on Wemos D1 Mini



// Start with a low value and test what works in your environment via the Serial Monitor
const int threshold = 750; 
int inputFromMAX9814 = 0;

const int secondsOLEDOn = 15;     // how long to keep the OLED visible after it is turned on
const int secondsOLEDRefresh = 1; // refresh the OLED (when visible) every <n> seconds

const unsigned long twentyFourHours = 24*60*60;
bool activeOLED = true;
unsigned long timeOLEDActivated = 0;
unsigned long timeLastScreenUpdate = 0;
unsigned long timeLastWebCheck = 0;
unsigned long timeWebBase = 0;
unsigned long sunsetWebBase = 0;
long onAfterSunset = (3*60 + 30) * 60; // 3.5 hours
long onOverride = 0;

// ########## OLED code ##########

#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
// Declaration for an SSD1306 display connected to I2C (SDA, SCL pins)
#define OLED_RESET     -1 // Reset pin # (or -1 if sharing Arduino reset pin)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

void printOLED(int iColumn, int iRow, const char * text)
{
  // https://en.wikipedia.org/wiki/Code_page_437
  int x = iColumn * 4;
  int y = iRow * 9;
  display.setCursor(x, y); // top left is 0,0
  int length = strlen(text);
  for
  (int i=0; i<length; i++) 
  {
    display.write(text[i]);
  }
  display.display();
}

void clearScreen()
{
  display.clearDisplay();
}

String GetLocalIP();

void initOLED()
{
  // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) 
  { 
#ifdef SERIAL_MONITOR
    Serial.println(F("SSD1306 allocation failed"));
#endif
    for(;;); // Don't proceed, loop forever
  }
  else
  {
    display.clearDisplay();
    display.setTextSize(1);      // Normal 1:1 pixel scale
    display.setTextColor(WHITE); // Draw white text
    display.setCursor(0, 0);     // Start at top-left corner
    display.cp437(true);         // Use full 256 char 'Code Page 437' font (https://en.wikipedia.org/wiki/Code_page_437)
    display.display();
    printOLED(0, 0, "Initializing:");
    printOLED(2, 1, "OLED");
  }
}

// ########## end of OLED code ##########

// ########## Web Server code ##########

const int httpPort = 80;
ESP8266WebServer server(httpPort);

bool ConnectToWifi(const char *  ssid, const char *  password)
{
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
#ifdef SERIAL_MONITOR  
  Serial.println("");
#endif
  int count = 0;
  while (WiFi.status() != WL_CONNECTED) 
  {
    delay(500);
#ifdef SERIAL_MONITOR    
    Serial.print(".");
#endif    
    count++;
    if (count > 20)
    {
      return false;
    }
  }
  return true;
}

void SendResponse(int httpCode, String message)
{
  server.send(httpCode, "text/plain", message);
}
bool HasArg(String name)
{
  return server.hasArg(name);
}
String GetArg(String name)
{
  return server.arg(name);
}
String GetLocalIP()
{
  String localIP = WiFi.localIP().toString();
  return localIP;
}

void handleNotFound() 
{
  String message = "File Not Found\n\n";
  message += "URI: ";
  message += server.uri();
  message += "\n";
  SendResponse(404, message);
}
void handleRoot() 
{
  String message;
  unsigned long timeNow = millis() / 1000;
  
  
  unsigned long secondsSinceLastCheck = timeNow - timeLastWebCheck;
  unsigned long currentTime = timeWebBase + secondsSinceLastCheck;
  while (currentTime > twentyFourHours)
  {
    currentTime = currentTime - twentyFourHours;
  }
  
  bool redirect = false;
  if (server.hasArg("increase"))
  {
    onAfterSunset += 30 * 60;
    redirect = true;
  }
  if (server.hasArg("decrease"))
  {
    onAfterSunset -= 30 * 60;
    if (onAfterSunset < 0)
    {
      onAfterSunset = 0;
    }
    redirect = true;
  }
  if (server.hasArg("override"))
  {
    onOverride += 5 * 60;
    redirect = true;
  }
  if (redirect)
  {
    message += "<html>";
    message += "<head>";
    message += "  <title>HTML Meta Tag</title>";
    message += "  <meta http-equiv=\"refresh\" content=\"0; url=http://" + GetLocalIP() + "\" />";
    message += "<style>";
    message += "body {\n";
    message += "  background-color: #005C5F;\n";
    message += "}\n";
    message += "</style>";
    message += "</head>";
    message += "<body>";
    message += "  <p>Reloading</p>";
    message += "</body>";
    message += "</html>";
    server.send(200, "text/html", message);
    return;
  }
  
  
  
  message += "<html>";
  message += "<head>";
  message += "<style>";
  message += "body {\n";
  message += "  background-color: #005C5F;\n";
  message += "}\n";
  message += "h3 {\n";
  message += "  font-weight: 600;\n";
  message += "  font-size: 30pt;\n";
  message += "  color: #FFFFFF;\n";
  message += "  font-family: sans-serif;";
  message += "}\n";
  message += "p, body {\n";
  message += "  font-weight: 600;\n";
  message += "  font-size: 18pt;\n";
  message += "  color: #FFFFFF;\n";
  message += "  font-family: sans-serif;";
  message += "}\n";
  message += "</style>\n";
  message += "</head>";
  message += "<body>";
  message += "<h3>Sunset Switch</h3>";
  message += "<table>";
  message += "<tr>";
  message += "<td>";
  message += "Time:";
  message += "</td>";
  message += "<td>";

  int hours = currentTime / (60*60);
  int minutes = (currentTime / 60) % 60;
  int seconds = currentTime % 60;
  char text[128];
  sprintf(text, "%02d:%02d:%02d", hours, minutes, seconds);

  message += text;
  message += "</td>";
  message += "</tr>";
  message += "<tr>";
  message += "<td>";
  message += "Sunset:";
  message += "</td>";
  message += "<td>";

  hours = sunsetWebBase / (60*60);
  minutes = (sunsetWebBase / 60) % 60;
  seconds = sunsetWebBase % 60;
  sprintf(text, "%02d:%02d:%02d", hours, minutes, seconds);
  
  message += text;
  message += "</td>";
  message += "</tr>";
  message += "<tr>";
  message += "<td>";
  message += "&nbsp;";
  message += "</td>";
  message += "</tr>";
  message += "<tr>";
  message += "<td>";
  message += "On time:";
  message += "</td>";
  message += "<td>";

  hours = onAfterSunset / (60*60);
  minutes = (onAfterSunset / 60) % 60;
  sprintf(text, "+%02d:%02d:00", hours, minutes);
  
  message += text;
  message += "<td>";
  message += "<button onclick=\"window.location.href='/?decrease=1';\">";
  message += "Decrease";
  message += "</button>";
  message += "</td>";
  message += "<td>";
  message += "<button onclick=\"window.location.href='/?increase=1';\">";
  message += "Increase";
  message += "</button>";
  message += "</td>";
  message += "</td>";
  message += "</tr>";

  message += "<tr>";
  message += "<td>";
  message += "&nbsp;";
  message += "</td>";
  message += "</tr>";
  message += "<tr>";
  message += "<td>";
  message += "Override:";
  message += "</td>";
  message += "<td>";

  hours = onOverride / (60*60);
  minutes = (onOverride / 60) % 60;
  seconds = onOverride % 60;
  sprintf(text, "+%02d:%02d:%02d", hours, minutes, seconds);
  
  message += text;
  message += "<td>";
  message += "&nbsp;";
  message += "</td>";
  message += "<td>";
  message += "<button onclick=\"window.location.href='/?override=1';\">";
  message += "Increase";
  message += "</button>";
  message += "</td>";
  message += "</td>";
  message += "</tr>";
  message += "</table>";
  message += "<p>";

  message += "</p>";
  message += "</body>";
  message += "</html>";
  server.send(200, "text/html", message);
}

void initServer()
{
  String ssid = STASSID;
  if (!ConnectToWifi(STASSID, STAPSK))
  {
#ifdef SERIAL_MONITOR
    Serial.println("");
    Serial.print("Failed to Connect to WiFi");
#endif
    delay(5000);
    return;
  }
#ifdef SERIAL_MONITOR      
  Serial.println("");
  Serial.print("Connected to ");
  Serial.println(ssid);
  Serial.print("IP address: ");
#endif  
  String localIP = GetLocalIP();
#ifdef SERIAL_MONITOR      
  Serial.println(localIP);
#endif

  if (MDNS.begin("esp8266")) 
  {
#ifdef SERIAL_MONITOR          
    Serial.println("MDNS responder started");
#endif    
  }

  printOLED(2, 2, "WebServer");

  printOLED(4, 4, localIP.c_str());

  //server.on("/hopper/poke", handlePoke);
  server.on("/", handleRoot);
  server.onNotFound(handleNotFound);

  server.begin();
#ifdef SERIAL_MONITOR  
  Serial.println("HTTP server started");
  Serial.println("");
#endif
  timeOLEDActivated = millis() / 1000;
  timeLastScreenUpdate = timeOLEDActivated;
}

// ########## end of Web Server code ##########

void showOLED(bool onOff)
{
  if (onOff)
  {
    display.ssd1306_command(SSD1306_DISPLAYON);
    timeOLEDActivated = millis() / 1000;
  }
  else
  {
    display.ssd1306_command(SSD1306_DISPLAYOFF);
  }
}


void webCheck()
{
  int timeZoneHours = 0;
  WiFiClient timeClient;
  if (timeClient.connect("worldtimeapi.org",80))
  {
#ifdef SERIAL_MONITOR  
    Serial.println("Connected to worldtimeapi.org");
#endif
    
    timeClient.println("GET /api/timezone/pacific/auckland HTTP/1.1");
    timeClient.println("Host: worldtimeapi.org");
    timeClient.println("Connection: close");
    timeClient.println();
    unsigned long timeout = millis();
    while (timeClient.available() == 0) 
    {
      if (millis() - timeout > 30000)
      { 
#ifdef SERIAL_MONITOR          
        Serial.println("worldtimeapi.org Timeout !");
#endif        
        timeClient.stop(); 
        delay(5000);
        return; 
      } 
    } 
    String timeJSON;
    while (timeClient.available())
    { 
      char c = timeClient.read();
      timeJSON += c;
      delay(1);
    }
    timeClient.stop();

#ifdef SERIAL_MONITOR          
    //Serial.println(timeJSON);
#endif     

    int i = timeJSON.indexOf('{');
    if (i != -1)
    {
      timeJSON = timeJSON.substring(i);

      DynamicJsonDocument jsonDoc(2048);
      deserializeJson(jsonDoc, timeJSON);

      String dateTime = jsonDoc["datetime"];
      String uctDateTime = jsonDoc["utc_datetime"];

      i = dateTime.indexOf('+');
      String timeZone = dateTime.substring(i+1, i+3);
      i = dateTime.indexOf('T');
      dateTime = dateTime.substring(i+1, i+9);
      i = uctDateTime.indexOf('T');
      uctDateTime = uctDateTime.substring(i+1, i+9);
#ifdef SERIAL_MONITOR          
      Serial.println(dateTime);
      Serial.println(uctDateTime);
      Serial.println(timeZone);
#endif     
      int hours = dateTime.substring(0,2).toInt();
      int minutes = dateTime.substring(3,5).toInt();
      int seconds = dateTime.substring(6,8).toInt();

      timeZoneHours = timeZone.substring(0,2).toInt();
      timeWebBase = hours * 60 * 60 + minutes * 60 + seconds;
      timeLastWebCheck = millis() / 1000;

      showOLED(true);
    }
    else
    {
#ifdef SERIAL_MONITOR  
      Serial.println("{ not found in worldtimeapi.org response");
#endif      
      delay(5000);
    }
  }
  else
  {
#ifdef SERIAL_MONITOR  
    Serial.println("Failed to connect to worldtimeapi.org");
#endif    
    delay(5000);
  }

  WiFiClient sunsetClient;
  if (sunsetClient.connect("api.sunrise-sunset.org",80))
  {
#ifdef SERIAL_MONITOR  
    Serial.println("Connected to api.sunrise-sunset.org");
#endif
    
    sunsetClient.println("GET /json?lat=YY.YYYY&lng=XXX.XXXX&formatted=0 HTTP/1.1");
    sunsetClient.println("Host: api.sunrise-sunset.org");
    sunsetClient.println("Connection: close");
    sunsetClient.println();

    unsigned long timeout = millis();
    while (sunsetClient.available() == 0) 
    {
      if (millis() - timeout > 30000)
      { 
#ifdef SERIAL_MONITOR          
        Serial.println("api.sunrise-sunset.org Timeout !");
#endif        
        sunsetClient.stop(); 
        delay(5000);
        return; 
      } 
    } 

    String timeJSON;
    while (sunsetClient.available())
    { 
      char c = sunsetClient.read();
      timeJSON += c;
      delay(1);
    }
    sunsetClient.stop();

#ifdef SERIAL_MONITOR          
    //Serial.println(timeJSON);
#endif     

    int i = timeJSON.indexOf('{');
    if (i != -1)
    {
      timeJSON = timeJSON.substring(i);

      DynamicJsonDocument jsonDoc(2048);
      deserializeJson(jsonDoc, timeJSON);

      String sunsetTime = jsonDoc["results"]["sunset"];

      i = sunsetTime.indexOf('T');
      sunsetTime = sunsetTime.substring(i+1, i+9);
#ifdef SERIAL_MONITOR          
      Serial.println(sunsetTime);
#endif     
      int hours = sunsetTime.substring(0,2).toInt() + timeZoneHours;
      int minutes = sunsetTime.substring(3,5).toInt();
      int seconds = sunsetTime.substring(6,8).toInt();
      sunsetWebBase = hours * 60 * 60 + minutes * 60 + seconds;

      showOLED(true);
    }
    else
    {
#ifdef SERIAL_MONITOR  
      Serial.println("{ not found in api.sunrise-sunset.org response");
#endif      
      delay(5000);
    }
  }
  else
  {
#ifdef SERIAL_MONITOR  
    Serial.println("Failed to connect to api.sunrise-sunset.org");
#endif    
    delay(5000);
  }
}

void updateDisplay()
{
  unsigned long timeNow = millis() / 1000;

  clearScreen();
  char text[128];
  //sprintf(text, "Out:%d", inputFromMAX9814);
  //printOLED(0, 0, text);

  unsigned long secondsSinceLastCheck = timeNow - timeLastWebCheck;
  unsigned long currentTime = timeWebBase + secondsSinceLastCheck;
  while (currentTime > twentyFourHours)
  {
    currentTime = currentTime - twentyFourHours;
  }

  int hours = currentTime / (60*60);
  int minutes = (currentTime / 60) % 60;
  int seconds = currentTime % 60;
  
  printOLED(0, 0, "Time:");
  sprintf(text, "%02d:%02d:%02d", hours, minutes, seconds);
  printOLED(16, 0, text);

  hours = sunsetWebBase / (60*60);
  minutes = (sunsetWebBase / 60) % 60;
  seconds = sunsetWebBase % 60;
  
  printOLED(0, 1, "Sunset:");
  sprintf(text, "%02d:%02d:%02d", hours, minutes, seconds);
  printOLED(16, 1, text);

  hours = onAfterSunset / (60*60);
  minutes = (onAfterSunset / 60) % 60;
  
  printOLED(0, 2, "On time:");
  sprintf(text, "+%02d:%02d:00", hours, minutes);
  printOLED(15, 2, text);

  hours = onOverride / (60*60);
  minutes = (onOverride / 60) % 60;
  seconds = onOverride % 60;
  
  printOLED(0, 3, "Override:");
  sprintf(text, "+%02d:%02d:%02d", hours, minutes, seconds);
  printOLED(15, 3, text);

  //sprintf(text, "Seconds left:%d", (int)(secondsOLEDOn-(timeNow - timeOLEDActivated)));
  //printOLED(0, 4, text);

  String localIP = GetLocalIP();
  printOLED(6, 5, localIP.c_str());
  timeLastScreenUpdate = timeNow;
}

void setup() 
{
  pinMode(SWITCH_PIN, OUTPUT);
  digitalWrite(SWITCH_PIN, LOW); // start with lights off
#ifdef SERIAL_MONITOR  
  Serial.begin(115200); 
#endif
  initOLED();
  initServer();
  delay(1000);
  webCheck();
  updateDisplay();
}



int overrideLaps = 0;

void loop() 
{
  unsigned long timeNow = millis() / 1000;
  server.handleClient();

  unsigned long secondsSinceLastWebCheck = timeNow - timeLastWebCheck;
  if (secondsSinceLastWebCheck > 24*60*60)
  {
    webCheck();
  }

  // read the sensor and store it in the variable sensorReading:
  inputFromMAX9814 = analogRead(KNOCK_PIN);

  if (activeOLED && (timeNow - timeOLEDActivated > secondsOLEDOn))
  {
    activeOLED = false;
    showOLED(activeOLED);
    updateDisplay();
  }
  else if (inputFromMAX9814 >= threshold) 
  {
#ifdef SERIAL_MONITOR      
    Serial.println(inputFromMAX9814);
#endif
    activeOLED = true;
    showOLED(activeOLED);
    updateDisplay();
    
    // this delay ensures that we only trigger our switch once per second ("debouncing")
    delay(1000);
  }
  else if (timeLastScreenUpdate - timeNow > secondsOLEDRefresh)
  {
    updateDisplay();
  }
  // All of these are in seconds
  // - timeLastWebCheck : millis()/1000 when we last updated timeWebBase and sunsetWebBase
  // - timeWebBase   : current time in seconds the last time we checked
  // - sunsetWebBase : sunset time in seconds the last time we checked
  // - onAfterSunset : delta to keep lights on in seconds
  // - secondsSinceLastWebCheck : millis()/100 since last time we checked

  bool switchOn = false;
  unsigned long currentTime = timeWebBase + secondsSinceLastWebCheck;
  while (currentTime > twentyFourHours)
  {
    currentTime = currentTime - twentyFourHours;
  }
  unsigned long offTime = sunsetWebBase + onAfterSunset;
  if ((currentTime > sunsetWebBase) && (currentTime < offTime))
  {
    switchOn = true;
  }
  else if (onOverride > 0)
  {
    if (overrideLaps <= 0)
    {
      onOverride--;
      overrideLaps = 100;
    }
    else
    {
      overrideLaps--;
    }
    switchOn = (onOverride > 0);
  }
  digitalWrite(SWITCH_PIN, switchOn ? HIGH : LOW);

  // If the endless loop is too tight, we spin our wheels in the call to 'server.handleClient()'
  // If we spend too much time in each loop, then the audio switch won't work (which is why we have overrideLaps)
  delay(10); 
}

Credits

Michael Cartwright

Michael Cartwright

21 projects • 13 followers

Comments