Network Time Protocol (NTP), Timezone and Daylight saving time (DST) with esp8266, esp32 or Arduino

Spread the love

When you create a device you probably need to know the correct time, on wifi device the resonably choiche is to use Internet to get time via NTP.

NTP client, Time Zones, DST with Arduino and esp8266

The Network Time Protocol (NTP) is a networking protocol for clock synchronization between computer systems over packet-switched, variable-latency data networks. In operation since before 1985, NTP is one of the oldest Internet protocols in current use. NTP was designed by David L. Mills of the University of Delaware. (cit. wiki)

NTPClient Library

The Arduino IDE have a library very simple to use NTPClient.

You can download It directly from Arduino IDE --> Manage Libraries.

NTP Library Arduino IDE

I add directly the example code, in this article I try to explain by comment on code, because the code is very simple.

/*
 * Simple NTP client
 * https://mischianti.org/
 *
 * The MIT License (MIT)
 * written by Renzo Mischianti <www.mischianti.org>
 */

#include <NTPClient.h>
// change next line to use with another board/shield
#include <ESP8266WiFi.h>
//#include <WiFi.h> // for WiFi shield
//#include <WiFi101.h> // for WiFi 101 shield or MKR1000
#include <WiFiUdp.h>

const char *ssid     = "<SSID>";
const char *password = "<PASSWORD>";

WiFiUDP ntpUDP;

// By default 'pool.ntp.org' is used with 60 seconds update interval and
// no offset
// NTPClient timeClient(ntpUDP);

// You can specify the time server pool and the offset, (in seconds)
// additionaly you can specify the update interval (in milliseconds).
int GTMOffset = 1;
NTPClient timeClient(ntpUDP, "europe.pool.ntp.org", GTMOffset*60*60, 60000);

void setup(){
  Serial.begin(115200);
  WiFi.begin(ssid, password);

  while ( WiFi.status() != WL_CONNECTED ) {
    delay ( 500 );
    Serial.print ( "." );
  }

  timeClient.begin();
}

void loop() {
  // I'm going to update time from internet
  timeClient.update();
  // Print the updated time
  Serial.println(timeClient.getFormattedTime());

  delay(1000);
}

Now your arduino or esp became a clock.

Time library

This example have some limitation, to update the date you must call service, a simple way to update less time the date you can use Time library. This Arduino library use internal clock as a normal clock that after you adjust It via NTP is a normal clock.

You can find the library here GitHub.

But I show you a simple example.

/*
 * Simple NTP client
 * https://mischianti.org/
 *
 * The MIT License (MIT)
 * written by Renzo Mischianti <www.mischianti.org>
 */

const char *ssid     = "<SSID>";
const char *password = "<PASSWORD>";

#include <NTPClient.h>
// change next line to use with another board/shield
#include <ESP8266WiFi.h>
//#include <WiFi.h> // for WiFi shield
//#include <WiFi101.h> // for WiFi 101 shield or MKR1000
#include <WiFiUdp.h>
#include <TimeLib.h>
#include <time.h>

/**
 * Input time in epoch format and return tm time format
 * by Renzo Mischianti <www.mischianti.org> 
 */
static tm getDateTimeByParams(long time){
    struct tm *newtime;
    const time_t tim = time;
    newtime = localtime(&tim);
    return *newtime;
}

/**
 * Input tm time format and return String with format pattern
 * by Renzo Mischianti <www.mischianti.org>
 */
static String getDateTimeStringByParams(tm *newtime, char* pattern = (char *)"%d/%m/%Y %H:%M:%S"){
    char buffer[30];
    strftime(buffer, 30, pattern, newtime);
    return buffer;
}

/**
 * Input time in epoch format format and return String with format pattern
 * by Renzo Mischianti <www.mischianti.org> 
 */
static String getEpochStringByParams(long time, char* pattern = (char *)"%d/%m/%Y %H:%M:%S"){
//    struct tm *newtime;
    tm newtime;
    newtime = getDateTimeByParams(time);
    return getDateTimeStringByParams(&newtime, pattern);
}

WiFiUDP ntpUDP;

// By default 'pool.ntp.org' is used with 60 seconds update interval and
// no offset
// NTPClient timeClient(ntpUDP);

// You can specify the time server pool and the offset, (in seconds)
// additionaly you can specify the update interval (in milliseconds).
int GTMOffset = 1;
NTPClient timeClient(ntpUDP, "europe.pool.ntp.org", GTMOffset*60*60, 60*60*1000);

