ESP32 Update Time of DS3231 RTC with NTP Server incl.Daylight Saving Time for DE

Hello,

I'm want to use an ESP32 with a DS3231 RTC to have a track of Time. Also, I want to be able to set the Daylight Saving Time, I thought about writing the code that it Updates the time over a NTP Server every last Sunday of March at 2 am and Oktober at 3 am. But therefore I need the code to know which date every last Sunday of March and Oktober is. I tried out to use the Timezone Library but I got error messages that it is configured on a different board.

That's my code. It's still all messy because I still try some thing.

If you have a code or idea to get daylight saving time for my project, please tell me.

Thanks in advance.

#include "RTClib.h"
#include "Wire.h"
#include "WiFi.h"
#include "time.h"

RTC_DS3231 rtc;

char daysOfTheWeek[7][12] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};

const char* ssid      = "Test";
const char* password  = "27042026";

const char* ntpServer = "de.pool.ntp.org";
const long  gmtOffset_sec = 0;
const int   daylightOffset_sec = 3600;

void setup () {
  Wire.begin(21,22);
  Serial.begin(115200);

  //Connect to WiFi
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi connected.");
  
  // Init and get the time
  configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
  printLocalTime();

  //disconnect WiFi as it's no longer needed
  WiFi.disconnect(true);
  WiFi.mode(WIFI_OFF);

  if (! rtc.begin()) {
    Serial.println("Couldn't find RTC");
    Serial.flush();
    abort();
  }
  
  if (rtc.lostPower()) {
    Serial.println("RTC lost power, let's set the time!");
    // When time needs to be set on a new device, or after a power loss, the
    // following line sets the RTC to the date & time this sketch was compiled
    rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
    // This line sets the RTC with an explicit date & time, for example to set
    // January 21, 2014 at 3am you would call:
    // rtc.adjust(DateTime(2014, 1, 21, 3, 0, 0));
  }

  // When time needs to be re-set on a previously configured device, the
  // following line sets the RTC to the date & time this sketch was compiled
  // rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
  // This line sets the RTC with an explicit date & time, for example to set
  // January 21, 2014 at 3am you would call:
  // rtc.adjust(DateTime(2014, 1, 21, 3, 0, 0));
}

void loop () {

    delay(1000);
    printLocalTime();
  
    DateTime now = rtc.now();

    Serial.print(now.year(), DEC);
    Serial.print('/');
    Serial.print(now.month(), DEC);
    Serial.print('/');
    Serial.print(now.day(), DEC);
    Serial.print(" (");
    Serial.print(daysOfTheWeek[now.dayOfTheWeek()]);
    Serial.print(") ");
    Serial.print(now.hour(), DEC);
    Serial.print(':');
    Serial.print(now.minute(), DEC);
    Serial.print(':');
    Serial.print(now.second(), DEC);
    Serial.println();
  
    Serial.println();
    delay(3000);
}

void printLocalTime(){
  struct tm timeinfo;
  if(!getLocalTime(&timeinfo)){
    Serial.println("Failed to obtain time");
    return;
  }
  Serial.println(&timeinfo, "%A, %B %d %Y %H:%M:%S");
  Serial.print("Day of week: ");
  Serial.println(&timeinfo, "%A");
  Serial.print("Month: ");
  Serial.println(&timeinfo, "%B");
  Serial.print("Day of Month: ");
  Serial.println(&timeinfo, "%d");
  Serial.print("Year: ");
  Serial.println(&timeinfo, "%Y");
  Serial.print("Hour: ");
  Serial.println(&timeinfo, "%H");
  Serial.print("Hour (12 hour format): ");
  Serial.println(&timeinfo, "%I");
  Serial.print("Minute: ");
  Serial.println(&timeinfo, "%M");
  Serial.print("Second: ");
  Serial.println(&timeinfo, "%S");

  Serial.println("Time variables");
  char timeHour[3];
  strftime(timeHour,3, "%H", &timeinfo);
  Serial.println(timeHour);
  char timeWeekDay[10];
  strftime(timeWeekDay,10, "%A", &timeinfo);
  Serial.println(timeWeekDay);
  Serial.println();
}

