/*
==================================
ESPtime_NewYorkDST_DHT22_OLED.ino
==================================
AUTHORED by Robert F Gabriel April 26, 2023

THIS APP IS DESIGNED TO RETRIEVE THE NTP TIME, THE TEMPERATURE AND HUMIDITY FROM
AN dht22 SENSOR, AND  DISPLAY IT ALL OM AN SSD1306 OLED DISPLAY. The code needs
some cleanup and optimization but the intent was to first obtain a properly
functioning setup; beautification to come later.

A LARGE AMOUNT OF THIS CODE WAS ADOPTED BECAUSE OF ITS USE OF ACE_TIME AND ITS
INCLUSION OF DAY LIGHT SAVINGS TIME ADJUSTMENT AUTOMATICALLY
================================================================================
Use the built-in SNTP client on the ESP8266 and ESP32 platforms to configure
the C-library `time()` function to return the number of seconds since Unix epoch
(1970-01-01T00:00:00). The epochSeconds is converted to human readable date-time
strings in 4 ways:
`
2) UTC using ace_time::LocalDateTime


The SNTP client apparently performs automatic synchronization of the time()
function every 1h, but the only documentation for this that I can find is in
this example file:
https://github.com/esp8266/Arduino/tree/master/libraries/esp8266/examples/NTP-TZ-DST


*/
// deltas from actual implementation on ESP32_NodeMCU_12E
//#include "DHTesp.h"  //from Wokwi example 
//# ESP32 Pin assignment 
//i2c = I2C(0, scl=Pin(22), sda=Pin(21))
//=============original INCLUDES======================
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_Sensor.h>
#include <DHT.h>
#include <Arduino.h>
#include <time.h>  // gmtime_r()
#include <AceTime.h>
using namespace ace_time;

#include <WiFiUdp.h>

#include <WiFi.h>

#if !defined(WIFI_SSID)
#define WIFI_SSID "Wokwi-GUEST"
#endif

#if !defined(WIFI_PASSWORD)
#define WIFI_PASSWORD ""
#endif

#if !defined(NTP_SERVER)
#define NTP_SERVER "pool.ntp.org"
#endif

#ifndef STASSID
#define STASSID "Wokwi_GUEST"
#define STAPSK ""
#endif

//======HARDWARE===============================================
#define SCREEN_WIDTH 128  // OLED display width, in pixels
#define SCREEN_HEIGHT 64  // OLED display height, in pixels
// Character size 5x8 = Size 1 10x16 if Size 2
// Declaration for an SSD1306 display connected to I2C (SDA, SCL pins)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

#define DHTPIN 14  // Digital pin connected to the DHT sensor
// NodeMCU 12e pins 6-11 are not usable

// Uncomment the type of sensor in use:
//#define DHTTYPE    DHT11     // DHT 11
#define DHTTYPE DHT22  // DHT 22 (AM2302)
//#define DHTTYPE    DHT21     // DHT 21 (AM2301)
DHT dht(DHTPIN, DHTTYPE);

//------------------VARIABLES & CONSTANTS-------------------------------------
 String myPad4Sec = "0";
 String displaySec = "00";
 String myPad4Min = "0";
 String displayMin = "00";
 String myPad4Hour = "0";
 String displayHour = "00";
 String displayDOW = "          ";
 String displayDay = "00";

const char* ssid = STASSID;  // your network SSID (name)
const char* pass = STAPSK;   // your network password

unsigned int localPort = 2390;  // local port to listen for UDP packets

/* Don't hardwire the IP address or we won't get the benefits of the pool.
    Lookup the IP address for the host name instead */
// IPAddress timeServer(129, 6, 15, 28); // time.nist.gov NTP server
IPAddress timeServerIP;  // time.nist.gov NTP server address
const char* ntpServerName = "pool.ntp.org";
const int NTP_PACKET_SIZE = 48;  // NTP time stamp is in the first 48 bytes of the message

byte packetBuffer[NTP_PACKET_SIZE];  // buffer to hold incoming and outgoing packets

// A UDP instance to let us send and receive packets over UDP
WiFiUDP udp;

// Value of time_t for 2000-01-01 00:00:00, used to detect invalid SNTP
// responses.
static const time_t EPOCH_2000_01_01 = 946684800;
// Number of millis to wait for a WiFi connection before doing a software
// reboot.
static const unsigned long REBOOT_TIMEOUT_MILLIS = 15000;