void setup(){
  Serial.begin(115200);
  WiFi.begin(ssid, password);

  while ( WiFi.status() != WL_CONNECTED ) {
    delay ( 500 );
    Serial.print ( "." );
  }

  timeClient.begin();
  delay ( 1000 );
  if (timeClient.update()){
	 Serial.print ( "Adjust local clock" );
     unsigned long epoch = timeClient.getEpochTime();
     // HERE I'M UPDATE LOCAL CLOCK
     setTime(epoch);
  }else{
	 Serial.print ( "NTP Update not WORK!!" );
  }

}

void loop() {
  // Here I print time from local clock
  Serial.println(getEpochStringByParams(now()));

  delay(1000);
}

As you can see I create some simple function to parse and format the date, very usefully for me.

/**
 * Input time in epoch format and return tm time format
 * by Renzo Mischianti <www.mischianti.org> 
 */
static tm getDateTimeByParams(long time){
    struct tm *newtime;
    const time_t tim = time;
    newtime = localtime(&tim);
    return *newtime;
}
/**
 * Input tm time format and return String with format pattern
 * by Renzo Mischianti <www.mischianti.org>
 */
static String getDateTimeStringByParams(tm *newtime, char* pattern = (char *)"%d/%m/%Y %H:%M:%S"){
    char buffer[30];
    strftime(buffer, 30, pattern, newtime);
    return buffer;
}
/**
 * Input time in epoch format format and return String with format pattern
 * by Renzo Mischianti <www.mischianti.org> 
 */
static String getEpochStringByParams(long time, char* pattern = (char *)"%d/%m/%Y %H:%M:%S"){
//    struct tm *newtime;
    tm newtime;
    newtime = getDateTimeByParams(time);
    return getDateTimeStringByParams(&newtime, pattern);
}

You must pay attention to line 20, there you can choiche if you want UTC time, by set GTMOffset to 0 or if you want set your area GTM offset.

I live in Italy so I’m going to set GTM+1 and I set the last parameter to 60000 the update interval in milliseconds, this protocol use UDP as you can see, and you must choiche an NTP server.

But this is not sufficient, some zone have DST (Daylight saving time), so you must calculate the offset change, in the summer time Italy put another hour to the offset (so we are going formally in GTM+2), but a lot of country for energy saving purpose use DST.

DST Countries Map

But calculate that dynamic offset is very tedious, but exist a library that do It for you.

Timezone library

To manage DST you can use Timezone library.

You can download It directly from Arduino IDE --> Manage Libraries.

Timezone Library Arduino IDE

The usage is not so simple, but with the example, you can understand It.

First you must configure your DST, there is an example with some configuration that you can use as starting point.

// Australia Eastern Time Zone (Sydney, Melbourne)
TimeChangeRule aEDT = {"AEDT", First, Sun, Oct, 2, 660};    // UTC + 11 hours
TimeChangeRule aEST = {"AEST", First, Sun, Apr, 3, 600};    // UTC + 10 hours
Timezone ausET(aEDT, aEST);

// Moscow Standard Time (MSK, does not observe DST)
TimeChangeRule msk = {"MSK", Last, Sun, Mar, 1, 180};
Timezone tzMSK(msk);

// Central European Time (Frankfurt, Paris)
TimeChangeRule CEST = {"CEST", Last, Sun, Mar, 2, 120};     // Central European Summer Time
TimeChangeRule CET = {"CET ", Last, Sun, Oct, 3, 60};       // Central European Standard Time
Timezone CE(CEST, CET);

// United Kingdom (London, Belfast)
TimeChangeRule BST = {"BST", Last, Sun, Mar, 1, 60};        // British Summer Time
TimeChangeRule GMT = {"GMT", Last, Sun, Oct, 2, 0};         // Standard Time
Timezone UK(BST, GMT);

// UTC
TimeChangeRule utcRule = {"UTC", Last, Sun, Mar, 1, 0};     // UTC
Timezone UTC(utcRule);

// US Eastern Time Zone (New York, Detroit)
TimeChangeRule usEDT = {"EDT", Second, Sun, Mar, 2, -240};  // Eastern Daylight Time = UTC - 4 hours
TimeChangeRule usEST = {"EST", First, Sun, Nov, 2, -300};   // Eastern Standard Time = UTC - 5 hours
Timezone usET(usEDT, usEST);

// US Central Time Zone (Chicago, Houston)
TimeChangeRule usCDT = {"CDT", Second, Sun, Mar, 2, -300};
TimeChangeRule usCST = {"CST", First, Sun, Nov, 2, -360};
Timezone usCT(usCDT, usCST);

