NTP DST for the ESP8266

A NTP Request on the ESP8266 including Daylight Saving Time

NTP is part of the Arduino core for the ESP8266 since 2019. This also includes the proper handling of the daylight saving time. You don't need any third party libraries, you don't need manual SNTP calls and you don't need any extra code for the daylight saving time. The IDE example ESP8266 | NTP-TZ-DST is kept simple, but comes with lot of "optional" functions. Therefore I am presenting a "NTP-TZ-DST - bare minium" as a complete sketch and I hope it may be helpful for others.

The abbreviation NTP-TZ-DST bare minimum stands for

  • NTP: NetWork Time Protocol
  • TZ: Time Zone
  • DST: Daylight Saving Time ("Summer time / Winter time")
  • Bare minimums: the absolute minimum that is necessary to make NTP working

Well, it takes a few more lines for the output, but all in all there are just 60 lines of code, of which only one is REALLY important.

Changes in the NTP example

The shortened NTP sketch for the ESP8266 looks like following:

/*
  NTP TZ DST - bare minimum
  NetWork Time Protocol - Time Zone - Daylight Saving Time

  Our target for this MINI 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
  
  This example is a stripped down version of the NTP-TZ-DST (v2)
  And works for ESP8266 core 2.7.4 and 3.0.2

  by noiasca
  2020-09-22 see https://werner.rothschopf.net/202011_arduino_esp8266_ntp_en.htm
*/

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

/* Configuration of NTP */
#define MY_NTP_SERVER "at.pool.ntp.org"           
#define MY_TZ "CET-1CEST,M3.5.0/02,M10.5.0/03"   

/* Necessary Includes */
#include <ESP8266WiFi.h>            // we need wifi to get internet access
#include <time.h>                   // for time() ctime()

/* Globals */
time_t now;                         // this are the seconds since Epoch (1970) - UTC
tm tm;                              // the structure tm holds time information in a more convenient way

void showTime() {
  time(&now);                       // read the current time
  localtime_r(&now, &tm);           // update the structure tm with the current time
  Serial.print("year:");
  Serial.print(tm.tm_year + 1900);  // years since 1900
  Serial.print("\tmonth:");
  Serial.print(tm.tm_mon + 1);      // January = 0 (!)
  Serial.print("\tday:");
  Serial.print(tm.tm_mday);         // day of month
  Serial.print("\thour:");
  Serial.print(tm.tm_hour);         // hours since midnight  0-23
  Serial.print("\tmin:");
  Serial.print(tm.tm_min);          // minutes after the hour  0-59
  Serial.print("\tsec:");
  Serial.print(tm.tm_sec);          // seconds after the minute  0-61*
  Serial.print("\twday");
  Serial.print(tm.tm_wday);         // days since Sunday 0-6
  if (tm.tm_isdst == 1)             // Daylight Saving Time flag
    Serial.print("\tDST");
  else
    Serial.print("\tstandard");
  Serial.println();
}

void setup() {
  Serial.begin(115200);
  Serial.println("\nNTP TZ DST - bare minimum");

  configTime(MY_TZ, MY_NTP_SERVER); // --> Here is the IMPORTANT ONE LINER needed in your sketch!

  // start network
  WiFi.persistent(false);
  WiFi.mode(WIFI_STA);
  WiFi.begin(STASSID, STAPSK);
  while (WiFi.status() != WL_CONNECTED) {
    delay(200);
    Serial.print ( "." );
  }
  Serial.println("\nWiFi connected");
  // by default, the NTP will be started after 60 secs
}

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

The actual configuration is done using two pre-compilers defines. The first define is the NTP server to which you want to send the request. It is best to choose the server according to your location. In my case it is a NTP server pool in Austria:

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

To convert UTC to your specific local time, you use an existing time zone definition:

#define MY_TZ "CET-1CEST,M3.5.0/02,M10.5.0/03" // https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv

You select the entry from the list linked at the end of the page. The example will work for CEST (Spain, France, Germany, Sweden, Italy, Poland to name a few). The entry contains all information for ESP8266 to calculate the local time including daylight saving time. In my example the daylight saving time changes in the third month (M3), the fifth week (5), on Sunday (0) at two o'clock (02). The winter time changes in the 10th month (M10), in the fifth week (5) on Sunday (0) at three o'clock (03).

With the ESP Core comes a time library which needs to be included:

#include <time.h>                   // for time() ctime()