//-----------------------------------------------------------------------------
// Define 1 zone processors to handle 1 timezones (America/New_Yorks,
// efficiently. It is possible to use only 1 to save memory, at
// the cost of slower performance. These are heavy-weight objects so should be
// created during the initialization of the app.
ExtendedZoneProcessor zoneProcessorNew_York;

//========================================================
void setup() {
  pinMode(2, OUTPUT);     // Initialize the LED pin 2 as an output
 
  delay(1000);
  Serial.begin(115200);

  setupWifi();
  digitalWrite(2, LOW);


  setupSntp();

  //OLED=SETUP=======
  dht.begin();
  delay(5000);
  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    Serial.println(F("SSD1306 allocation failed"));
    for (;;)
      ;
  }
  delay(2000);
  display.clearDisplay();
  display.setTextColor(WHITE);
}
//========================================================
void setupSntp() {
// Setup the SNTP client. Set the local time zone to be UTC, with no DST offset,
// because we will be using AceTime to perform the timezone conversions. The
// built-in timezone support provided by the ESP8266/ESP32 API has a number of
// deficiencies, and the API can be quite confusing.
  Serial.print(F("Configuring SNTP"));
  configTime(0 /*timezone*/, 0 /*dst_sec*/, NTP_SERVER);

  // Wait until SNTP stabilizes by ignoring values before year 2000.
  unsigned long startMillis = millis();
  while (true) {
    Serial.print('.');  // Each '.' represents one attempt.
    time_t now = time(nullptr);
    if (now >= EPOCH_2000_01_01) {
      Serial.println(F(" Done."));
      break;
    }

    // Detect timeout and reboot.
    unsigned long nowMillis = millis();
    if ((unsigned long)(nowMillis - startMillis) >= REBOOT_TIMEOUT_MILLIS) {
#if defined(ESP8266)
      Serial.println(F(" FAILED! Rebooting..."));
      delay(1000);
      ESP.reset();
#elif defined(ESP32)
      Serial.println(F(" FAILED! Rebooting..."));
      delay(1000);
      ESP.restart();
#else
      Serial.print(F(" FAILED! But cannot reboot. Continuing"));
      startMillis = nowMillis;
#endif
    }

    delay(500);
  }
}

//========================================================
void setupWifi() {
// Connect to WiFi. Sometimes the board will connect instantly. Sometimes it
// will struggle to connect. I don't know why. Performing a software reboot
// seems to help, but not always.
  Serial.print(F("Connecting to WiFi"));
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

  unsigned long startMillis = millis();
  while (true) {
    Serial.print('.');  // Each '.' represents one attempt.
    if (WiFi.status() == WL_CONNECTED) {
      Serial.println(F(" Done."));
      break;
    }

    // Detect timeout and reboot.
    unsigned long nowMillis = millis();
    if ((unsigned long)(nowMillis - startMillis) >= REBOOT_TIMEOUT_MILLIS) {
#if defined(ESP8266)
      Serial.println(F(" FAILED! Rebooting..."));
      delay(1000);
      ESP.reset();
#elif defined(ESP32)
      Serial.println(F(" FAILED! Rebooting..."));
      delay(1000);
      ESP.restart();
#else
      Serial.print(F(" FAILED! But cannot reboot. Continuing"));
      startMillis = nowMillis;
#endif
    }

    delay(500);
  }
}