I also have been asking this question. In AVR GCC, I did some digging and found the DST code, which I tested and found that it works:

  set_dst(na_dst);

This points the time library to a callback function, usa_dst.h. I renamed it to be not so USA centric. :slight_smile:
There is another one, eu_dst.h presumably for Europe.

But SAMD and ESP use a different compiler, and the time.h functionality is not the same, AFAIK.

If you read the source of the callback function you can see how it operates. time library calls it anytime it needs local time, and it returns the DST offset given the current time:

/*
   (c)2012 Michael Duane Rice All rights reserved.

   Redistribution and use in source and binary forms, with or without
   modification, are permitted provided that the following conditions are
   met:

   Redistributions of source code must retain the above copyright notice, this
   list of conditions and the following disclaimer. Redistributions in binary
   form must reproduce the above copyright notice, this list of conditions
   and the following disclaimer in the documentation and/or other materials
   provided with the distribution. Neither the name of the copyright holders
   nor the names of contributors may be used to endorse or promote products
   derived from this software without specific prior written permission.

   THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
   AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
   ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
   LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
   CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
   SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
   INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
   CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
   ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
   POSSIBILITY OF SUCH DAMAGE.
*/

/* $Id: usa_dst.h 2344 2013-04-10 19:52:09Z swfltek $ */

/**
    Daylight Saving function for the USA. To utilize this function, you must
    \code #include <util/usa_dst.h> \endcode
    and
    \code set_dst(usa_dst); \endcode

    Given the time stamp and time zone parameters provided, the Daylight Saving function must
    return a value appropriate for the tm structures' tm_isdst element. That is...

    0 : If Daylight Saving is not in effect.

    -1 : If it cannot be determined if Daylight Saving is in effect.

    A positive integer : Represents the number of seconds a clock is advanced for Daylight Saving.
    This will typically be ONE_HOUR.

    Daylight Saving 'rules' are subject to frequent change. For production applications it is
    recommended to write your own DST function, which uses 'rules' obtained from, and modifiable by,
    the end user ( perhaps stored in EEPROM ).

*/

// 2020-06-15 housekeeping K. Willmott

#ifndef NA_DST_H
#define NA_DST_H

#ifdef __cplusplus
extern          "C" {
#endif

#include <time.h>
#include <inttypes.h>

#ifndef DST_START_MONTH
#define DST_START_MONTH MARCH
#endif

#ifndef DST_END_MONTH
#define DST_END_MONTH NOVEMBER
#endif

#ifndef DST_START_WEEK
#define DST_START_WEEK 2
#endif

#ifndef DST_END_WEEK
#define DST_END_WEEK 1
#endif

const int SAME_TIME = 0;
const int TIME_DIFFERENCE = ONE_HOUR;

int na_dst(const time_t * timer, int32_t * z) {
  time_t          t;
  struct tm       tmptr;
  uint8_t         month, week, hour, day_of_week, d;
  int             n;

  /* obtain the variables */
  t = *timer + *z;
  gmtime_r(&t, &tmptr);
  month = tmptr.tm_mon;
  day_of_week = tmptr.tm_wday;
  week = week_of_month(&tmptr, 0);
  hour = tmptr.tm_hour;

  if ((month > DST_START_MONTH) && (month < DST_END_MONTH))
    return TIME_DIFFERENCE;

  if (month < DST_START_MONTH)
    return SAME_TIME;
  if (month > DST_END_MONTH)
    return SAME_TIME;

  if (month == DST_START_MONTH) {

    if (week < DST_START_WEEK)
      return SAME_TIME;
    if (week > DST_START_WEEK)
      return TIME_DIFFERENCE;

    if (day_of_week > SUNDAY)
      return TIME_DIFFERENCE;
    if (hour >= 2)
      return TIME_DIFFERENCE;
    return SAME_TIME;
  }
  if (week > DST_END_WEEK)
    return SAME_TIME;
  if (week < DST_END_WEEK)
    return TIME_DIFFERENCE;
  if (day_of_week > SUNDAY)
    return SAME_TIME;
  if (hour >= 1)
    return SAME_TIME;
  return TIME_DIFFERENCE;

}

#ifdef __cplusplus
}
#endif

