Timezone_Generic Library to convert UTC to local time

Timezone_Generic Library

How To Install Using Arduino Library Manager

Why do we need this Timezone_Generic Library

The Timezone_Generic Library is designed to work in conjunction with the Arduino Time library, which must also be installed on your system. This documentation assumes some familiarity with the Time library.

The primary goal of the Timezone_Generic Library is to convert Universal Coordinated Time (UTC) to the correct local time, whether it is Daylight Saving Time (a.k.a. summer time, DST) or standard time. The time source could be a GPS receiver, an NTP server, or a Real-Time Clock (RTC) set to UTC. But whether a hardware RTC or other time source is even present is immaterial, since the Time library can function as a software RTC without additional hardware (although its accuracy is dependent on the accuracy of the microcontroller's system clock.)

The Timezone_Generic Library implements two objects to facilitate time zone conversions:

  • A TimeChangeRule object describes when local time changes to daylight (summer) time, or to standard time, for a particular locale.

  • A Timezone object uses TimeChangeRules to perform conversions and related functions. It can also write its TimeChangeRules to or read them from EEPROM/DueFlashStorage/FlashStorage/LittleFS/SPIFFS. Multiple time zones can be represented by defining multiple Timezone objects.

The examples will demonstrate how to get the UTC time from NTP server, then update the DS3231 RTC to make sure the time is perfectly correct.
You can also modify the examples to read the NTP and update RTC once per every pre-determined period to ensure the RTC accuracy.

This Timezone_Generic Library is based on and modified from Jack Christensen's Timezone Library to add functions and support to many boards and shields.

Releases v1.2.5

  1. Add examples to use STM32 Built-In RTC.

Releases v1.2.4

  1. Initial porting to many Generic boards using WiFi/Ethernet modules/shields.
  2. Add support to SAMD21/SAMD51, nRF52, STM32F/L/H/G/WB/MP1 with WiFiNINA, ESP8266-AT, W5x00, ENC28J60, LAN8742A Ethernet modules/shields.
  3. Add support to SAM DUE DueFlashStorage, SAMD FlashStorage, nRF52 LittleFS, STM32 and AVR EEPROM
  4. Add functions.
  5. Completely new examples using NTP time to update DS3231 RTC.

Currently Supported Boards

  • ESP8266. To be done soon.
  • ESP32. To be done soon.
  • AdaFruit Feather nRF52832, nRF52840 Express, BlueFruit Sense, Itsy-Bitsy nRF52840 Express, Metro nRF52840 Express, NINA_B302_ublox, NINA_B112_ublox etc..
  • Arduino SAMD21 (ZERO, MKR, NANO_33_IOT, etc.).
  • Adafruit SAMD21 (Itsy-Bitsy M0, Metro M0, Feather M0, Gemma M0, etc.).
  • Adafruit SAMD51 (Itsy-Bitsy M4, Metro M4, Grand Central M4, Feather M4 Express, etc.).
  • Seeeduino SAMD21/SAMD51 boards (SEEED_WIO_TERMINAL, SEEED_FEMTO_M0, SEEED_XIAO_M0, Wio_Lite_MG126, WIO_GPS_BOARD, SEEEDUINO_ZERO, SEEEDUINO_LORAWAN, SEEED_GROVE_UI_WIRELESS, etc.)
  • STM32 (Nucleo-144, Nucleo-64, Nucleo-32, Discovery, STM32F1, STM32F3, STM32F4, STM32H7, STM32L0, etc.).
  • STM32F/L/H/G/WB/MP1 (Nucleo-64 L053R8,Nucleo-144, Nucleo-64, Nucleo-32, Discovery, STM32Fx, STM32H7, STM32Lx, STM32Gx, STM32WB, STM32MP1, etc.) having 64K+ Flash program memory.

Currently Supported WiFi Modules/Shields

  • ESP8266 built-in WiFi. To be done soon.
  • ESP32 built-in WiFi. To be done soon.
  • WiFiNINA using WiFiNINA or WiFiNINA_Generic library.
  • ESP8266-AT, ESP32-AT WiFi shields using WiFiEspAT or ESP8266_AT_WebServer library.

Currently Supported Ethernet Modules/Shields

  • W5x00's using Ethernet, EthernetLarge, Ethernet2 or Ethernet3 Library.
  • ENC28J60 using EthernetENC or UIPEthernet library.
  • LAN8742A using STM32Ethernet / STM32 LwIP libraries.

Currently Supported storage

  • ESP8266 EEPROM, LittleFS, SPIFFS. To be done soon.
  • ESP32 EEPROM, SPIFFS. To be done soon.
  • SAM DUE DueFlashStorage.
  • SAMD FlashStorage.
  • nRF52 LittleFS.
  • STM32 and AVR EEPROM.
1 Like

Sample Code

This is the RTC_WiFiNINA example

#include "defines.h"

#include <Timezone_Generic.h>             // https://github.com/khoih-prog/Timezone_Generic

#include <DS323x_Generic.h>               // https://github.com/khoih-prog/DS323x_Generic

DS323x rtc;

//////////////////////////////////////////

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

TimeChangeRule *tcr;        //pointer to the time change rule, use to get TZ abbrev

//////////////////////////////////////////

int status = WL_IDLE_STATUS;      // the Wifi radio's status

char timeServer[]         = "time.nist.gov";  // NTP server
unsigned int localPort    = 2390;             // local port to listen for UDP packets

const int NTP_PACKET_SIZE = 48;       // NTP timestamp is in the first 48 bytes of the message
const int UDP_TIMEOUT     = 2000;     // timeout in miliseconds to wait for an UDP packet to arrive

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;

// send an NTP request to the time server at the given address
void sendNTPpacket(char *ntpSrv)
{
  // set all bytes in the buffer to 0
  memset(packetBuffer, 0, NTP_PACKET_SIZE);
  // Initialize values needed to form NTP request
  // (see URL above for details on the packets)

  packetBuffer[0] = 0b11100011;   // LI, Version, Mode
  packetBuffer[1] = 0;     // Stratum, or type of clock
  packetBuffer[2] = 6;     // Polling Interval
  packetBuffer[3] = 0xEC;  // Peer Clock Precision
  // 8 bytes of zero for Root Delay & Root Dispersion
  packetBuffer[12]  = 49;
  packetBuffer[13]  = 0x4E;
  packetBuffer[14]  = 49;
  packetBuffer[15]  = 52;

  // all NTP fields have been given values, now
  // you can send a packet requesting a timestamp:
  Udp.beginPacket(ntpSrv, 123); //NTP requests are to port 123

  Udp.write(packetBuffer, NTP_PACKET_SIZE);

  Udp.endPacket();
}

void getNTPTime(void)
{
  static bool gotCurrentTime = false;

  // Just get the correct ime once
  if (!gotCurrentTime)
  {
    sendNTPpacket(timeServer); // send an NTP packet to a time server
    // wait to see if a reply is available
    delay(1000);

    if (Udp.parsePacket())
    {
      Serial.println(F("Packet received"));
      // We've received a packet, read the data from it
      Udp.read(packetBuffer, NTP_PACKET_SIZE); // read the packet into the buffer

      //the timestamp starts at byte 40 of the received packet and is four bytes,
      // or two words, long. First, esxtract the two words:

      unsigned long highWord = word(packetBuffer[40], packetBuffer[41]);
      unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]);
      // combine the four bytes (two words) into a long integer
      // this is NTP time (seconds since Jan 1 1900):
      unsigned long secsSince1900 = highWord << 16 | lowWord;
      Serial.print(F("Seconds since Jan 1 1900 = "));
      Serial.println(secsSince1900);

      // now convert NTP time into everyday time:
      Serial.print(F("Unix time = "));
      // Unix time starts on Jan 1 1970. In seconds, that's 2208988800:
      const unsigned long seventyYears = 2208988800UL;
      // subtract seventy years:
      unsigned long epoch = secsSince1900 - seventyYears;

      // print Unix time:
      Serial.println(epoch);
      
      // Get the time_t from epoch
      time_t epoch_t = epoch;

      // set the system time to UTC
      // warning: assumes that compileTime() returns US EDT
      // adjust the following line accordingly if you're in another time zone
      setTime(epoch_t);

      // Update RTC
      // Can use either one of these functions
      
      // 1) DateTime(tmElements_t). Must create tmElements_t if not present
      //tmElements_t tm;
      //breakTime(epoch_t, tm);
      //rtc.now( DateTime(tm) );
      
      // 2) DateTime(year, month, day, hour, min, sec)
      //rtc.now( DateTime(year(epoch_t), month(epoch_t), day(epoch_t), hour(epoch_t), minute(epoch_t), second(epoch_t) ) );

      // 3) DateTime (time_t)
      //rtc.now( DateTime(epoch_t) );

      // 4) DateTime(unsigned long epoch). The best and easiest way
      rtc.now( DateTime(epoch) );
       
      // print the hour, minute and second:
      Serial.print(F("The UTC time is "));       // UTC is the time at Greenwich Meridian (GMT)
      Serial.print((epoch  % 86400L) / 3600); // print the hour (86400 equals secs per day)
      Serial.print(':');

      if (((epoch % 3600) / 60) < 10)
      {
        // In the first 10 minutes of each hour, we'll want a leading '0'
        Serial.print('0');
      }
      Serial.print((epoch  % 3600) / 60); // print the minute (3600 equals secs per minute)
      Serial.print(':');

      if ((epoch % 60) < 10)
      {
        // In the first 10 seconds of each minute, we'll want a leading '0'
        Serial.print('0');
      }
      Serial.println(epoch % 60); // print the second

      gotCurrentTime = true;
    }
    else
    {
      // wait ten seconds before asking for the time again
      delay(10000);
    }
  }
}