//========================================================
void loop() {
  time_t now = time(nullptr);

  printNowUsingAceTime(now);
  Serial.println();

  //post the temp and humidity to OLED
  //delay(5000);
  digitalWrite(2, HIGH);
  //read temperature and humidity
  float t = dht.readTemperature();
  t = ((t * 1.8) + 32);

  float h = dht.readHumidity();
  if (isnan(h) || isnan(t)) {
    Serial.println("Failed to read from DHT sensor!");
  }
  // clear display
  display.clearDisplay();
  // display temperature
  display.setTextSize(1);
  display.setCursor(0, 0);
  display.print("Temperature: ");
  display.setTextSize(2);
  display.setCursor(0, 10);
  display.print(t);
  display.print(" ");
  display.setTextSize(1);
  display.drawCircle(1, 10, 2, WHITE);
  display.setTextSize(2);
  display.print((char)247);  // degree symbol
  display.print("F");
  // display humidity
  display.setTextSize(1);
  display.setCursor(0, 35);
  display.print("Humidity: ");
  display.setTextSize(2);
  display.setCursor(0, 45);
  display.print(h);
  display.print(" %");
  //make the display go live
  display.display();
  digitalWrite(2, LOW);
  delay(5000);

  // DONE with Temp and Humid
 
  // Display the hour, minute, second and day name
  display.clearDisplay();
  display.setTextSize(2);
  display.setCursor(19, 10);
  //PROCESS HOURS==============================
  // print the hour (86400 equals secs per day)
  display.print(displayHour);
  display.print(":");
  //PROCESS MINUTES============================
  display.print(displayMin);
  display.print(":");
  //PROCESS SECONDS============================
  if (displaySec.length() > 2) {
    displaySec = displaySec.substring(0,1);
  }
  display.print(displaySec);
  //PROCESS DAY OF WEEK
  display.setCursor(0, 45);
  display.print(displayDOW);
  Serial.print("DOW= ");
  Serial.println(displayDOW);
  //display the day of the current month
  display.setTextSize(1);
  display.print(" ");
  //display the day of the month as a superscript
  display.print(displayDay);
  digitalWrite(2, HIGH);
  display.display();
  delay(4000);
  
}
//====================================================================
void printNowUsingAceTime(time_t now) {
// Print the UTC time, New_York time using
// the AceTime library. TimeZone objects are light-weight and can be created on
// the fly.

// Utility to convert ISO day of week with Monday=1 to human readable string.
  DateStrings dateStrings;

// Convert to UTC time.
  LocalDateTime ldt = LocalDateTime::forUnixSeconds64(now);

  //===================================================================
  //Turn on NTP server LED=========
  //===================================================================

  //======Convert Unix time to New York time.==========================
  TimeZone tzNew_York = TimeZone::forZoneInfo(
    &zonedbx::kZoneAmerica_New_York,
    &zoneProcessorNew_York);
  ZonedDateTime zdtNew_York = ZonedDateTime::forUnixSeconds64(
    now, tzNew_York);

  // Hours minutes and seconds are strings that do not contain any leading zero

    displayDay = String(zdtNew_York.day());
    Serial.println(displayDay);

  String mySec = String(zdtNew_York.second());
  //Pad single digit time segments
    myPad4Sec = "0";
  if (mySec.length() == 1) {
    myPad4Sec += mySec;
    displaySec = myPad4Sec;
  } else if (mySec.length() != 1) {
    displaySec = String(zdtNew_York.second());
  }

  String myMin = String(zdtNew_York.minute());
  //Pad single digit time segments
  myPad4Min = "0";
  if (myMin.length() == 1) {
    myPad4Min += myMin;
    displayMin = myPad4Min;
  } else if (myMin.length() != 1) {
    displayMin = String(zdtNew_York.minute());
  }

  String myHour = String(zdtNew_York.hour());
  //Pad single digit time segments
  myPad4Hour = "0";
  if (myHour.length() == 1) {
    myPad4Hour += myHour;
    displayHour = myPad4Hour;
  } else if (myHour.length() != 1) {
    displayHour = String(zdtNew_York.hour());
  }
  displayDOW = dateStrings.dayOfWeekLongString(zdtNew_York.dayOfWeek());

  //DEBUG==============================
  //The following yields=> day of week
  Serial.print(dateStrings.dayOfWeekLongString(zdtNew_York.dayOfWeek()));
  Serial.print("  ");
  Serial.print(displayDOW);
  Serial.print("<");
  Serial.print(displayHour);
  Serial.print(":");
  Serial.print(displayMin);
  Serial.print(":");
    if (displaySec.length() > 2) {
      Serial.print(displaySec.length() );
    displaySec = displaySec.substring(0,1);
          Serial.print(displaySec);
  }
  Serial.print(displaySec);
  Serial.print(">    ");
  Serial.print(zdtNew_York.hour());
  Serial.print(":");
  Serial.print(zdtNew_York.minute());
  Serial.print(":");
  Serial.print(zdtNew_York.second());
}