NTP with Day Light Saving Time and RTC Backup for the ESP8266

Retrieving the correct time on the ESP8266 with NTP is quite easy. On this page I want to show how to add a RTC like a D3231 as fallback if for any reason NTP is not available or a proper time should be available immediately after startup.

I started with my code "NTP-TZ-DST bare minimum", it's quite simple and you can read more about on the page for NTP and DST. It uses the time.h and the build in ESP functions to retrieve NTP without an additional UDP library. As RTC I'm using a DS3231 and the "RTClib.h" from Adafruit.

Newer ESP Cores support NTP and Daylight Saving Time. The internal time runs in UTC. Timezone and DST offset is added when you request actual time information. Therefore I decided to let the RTC also run in UTC.

The example sketch will do following:

  • on startup: if at the end of setup() the time information is to old (date before September 2000) and if RTC is connected the internal time gets updated based on the RTC time.
  • print time information every second
  • each time we have a successful NTP response, we also update the time information in the RTC.

Some Functions of time.h

There are some important functions in the time.h:

time_t mktime(struct tm *tm)
takes an argument
representing broken-down time
struct tm *localtime(const time_t *timep)
converts a time_t into a tm structure in your local time (timezone and DST offset)
struct tm *gmtime(const time_t *timep)
converts a time_t into a tm structure in UTC
struct tm *gmtime_r(const time_t *restrict timep,
struct tm *restrict result);
The gmtime() function converts the calendar time timep to broken-
down time representation, expressed in Coordinated Universal Time
(UTC). It may return NULL when the year does not fit into an
integer. The return value points to a statically allocated
struct which might be overwritten by subsequent calls to any of
the date and time functions.
struct tm *gmtime(const time_t *timep)
The gmtime_r() function does the
same, but stores the data in a user-supplied struct.
int settimeofday(const struct timeval *tv, const struct timezone *tz)
can set the time as well as a timezone.
size_t strftime (char* ptr, size_t maxsize, const char* format, const struct tm* timeptr );
Copies into ptr the content of format, expanding its format specifiers into the corresponding values that represent the time described in timeptr, with a limit of maxsize characters.

The POSIX functions timelocal() and timegm() are not available in the ESP8266 Core 3.0.2. Therefore I adopted a function getTimestamp()  which will accept a broken down time information and return the Unix timestamp.

The NTP DST RTC example

The NTP sketch for the ESP looks like following:

/*
  NTP TZ DST - RTC fallback
  NetWork Time Protocol - Time Zone - Daylight Saving Time - Real Time Clock

  Target for this sketch is:
  - get the SNTP request running
  - set the timezone
  - (implicit) respect daylight saving time
  - how to "read" time to be printed to Serial.Monitor
  - get date/time from RTC until first NTP
  - Update date/time of RTC if NTP was successful
  
  This example is a stripped down version of the NTP-TZ-DST (v2) from the ESP8266
  based on the idea of https://forum.arduino.cc/t/set-ds3231-with-ntp-server/939845/9
    
  Hardware:
  NodeMCU / ESP8266 and DS3231 wiring
  D0 GPI16 (use for reset after deepsleep)
  D1 GPIO5 - I2C SCL
  D2 GPIO4 - I2C SDA
   
  Dependencies:
  ESP8266 Core 3.0.2 (we need at least 3.0.0 because of the parameter in the callback)
   
  by noiasca https://werner.rothschopf.net/microcontroller/202112_arduino_esp_ntp_rtc_en.htm
  2021-12-30 OK
*/

#if ARDUINO_ESP8266_MAJOR < 3
#pragma message("This sketch requires at least ESP8266 Core Version 3.0.0")
#endif

#ifndef STASSID
#define STASSID "your-ssid"            // set your SSID
#define STAPSK  "your-password"        // set your wifi password
#endif

/* Configuration of NTP */
// choose the best fitting NTP server pool for your country
#define MY_NTP_SERVER "at.pool.ntp.org"

// choose your time zone from this list
// https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv
#define MY_TZ "CET-1CEST,M3.5.0/02,M10.5.0/03" // Berlin, Vienna, Rom, ...

/* Necessary includes */
#include                // we need wifi to get internet access
#include                       // for time() ctime() ...
#include                  // optional settimeofday_cb() callback to check on server
#include                     // Adafruit 2.0.2 (1.12.5 also tested) for RTC like DS3231
RTC_DS3231 rtc;

/*
    Sets the internal time
    epoch (seconds in GMT)
    microseconds
*/
void setInternalTime(uint64_t epoch = 0, uint32_t us = 0)
{
  struct timeval tv;
  tv.tv_sec = epoch;
  tv.tv_usec = us;
  settimeofday(&tv, NULL);
}

/*
   prints an one digit integer with a leading 0
*/
void print10(int value)
{
  if (value < 10) Serial.print("0");
  Serial.print(value);
}

/*
   ESP8266 has no timegm, so we need to create our own...

   Take a broken-down time and convert it to calendar time (seconds since the Epoch 1970)
   Expects the input value to be Coordinated Universal Time (UTC)

   Parameters and values:
   - year  [1970..2038]
   - month [1..12]  ! - start with 1 for January
   - mday  [1..31]
   - hour  [0..23]
   - min   [0..59]
   - sec   [0..59]
   Code based on https://de.wikipedia.org/wiki/Unixzeit example "unixzeit"
*/
int64_t getTimestamp(int year, int mon, int mday, int hour, int min, int sec)
{
  const uint16_t ytd[12] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}; /* Anzahl der Tage seit Jahresanfang ohne Tage des aktuellen Monats und ohne Schalttag */
  int leapyears = ((year - 1) - 1968) / 4
                  - ((year - 1) - 1900) / 100
                  + ((year - 1) - 1600) / 400; /* Anzahl der Schaltjahre seit 1970 (ohne das evtl. laufende Schaltjahr) */
  int64_t days_since_1970 = (year - 1970) * 365 + leapyears + ytd[mon - 1] + mday - 1;
  if ( (mon > 2) && (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)) )
    days_since_1970 += 1; /* +Schalttag, wenn Jahr Schaltjahr ist */
  return sec + 60 * (min + 60 * (hour + 24 * days_since_1970) );
}