The example sketch needs two global variables:

now is used for the UNIX timestamp, the epoch. These are the seconds since January 1st, 1970:

time_t now;                         // this are the seconds since Epoch (1970) - UTC

As humans are used to work with date/time in parts of days, months, years ... you can use the structure tm

tm tm;                              // the structure tm holds time information in a more convenient way

The structure is defined in the time.h. Later on we will see the member variables for the output.

In setup() you include the most important line of code:

 configTime(MY_TZ, MY_NTP_SERVER); // --> Here is the IMPORTANT ONE LINER needed in your sketch!

With this single line you define your timezone an the NTP server.

You don't need more for NTP on an ESP8266! (!!!)

In the sketch there is also a function for the output of the current time on the serial monitor. The output is done in 3 steps:

First you update the variable now and take over the current time stamp. With localtime_r you convert the time stamp into the structure tm. The structure contains the following important member variables:

  Member   Type  Meaning Range
  tm_sec    int   seconds after the minute  0-61*
  tm_min    int   minutes after the hour  0-59
  tm_hour   int   hours since midnight  0-23
  tm_mday   int   day of the month  1-31
  tm_mon    int   months since January  0-11
  tm_year   int   years since 1900
  tm_wday   int   days since Sunday 0-6
  tm_yday   int   days since January 1  0-365
  tm_isdst  int   Daylight Saving Time flag

* tm_sec is generally 0-59. The extra range is to accommodate for leap seconds in certain systems.

Example: With tm.tm_min, you can access the minute time component. Just be careful with the month: The months are specified from 0 (January) to 11 (December) - so just ad 1 to get the month. The years start with 1900 - so just add 1900 to get the 4-digit year.

That's all for NTP on a ESP8266!

Change Startup Delay for NTP

When you upload the sketch to your ESP you will notice that the timestamp will start with 01/01/1970. By default, the first NTP request begins after 60 seconds. That can be changed. The original IDE example shows how to implement the function sntp_startup_delay_MS_rfc_not_less_than_60000.

uint32_t sntp_startup_delay_MS_rfc_not_less_than_60000 () {
  return 60000UL; // 60s
}

A variant for a random NTP startup delay of 5 seconds could look like

uint32_t sntp_startup_delay_MS_rfc_not_less_than_60000 () {
  randomSeed(A0);
  return random(5000);
}

For testing I like to keep the one minute interval because it shows clearly how the time is updated. The one minute delay was part of the now obsolete best practices in RFC 4330 page 22. In the current RFC 5905 I found no similar restriction so reducing this first delay might be valid.

The function is declared as "weak" function. You only need to define it and if it is available in your sketch the library will call it. You don't need to call it in setup() or loop().

Change NTP Polling Interval

Another standard value is the NTP polling interval. The default value is 60 minutes. This value can also be changed. You can find an example in the original sketch using the function  sntp_update_delay_MS_rfc_not_less_than_15000. You have to return the polling interval in milliseconds:

uint32_t sntp_update_delay_MS_rfc_not_less_than_15000 () {
  return 12 * 60 * 60 * 1000UL; // 12 hours
}

The function is declared as "weak" function. You only need to define it and if it is available in your sketch the library will call it. You don't need to call it in setup() or loop().

Callback on NTP Update

You can define a callback function which will be called when the NTP server gets called. You will need three components:

Include the coredecls.h

#include <coredecls.h> // optional settimeofday_cb() callback to check on server

define your callback function:

void time_is_set() {     // no parameter until 2.7.4
  Serial.println(F("time was sent!"));
}

The optional parameter can be used with ESP8266 Core 3.0.0 or higher

void time_is_set(bool from_sntp /* <= this optional parameter can be used with ESP8266 Core 3.0.0*/) {
  Serial.print(F("time was sent! from_sntp=")); Serial.println(from_sntp);
}

In setup() activate the callback

settimeofday_cb(time_is_set); // optional: callback if time was sent

Remarks and Changes

NTP was included in the ESP Core 2.6.0.

With the ESP Core version 2.7.4 there was a change in the API. Therefore, older examples might no longer work with the newer core. It can happen that the first NTP call will be processed correct, but each further sync (default after one hour) will return the UTC time only. The provided example was tested with ESP Core 2.7.4 and 3.0.2. My development was done on a NodeMCU but should work on every ESP8266 based board.

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: 2020-11-19 | Version: 2024-06-15