#endif

Oh, and by the way, I also want to do exactly the same thing, an ESP32 will get NTP time and convert to local time for display. If you ever find the answer, please let me know. :slight_smile:

If it's Sunday and the date is March 25-31 (inclusive), then it's the last Sunday of March.
If it's Sunday and the date is October 24-30 (inclusive), then it's the last Sunday of October.

Thanks for that edification but, at least for me, the point is to find the functionality in the existing time library and use it, rather than writing custom code. I do have in mind some kind of adaptation of the callback function so it can access a database of zones. But I definitely want to stick with the native functionality unless it turns out to be non-existent.

Reason: when you call 'localtime()' it unreasonable to have to play around with the result any more. It should just be The Local Time. I've proved that it already works in AVR GCC because I have a working test sketch:

/***************************************************
 ****************************************************/
// 2020-06-09 improve interface to time.h

/*
  struct tm {
  int8_t          tm_sec; //< seconds after the minute - [ 0 to 59 ]
  int8_t          tm_min; //< minutes after the hour - [ 0 to 59 ]
  int8_t          tm_hour; //< hours since midnight - [ 0 to 23 ]
  int8_t          tm_mday; //< day of the month - [ 1 to 31 ]
  int8_t          tm_wday; //< days since Sunday - [ 0 to 6 ]
  int8_t          tm_mon; //< months since January - [ 0 to 11 ]
  int16_t         tm_year; //< years since 1900
  int16_t         tm_yday; //< days since January 1 - [ 0 to 365 ]
  int16_t         tm_isdst; //< Daylight Saving Time flag
  };
*/

#include <time.h>
#include <locale.h>
//#include <util/usa_dst.h> https://www.nongnu.org/avr-libc/user-manual/usa__dst_8h_source.html
#include "na_dst.h"

#include <TerminalServer.h>
#include <vt100.h>
#include <Streaming.h>
#include "RTClib.h" // https://github.com/adafruit/RTClib

RTC_DS3231 rtc;
bool RTCfound = false;

TerminalServer term;
char input[COLUMNS_80];

unsigned long lastMillis;
const int displayInterval = 1000;

unsigned long lastTimeSync;
unsigned long timeSyncInterval = 60L * 1000;

uint8_t timeBuffer[4];
uint8_t minutes;
uint8_t seconds;

void setup() {
  Serial.begin(115200);
  term.begin(input, COLUMNS_80);
  printWelcome();
  // time system initialization:

  // required
  set_dst(na_dst);
  set_zone(-5 * ONE_HOUR);
  // optional
  // location is Toronto
  set_position( 43.741667 * ONE_DEGREE, -79.373333 * ONE_DEGREE);

  if (rtc.begin()) {
    RTCfound = true;
    syncToTimeProvider();
  }
  else {
    RTCfound = false;
    Serial.println(F("Couldn't find RTC, using CPU clock instead."));
    Serial.println(F("setting to Wednesday, January 1, 2020 12:00:00 AM"));
    set_system_time(1577836800L - UNIX_OFFSET);
  }
  printMenu();
  printPrompt();
  term.resume();
}

void loop() {
  term.update();
  parseInputUpdate();
  tickUpdate();
}

/*
   Serial input from the terminal
*/
void parseInputUpdate()
{
  if (term.available()) // command line received
  {
    parseInput(input);  // process command line
    term.resume();
  }
}