//////////////////////////////////////////

// format and print a time_t value, with a time zone appended.
void printDateTime(time_t t, const char *tz)
{
  char buf[32];
  char m[4];    // temporary storage for month string (DateStrings.cpp uses shared buffer)
  strcpy(m, monthShortStr(month(t)));
  sprintf(buf, "%.2d:%.2d:%.2d %s %.2d %s %d %s",
          hour(t), minute(t), second(t), dayShortStr(weekday(t)), day(t), m, year(t), tz);
  Serial.println(buf);
}

void setup()
{
  Serial.begin(115200);
  while (!Serial);

  Serial.print("\nStart RTC_WiFiNINA on " + String(BOARD_NAME));
  Serial.println(" with " + String(SHIELD_TYPE));

  Wire.begin();

  // check for the presence of the shield
#if USE_WIFI_NINA
  if (WiFi.status() == WL_NO_MODULE)
#else
  if (WiFi.status() == WL_NO_SHIELD)
#endif
  {
    Serial.println(F("WiFi shield not present"));
    // don't continue
    while (true);
  }

#if USE_WIFI_NINA
  String fv = WiFi.firmwareVersion();
  if (fv < WIFI_FIRMWARE_LATEST_VERSION)
  {
    Serial.println(F("Please upgrade the firmware"));
  }
#endif

  // attempt to connect to WiFi network
  while ( status != WL_CONNECTED)
  {
    Serial.print(F("Connecting to WPA SSID: "));
    Serial.println(ssid);
    // Connect to WPA/WPA2 network
    status = WiFi.begin(ssid, pass);
  }

  // you're connected now, so print out the data
  Serial.print(F("You're connected to the network, IP = "));
  Serial.println(WiFi.localIP());

  Udp.begin(localPort);

  rtc.attach(Wire);
}