/*
    print time of RTC to Serial
*/
void printRTC()
{
  DateTime dtrtc = rtc.now();          // get date time from RTC i
  if (!dtrtc.isValid())
  {
    Serial.println(F("E103: RTC not valid"));
  }
  else
  {
    time_t newTime = getTimestamp(dtrtc.year(), dtrtc.month(), dtrtc.day(), dtrtc.hour(), dtrtc.minute(), dtrtc.second());
    Serial.print(F("RTC:"));
    Serial.print(newTime);
    Serial.print(" ");
    Serial.print(dtrtc.year()); Serial.print("-");
    print10(dtrtc.month()); Serial.print("-");
    print10(dtrtc.day()); Serial.print(" ");
    print10(dtrtc.hour()); Serial.print(":");
    print10(dtrtc.minute()); Serial.print(":");
    print10(dtrtc.second());
    Serial.println(F(" UTC"));         // remember: the RTC runs in UTC
  }
}

/*
   get date/time from RTC and take over to internal clock
*/
void getRTC()
{
  Serial.println(F("getRTC --> update internal clock"));
  DateTime dtrtc = rtc.now();          // get date time from RTC i
  if (!dtrtc.isValid())
  {
    Serial.print(F("E127: RTC not valid"));
  }
  else
  {
    time_t newTime = getTimestamp(dtrtc.year(), dtrtc.month(), dtrtc.day(), dtrtc.hour(), dtrtc.minute(), dtrtc.second());
    setInternalTime(newTime);
    //Serial.print(F("UTC:")); Serial.println(newTime);
    printRTC();
  }
}

/*
   set date/time of external RTC
*/
void setRTC()
{
  Serial.println(F("setRTC --> from internal time"));
  time_t now;                          // this are the seconds since Epoch (1970) - seconds GMT
  tm tm;                               // the structure tm holds time information in a more convient way
  time(&now);                          // read the current time and store to now
  gmtime_r(&now, &tm);                 // update the structure tm with the current GMT
  rtc.adjust(DateTime(tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec));
}

void time_is_set(bool from_sntp) {
  if (from_sntp)                       // needs Core 3.0.0 or higher!
  {
    Serial.println(F("The internal time is set from SNTP."));
    setRTC();
    printRTC();
  }
  else
  {
    Serial.println(F("The internal time is set."));
  }
}

/*
   optional: by default, the NTP will be started after 60 secs
   lets start at a random time in 5 seconds
*/
uint32_t sntp_startup_delay_MS_rfc_not_less_than_60000()
{
  randomSeed(A0);
  return random(5000 + millis());
}

/*
   opional: set SNTP interval
*/
/*
  uint32_t sntp_update_delay_MS_rfc_not_less_than_15000 ()
  {
    return 60 * 1000UL; // 12 hours
  }
*/

/*
   print local time to serial
*/
void showTime() {
  time_t now;                          // this are the seconds since Epoch (1970) GMT
  tm tm;                               // a readable structure
  time(&now);                          // read the current time and store to now
  localtime_r(&now, &tm);              // update the structure tm with the current time
  char buf[50];
  strftime(buf, sizeof(buf), " %F %T %Z wday=%w", &tm); // https://www.cplusplus.com/reference/ctime/strftime/
  Serial.print("now:"); Serial.print(now); // in UTC!
  Serial.print(buf);
  if (tm.tm_isdst == 1)                // Daylight Saving Time flag
    Serial.print(" DST");
  else
    Serial.print(" standard");
  Serial.println();
}

void setup() {
  Serial.begin(115200);
  Serial.println("\n\nNTP TZ DST - RTC Fallback");
  if (!rtc.begin())
    Serial.println(F("Couldn't find RTC"));
  else
    Serial.println(F("RTC found"));
  configTime(MY_TZ, MY_NTP_SERVER);    // --> for the ESP8266 you only need this for Timezone and DST
  // start network
  WiFi.persistent(false);
  WiFi.mode(WIFI_STA);
  WiFi.begin(STASSID, STAPSK);
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    delay(200);
    static uint16_t counter = 0;
    counter++;
    if (counter % 30 == 0) Serial.println();
  }
  Serial.println(F("\nWiFi connected"));
  settimeofday_cb(time_is_set);                  // register callback if time was sent
  if (time(nullptr) < 1600000000) getRTC();      // Fallback to RTC on startup if we are before 2020-09-13
}

void loop() {
  showTime();
  delay(1000);     // dirty delay
}

Remeber to set the NTP server pool according to your location:

#define MY_NTP_SERVER "at.pool.ntp.org" // set the best fitting NTP server (pool) for your location

Remarks for the ESP8266 and Dependencies

The sketch uses the callback when the time was set. Only if the time was set by SNTP, we will also update the date/time of the RTC. Therefore we need at least ESP Core 3.0.0. The development was done on a NodeMCU with ESP8266-12E and ESP Core 3.0.2. For the DS3231 I'm using the RTClib from Adafruit Version 2.0.2 (and did a test with the older 1.12.5 which worked also).

Links

(*) Disclosure: Some of the links above are affiliate links, meaning, at no additional cost to you I will earn a (little) comission if you click through and make a purchase. I only recommend products I own myself and I'm convinced they are useful for other makers.

History

first upload: 2021-12-30 | Version: 2022-02-10