void parseInput(char* input)
{
  time_t timer;
  time_t sunrise;
  time_t sunset;
  time_t solarNoon;
  char sunRiseString[12];
  char sunSetString[12];
  char solarNoonString[12];
  struct tm * tm;

  // treat the first character as a command prefix...
  switch (input[0])
  {
    case 'a': //show configuration
      timer = time(NULL);
      tm = localtime(&timer);
      sunrise = sun_rise(&timer);
      sunset = sun_set(&timer);
      solarNoon = solar_noon(&timer);
      strftime(sunRiseString, 12, "%I:%M:%S %p", localtime(&sunrise));
      strftime(sunSetString, 12, "%I:%M:%S %p", localtime(&sunset));
      strftime(solarNoonString, 12, "%I:%M:%S %p", localtime(&solarNoon));

      //     Serial << endl
      //            << F("Current configuration settings:") << endl;
      drawInBox("Current configuration settings:");
      Serial << F("Local time: ");
      //             << ctime(&timer) << endl;
      tm = localtime(&timer);
      Serial << isotime(tm) << endl;
      Serial << F("Daylight Savings Time ") << (tm->tm_isdst ? "is" : "is not")
             << F(" in effect.") << endl << endl;
      Serial << F("Sun rises at: ")
             << sunRiseString << endl;
      Serial << F("Sun sets at: ")
             << sunSetString << endl;
      Serial << F("Solar noon is at: ")
             << solarNoonString << endl;
      Serial << F("Solar declination: ")
             << solar_declination(&timer)*RAD_TO_DEG << '\260' << endl << endl;
      Serial << F("Day ") << tm->tm_yday
             << " of " << (tm->tm_year + 1900) << endl;
      Serial << F("Week ") << week_of_year(tm, SUNDAY)
             << " of " << (tm->tm_year + 1900) << endl;
      Serial << F("Week ") << week_of_month(tm, SUNDAY)
             << " of " << "the month" << endl;
      Serial << F("Days in this month: ")
             << month_length(tm->tm_year, tm->tm_mon + 1) << endl;
      Serial << F("This year ") << (is_leap_year(tm->tm_year) ? "is" : "is not")
             << F(" a leap year.") << endl;
      Serial << F("moon phase: ") << moon_phase(&timer)
             << '%' << endl;
      if (RTCfound) {
        Serial << F("RTC temperature: ") << rtc.getTemperature()
               << F("\260C") << endl;
      }
      break;

    case 's': //set number
      someNumber = atoi(&input[1]);
      Serial << F("Number set to:");
      Serial << someNumber << endl;
      break;

    case 'l': //show local time
      timer = time(NULL);
      Serial << ctime(&timer)  << endl;
      break;

    case 'u': //show UTC time in ISO (also Canadian :=)
      timer = time(NULL);
      tm = gmtime(&timer);
      Serial << isotime(tm) << endl;
      break;

    case 'n': //show number
      Serial << someNumber << endl;
      break;

    case '\0':
      // Serial << F("no command entered") << endl;
      printMenu();
      break;

    default:
      Serial << F("unknown command: ") << input << endl;
      break;
  }
  printPrompt();
}

void printWelcome()
{
  Serial << F("\n\nWelcome, connected to this device") << endl;
}

void printPrompt()
{
  Serial << F("command:");
}

void printMenu()
{
  drawInBox("Serial Control Utility");
  Serial  << F("available commands:") << endl;

  Serial << VT_YELLOW
         << '\t' << F("l show local time") << endl
         << VT_DEFAULT_COLOUR;

  Serial << VT_YELLOW
         << '\t' << F("u show UTC time") << endl
         << VT_DEFAULT_COLOUR;

  Serial << VT_YELLOW
         << '\t' << F("a show all configuration") << endl
         << VT_DEFAULT_COLOUR;

  Serial << VT_YELLOW
         << '\t' << F("n show number") << endl
         << VT_DEFAULT_COLOUR;

  Serial << VT_YELLOW
         << '\t' << F("s <value> set number to value") << endl
         << VT_DEFAULT_COLOUR;
}