void loop()
{
  // Get time from NTP once, then update RTC
  // You certainly can make NTP check every hour/day to update RTC ti have better accuracy
  getNTPTime();

  // Display time from RTC
  DateTime now = rtc.now();

  Serial.println("============================");

  time_t utc = now.get_time_t();
  time_t local = myTZ.toLocal(utc, &tcr);
  
  printDateTime(utc, "UTC");
  printDateTime(local, tcr -> abbrev);
  
  delay(10000);
}
1 Like

Debug Termimal Output Samples

  1. This is terminal debug output when running TZ_NTP_WorldClock_Ethernet example on Adafruit nRF52 NRF52840_FEATHER_EXPRESS with ENC28J60 & EthernetENC Library.
Start TZ_NTP_WorldClock_Ethernet on NRF52840_FEATHER with ENC28J60 using EthernetENC Library
[ETHERNET_WEBSERVER] =========================
[ETHERNET_WEBSERVER] Default SPI pinout:
[ETHERNET_WEBSERVER] MOSI: 25
[ETHERNET_WEBSERVER] MISO: 24
[ETHERNET_WEBSERVER] SCK: 26
[ETHERNET_WEBSERVER] SS: 5
[ETHERNET_WEBSERVER] =========================
[ETHERNET_WEBSERVER] Board : NRF52840_FEATHER , setCsPin: 10
=========================
Currently Used SPI pinout:
MOSI:25
MISO:24
SCK:26
SS:5
=========================
Using mac index = 9
You're connected to the network, IP = 192.168.2.89
Packet received
Seconds since Jan 1 1900 = 3811983610
Unix time = 1602994810
The UTC time is 4:20:10