// US Mountain Time Zone (Denver, Salt Lake City)
TimeChangeRule usMDT = {"MDT", Second, Sun, Mar, 2, -360};
TimeChangeRule usMST = {"MST", First, Sun, Nov, 2, -420};
Timezone usMT(usMDT, usMST);

// Arizona is US Mountain Time Zone but does not use DST
Timezone usAZ(usMST);

// US Pacific Time Zone (Las Vegas, Los Angeles)
TimeChangeRule usPDT = {"PDT", Second, Sun, Mar, 2, -420};
TimeChangeRule usPST = {"PST", First, Sun, Nov, 2, -480};
Timezone usPT(usPDT, usPST);

In my case I must set CET and CEST parameter (line 10), and I’m going to modify original NTP example.

/*
 * Simple NTP client
 * https://mischianti.org/
 *
 * The MIT License (MIT)
 * written by Renzo Mischianti <www.mischianti.org>
 */

const char *ssid     = "<YOURSSID>";
const char *password = "<YOURPASSWD>";

#include <NTPClient.h>
// change next line to use with another board/shield
#include <ESP8266WiFi.h>
//#include <WiFi.h> // for WiFi shield
//#include <WiFi101.h> // for WiFi 101 shield or MKR1000
#include <WiFiUdp.h>
#include <TimeLib.h>
#include <time.h>
#include <Timezone.h>    // https://github.com/JChristensen/Timezone

/**
 * Input time in epoch format and return tm time format
 * by Renzo Mischianti <www.mischianti.org> 
 */
static tm getDateTimeByParams(long time){
    struct tm *newtime;
    const time_t tim = time;
    newtime = localtime(&tim);
    return *newtime;
}
/**
 * Input tm time format and return String with format pattern
 * by Renzo Mischianti <www.mischianti.org>
 */
static String getDateTimeStringByParams(tm *newtime, char* pattern = (char *)"%d/%m/%Y %H:%M:%S"){
    char buffer[30];
    strftime(buffer, 30, pattern, newtime);
    return buffer;
}

/**
 * Input time in epoch format format and return String with format pattern
 * by Renzo Mischianti <www.mischianti.org> 
 */
static String getEpochStringByParams(long time, char* pattern = (char *)"%d/%m/%Y %H:%M:%S"){
//    struct tm *newtime;
    tm newtime;
    newtime = getDateTimeByParams(time);
    return getDateTimeStringByParams(&newtime, pattern);
}

WiFiUDP ntpUDP;

// By default 'pool.ntp.org' is used with 60 seconds update interval and
// no offset
// NTPClient timeClient(ntpUDP);

// You can specify the time server pool and the offset, (in seconds)
// additionaly you can specify the update interval (in milliseconds).
int GTMOffset = 0; // SET TO UTC TIME
NTPClient timeClient(ntpUDP, "europe.pool.ntp.org", GTMOffset*60*60, 60*60*1000);

// Central European Time (Frankfurt, Paris)
TimeChangeRule CEST = {"CEST", Last, Sun, Mar, 2, 120};     // Central European Summer Time
TimeChangeRule CET = {"CET ", Last, Sun, Oct, 3, 60};       // Central European Standard Time
Timezone CE(CEST, CET);

void setup(){
  Serial.begin(115200);
  WiFi.begin(ssid, password);

  while ( WiFi.status() != WL_CONNECTED ) {
    delay ( 500 );
    Serial.print ( "." );
  }

  timeClient.begin();
  delay ( 1000 );
  if (timeClient.update()){
	 Serial.print ( "Adjust local clock" );
     unsigned long epoch = timeClient.getEpochTime();
     setTime(epoch);
  }else{
	 Serial.print ( "NTP Update not WORK!!" );
  }

}

void loop() {
  // I print the time from local clock but first I check DST 
  // to add hours if needed
  Serial.println(getEpochStringByParams(CE.toLocal(now())));

  delay(1000);
}

Now you have your time with DST correctly set.

Thanks


Spread the love