/*
   System timer tick and sync
*/
void tickUpdate() {
  static unsigned long lastTickTime;
  static int syncIntervalCount = 0;
  if (micros() - lastTickTime >= 1000000) {
    lastTickTime += 1000000;
    system_tick();
    if (RTCfound) {
      ++syncIntervalCount;
      if (syncIntervalCount >= 42) {
        syncIntervalCount = 0;
        syncToTimeProvider();
      }
    }
  }
}

// RTC specific time sync
void syncToTimeProvider() {
  DateTime now = rtc.now();
  set_system_time(now.unixtime() - UNIX_OFFSET);
}

Okay, this looks very promising:

and for reference

It appears you are making the classic mistake of trying to keep your RTC on local time. Been there, done that.

Just don't do it!

It seems like a good idea at the time, but it always seems to cause more and more problems. The best way to use the RTC is to simply keep it running UTC (always!), then the only updates needed are because of drift. It simplifies NTP management as well, since your RTC and NTP are using the same time.

When you need to display the time, convert to local and print that. For example, the 'Timezone' library lets you easily set up timezone rules, and then it handles all the conversions for you - UTC to local, local to UTC. If you need to do something at a particular time, just convert from local to UTC, then wait until that time.

With the RTC on UTC, then you never have to worry about handling time differences over a transition. You can be confident that every time value is a valid value - NOT true for local time!

MHotchin:
It appears you are making the classic mistake of trying to keep your RTC on local time.

Dude, no way! You completely misunderstand. the code in time.h takes Epoch time and manages it for you, this thread is about how to make it produce a correct local time for the locale.

How are you converting your UTC time to local, and on what architecture? I'm trying to use native functions instead of Arduino specific libraries, both to standardize and eliminate redundancy, and to enhance portability. It is pointless and not very future proof to use a custom library to do something that is well supported and documented in the language.

I think the reason there is such a mystery about this, is that not enough people are actually doing it, creating a kind of chicken and egg situation around the knowledge base. But C has had time management for many decades. As you can see if you investigate the first sketch I posted, it is perfectly possible to manage both Epoch time, local time zones, and local time zone rules like DST, all without including any DateTime library or TimeZone library, in AVR GCC. It is possible, on a very standard IDE platform programming even the simplest board like the UNO.

aarg:
How are you converting your UTC time to local, and on what architecture?

On both AVR (Mega 2560) and ESP32, I just use the Arduino 'Timezone' library. I tell it the rules for my timezone, and.... I'm done.

Timezone tz(TimeChangeRule("PDT", Second, Sun, Mar, 2, -7 * 60),
	TimeChangeRule("PST", First, Sun, Nov, 2, -8 * 60));

Well, I have used that library many times, but I want to do things in a more professional way. That would involve learning something about the greater C environment and leveraging it on this platform.

aarg:
Well, I have used that library many times, but I want to do things in a more professional way. That would involve learning something about the greater C environment and leveraging it on this platform.

Using an existing, proven library is hardly unprofessional. Rolling your own over an existing solution? Sure, that's silly.

But using a proven library? Even if the C 'local' stuff can make things happen, the ease of use is enough to promote the 'Timezone' library as the desirable solution. Witness this thread - instead of figuring out how to do things the 40 year old way, you could be done by now!

Well, I don't see what is so hard about it. The sketch has only a few lines of code that actually do the job, and they aren't really very mysterious. I'm just highlighting the ones that you actually need (once a wifi connection is established...):

#include "time.h"
...
  configTime(0, 0, "pool.ntp.org");
...
setenv("TZ", "GMT0BST,M3.5.0/01,M10.5.0/02",1); // You must include '0' after first designator e.g. GMT0GMT-1, ',1' is true or ON
...
  struct tm timeinfo;
  if(!getLocalTime(&timeinfo)){
    Serial.println("Failed to obtain time");
    return "Time Error";
  }
...

That's all you need for a fully functioning time system. It even performs an NTP update for you. I fail to see how the TimeZone library achieves any greater simplicity.

As I suggested before, I think the main difficulty is that nobody is aware that it's possible. So there is not a lot of help around for it. If more people use it, that could change.