15:20:10 Sun 18 Oct 2020 AEDT Sydney
07:20:10 Sun 18 Oct 2020 MSK  Moscow
06:20:10 Sun 18 Oct 2020 CEST Paris
05:20:10 Sun 18 Oct 2020 BST  London
04:20:10 Sun 18 Oct 2020 UTC  Universal Coordinated Time
00:20:10 Sun 18 Oct 2020 EDT  New York
23:20:10 Sat 17 Oct 2020 CDT  Chicago
22:20:10 Sat 17 Oct 2020 MDT  Denver
21:20:10 Sat 17 Oct 2020 MST  Phoenix
21:20:10 Sat 17 Oct 2020 PDT  Los Angeles

15:20:20 Sun 18 Oct 2020 AEDT Sydney
07:20:20 Sun 18 Oct 2020 MSK  Moscow
06:20:20 Sun 18 Oct 2020 CEST Paris
05:20:20 Sun 18 Oct 2020 BST  London
04:20:20 Sun 18 Oct 2020 UTC  Universal Coordinated Time
00:20:20 Sun 18 Oct 2020 EDT  New York
23:20:20 Sat 17 Oct 2020 CDT  Chicago
22:20:20 Sat 17 Oct 2020 MDT  Denver
21:20:20 Sat 17 Oct 2020 MST  Phoenix
21:20:20 Sat 17 Oct 2020 PDT  Los Angeles
  1. The following is debug terminal output when running example TZ_NTP_WorldClock_WiFiNINA example on Arduino SAMD21 SAMD_NANO_33_IOT with WiFiNINA using WiFiNINA_Generic Library
Starting TZ_NTP_WorldClock_WiFiNINA on SAMD_NANO_33_IOT with WiFiNINA using WiFiNINA_Generic Library
Connecting to WPA SSID: HueNet1
You're connected to the network, IP = 192.168.2.128
Listening on port 2390
Packet received
Seconds since Jan 1 1900 = 3811983792
Unix time = 1602994992
The UTC time is 4:23:12

15:23:12 Sun 18 Oct 2020 AEDT Sydney
07:23:12 Sun 18 Oct 2020 MSK Moscow
06:23:12 Sun 18 Oct 2020 CEST Paris
05:23:12 Sun 18 Oct 2020 BST London
04:23:12 Sun 18 Oct 2020 UTC Universal Coordinated Time
00:23:12 Sun 18 Oct 2020 EDT New York
23:23:12 Sat 17 Oct 2020 CDT Chicago
22:23:12 Sat 17 Oct 2020 MDT Denver
21:23:12 Sat 17 Oct 2020 MST Phoenix
21:23:12 Sat 17 Oct 2020 PDT Los Angeles

15:23:22 Sun 18 Oct 2020 AEDT Sydney
07:23:22 Sun 18 Oct 2020 MSK Moscow
06:23:22 Sun 18 Oct 2020 CEST Paris
05:23:22 Sun 18 Oct 2020 BST London
04:23:22 Sun 18 Oct 2020 UTC Universal Coordinated Time
00:23:22 Sun 18 Oct 2020 EDT New York
23:23:22 Sat 17 Oct 2020 CDT Chicago
22:23:22 Sat 17 Oct 2020 MDT Denver
21:23:22 Sat 17 Oct 2020 MST Phoenix
21:23:22 Sat 17 Oct 2020 PDT Los Angeles
1 Like

If you are making changes to Jack Christensen's library, then you could consider addressing two things which I consider a design flaw in an otherwise very useful library.

  1. At the time you create a global TimeZone object, you have to give it a set of time zone rules, or a reference to such. This is not natural. If I produces a clock design, it is not clear in which time zone the user resides. That must be something which is entered during the initial configuration of the clock by the end user.
    Better would be to allow a "blank" global object and use a begin() method to set the actual time zone information.

  2. The handling of the EEPROM is rough, more or less assuming it is the only application using that EEPROM. It is also exactly this EEPROM handling which brings board dependencies, since this can be architecture specific. If the time zone information is configurable, it should be left to the developer to handle the EEPROM (or other) storage of time zone information.