11 Responses

  1. Willem Nijntjes says:

    Renzo, Great you made the NTP receiver with DST.
    But I got a compile error on line 37.
    can you please help me to find the cause of it?

    Arduino: 1.8.12 (Linux), Board: "LOLIN(WEMOS) D1 R2 & mini, 80 MHz, Flash, Legacy (new can return nullptr), All SSL ciphers (most compatible), 4MB (FS:2MB OTA:~1019KB), v2 Lower Memory, Disabled, None, Only Sketch, 921600"

    /home/wim/Desktop/Tijd/Tijd.ino: In function 'tm getDateTimeByParams(long int)':
    Tijd:25:26: error: 'amp' was not declared in this scope
    newtime = localtime(&tim);
    ^
    Tijd:25:33: error: expected ';' before ')' token
    newtime = localtime(&tim);
    Multiple libraries were found for "TimeLib.h"
    Used: /home/wim/Programs/Arduino/arduino-1.8.12/libraries/TimeLib
    Not used: /home/wim/Programs/Arduino/arduino-1.8.12/libraries/Time
    Not used: /home/wim/Arduino/libraries/Time
    Multiple libraries were found for "NTPClient.h"
    Used: /home/wim/Arduino/libraries/NTPClient
    Not used: /home/wim/Programs/Arduino/arduino-1.8.12/libraries/NTPClient
    ^
    /home/wim/Desktop/Tijd/Tijd.ino: In function 'String getEpochStringByParams(long int, char*)':
    Tijd:37:39: error: 'amp' was not declared in this scope
    return getDateTimeStringByParams(&newtime, pattern);
    ^
    Tijd:37:59: error: expected ';' before ')' token
    return getDateTimeStringByParams(&newtime, pattern);
    ^
    exit status 1
    'amp' was not declared in this scope

    This report would have more information with
    "Show verbose output during compilation"
    option enabled in File -> Preferences.

    • Hi Willem,
      there is a problem in the code viewer of wordpress, you must substitute & amp; (without space) with &, It’s uncorrectly rendered.

      I think now the highlighter is fixed.

      Bye Renzo

  2. Pavel says:

    Hi Renzo,
    are you sure, that using of function adjustTime(epoch); is correct? It is defined in the library as increment of systime:
    void adjustTime(long adjustment) {
    sysTime += adjustment;
    }
    I use function setTime in this case.
    And second remark: In zour scatch isn’t defined format function getDateTimeStringByParams

    Pavel

    • Hi Pavel,
      you are right in all part, I lost the function, and I used adjust but It was an error (but I did not notice it because by initializing everything immediately the gap was minimal).
      Thanks for your support, I fixed all.
      Bye Renzo

  3. Cam says:

    I am lost. I thought I understood how to apply this to my code, but apparently not.
    Salient portion of my code:tmElements_t tm;

    time_t current_time() {
      return make_unixtime(usPT.toLocal(second()),  usPT.toLocal(minute()),  usPT.toLocal(hour()),  usPT.toLocal(day()),  usPT.toLocal(month()),  usPT.toLocal(year()));
    }

    time_t make_unixtime(uint8_t sec,  uint8_t mn,  uint8_t hr,  uint8_t dy,  uint8_t mon,  uint16_t yr) {
      tm.Second = sec;
      tm.Hour = hr;
      tm.Minute = mn;
      tm.Day = dy;
      tm.Month = mon;
      tm.Year = yr – 1970;
      return  makeTime(tm);
    }

    Not matter, it is still 8 hours behind. Please, I am so lost.

  4. Hi Cam,
    It’s difficult to understand, please open a topic on forum and attach the code, so I can check launch and test locally.
    Bye Renzo

  5. Robin says:

    I like the code, but as I understand timeclient.update() only is called in the setup loop ?
    So I don’t understand the hourly update-interval (60*60*100).
    Am I missing somtehing ? Or how is it triggered ?
    For testing purposes it would be convenient having a signal or counter when time is really updated. Is that possible ?

    • Hi Robin,
      yes, you can put the update in loop and when retrieve data the function return true.
      But, unless there is a problem in the microcontroller clock it is sufficient to update at the beginning and update the time.
      Bye Renzo

  6. Bart says:

    Is this code known to be compatible with RPi Pico W? Getting a warning about library timezone being compatible with AVR architecture only, plus, Serial.println(getEpochStringByParams(CE.toLocal(now())));
    outputs 01/01/1970 01:04:26 ….

  7. mohd rais says:

    Hello, I am running the code on Pico W by using windows 11 and Arduino IDE 2.3.2. It works whether I used ESP8266.h or WiFi.h. When I move the mouse pointer on the tm (static tm getDateTimeByParams{…}, the pop up wrote : struct tm As required by POSIX.1-2008, declare tm as incomplete type. How to declare as a complete type?

Leave a Reply

Your email address will not be published. Required fields are marked *