To address the above points, I have my own lightly modified version of the library. The first point could be addressed easily and without breaking any existing code.

1 Like

Thanks for your valuable feedback.

  1. At the time you create a global TimeZone object, you have to give it a set of time zone rules, or a reference to such.

I'll definitely have a look at this issue and address it.

To address the above points, I have my own lightly modified version of the library. The first point could be addressed easily and without breaking any existing code.

Could you give the link of your modified version of the library to save me some time. Thanks.

  1. The handling of the EEPROM is rough, more or less assuming it is the only application using that EEPROM. It is also exactly this EEPROM handling which brings board dependencies, since this can be architecture specific. If the time zone information is configurable, it should be left to the developer to handle the EEPROM (or other) storage of time zone information.

This has been addressed, and depending on the boards, the appropriate storage will be used (EEPROM, DueFlashStorage, FlashStorage, LittleFS) with user-configurable address / offset.

The function to use the storage feature writeRules(address) is also optional.

1 Like

khoih-prog:
Thanks for your valuable feedback.

I'll definitely have a look at this issue and address it.

Could you give the link of your modified version of the library to save me some time. Thanks.
. . .

Here it is with a test sketch. I've used Jack Christensen's latest version and retro-fitted my changes to it. I originally used a much older version of this library (c. 5 years old) and he has made some changes in the mean time which I have not studied.

Timezone.cpp (11.2 KB)

Timezone.h (2.72 KB)

tz_test_V0_01.ino (1.35 KB)

1 Like

Thanks for your new and good-to-have library feature.
I'll include the mods in next library version and certainly your credit as well.

BR

Already updated to v1.2.6

Releases v1.2.6

  1. Allow un-initialized TZ then use begin() method to set the actual TZ. Credit of 6v6gt, see Timezone_Generic Library to convert UTC to local time
  2. Modify examples to use new un-initialized-TZ feature.

You'll see in example this code snippet

#define USING_INITIALIZED_TZ      false   //true

#if USING_INITIALIZED_TZ
  // US Eastern Time Zone (New York, Detroit,Toronto)
  TimeChangeRule myDST = {"EDT", Second, Sun, Mar, 2, -240};    // Daylight time = UTC - 4 hours
  TimeChangeRule mySTD = {"EST", First,  Sun, Nov, 2, -300};    // Standard time = UTC - 5 hours
  Timezone myTZ(myDST, mySTD);
#else
  // Allow a "blank" TZ object then use begin() method to set the actual TZ.
  // Feature added by 6v6gt (https://forum.arduino.cc/index.php?topic=711259)
  Timezone myTZ ;
  TimeChangeRule myDST;
  TimeChangeRule mySTD;
#endif

...

void setup()
{
...

#if !(USING_INITIALIZED_TZ)

  // Can read this info from EEPROM, storage, etc
  String tzName = "EDT/EST" ;

  // Time zone rules can be set as below or dynamically built, say through a configuration
  //  interface, or fetched from eeprom, flash etc.

  if ( tzName == "EDT/EST" )
  {
    // America Eastern Time
    myDST = (TimeChangeRule) {"EDT",  Second, Sun, Mar, 2, -240};    // Daylight time = UTC - 4 hours
    mySTD = (TimeChangeRule) {"EST",  First,  Sun, Nov, 2, -300};     // Standard time = UTC - 5 hours
  }
  else if ( tzName == "CET/CEST" ) 
  {
    // central Europe
    myDST = (TimeChangeRule) {"CEST", Last, Sun, Mar, 2, 120};
    mySTD = (TimeChangeRule) {"CET",  Last, Sun, Oct, 3, 60};
  }
  
  else if ( tzName == "GMT/BST" ) 
  {
    // UK
    myDST = (TimeChangeRule) {"BST",  Last, Sun, Mar, 1, 60};
    mySTD = (TimeChangeRule) {"GMT",  Last, Sun, Oct, 2, 0};
  }

  myTZ.init( myDST, mySTD ) ;
  
#endif

  Udp.begin(localPort);
}

and your contribution is noted in Contributions and-Thanks

1 Like