Library for time zone conversions and automatic DST adjustments

Very usefull as there are many questions about getting the time zone right (after I had written an NTP-RTC article on the playground).

I am wondering about the DST algorithm, does it include Europe? did not dive into the code yet :slight_smile:

robtillaart:
Very usefull as there are many questions about getting the time zone right (after I had written an NTP-RTC article on the playground).

I am wondering about the DST algorithm, does it include Europe? did not dive into the code yet :slight_smile:

Hi Rob, yes it should work for Europe, see the "WorldClock" example supplied with the library. The examples should work right out of the box on a bare Arduino. Glad to have you kick the tires, let me know if you find issues!

Should also work in the Southern hemisphere, I knew Nick would never let me live it down otherwise :wink:

The algorithm works from rules that you supply. For example, US Eastern Daylight Time, which has a UTC offset of -240 minutes, starts on the 2nd Sunday in March at 02:00 local time. So the rules are coded, in pairs, like this:

TimeChangeRule usEdt = {"EDT", Second, Sun, Mar, 2, -240};    //UTC - 4 hours
TimeChangeRule usEst = {"EST", First, Sun, Nov, 2, -300};     //UTC - 5 hours

I was pulling my hair out at one point during testing but discovered I had the rules wrong! :blush:

HI Jack,

The library is well done!

Just a few minor remarks

  1. For the countries that do not have DST, only an offset to UTC, a 3rd constructor would make sense. This would imply one object less. alternative is to use a single object for both parameters?
Timezone::Timezone(int TZoffset)  ==> conflict with readRules Address constructor...

better:?

Timezone::Timezone(TimeChangeRule stdStart)
{
    _dst = stdStart;
    _std = stdStart;
}
  1. Using EEPROM for storing DST makes sense, alternative is to put the TZ in PROGMEM as timezones are typically known at compiletime? Yes I can imagine at least 2 applications where this is not true.

Maybe an array of the 24 TZ's in Progmem (=288 bytes)?

Rob, thanks for the feedback! Actually I had changed the ReadMe file as follows but haven't pushed it up to github yet. So one less object this way as well:

For a time zone that does not change to daylight/summer time, pass the same rule
twice to the constructor, e.g.:
    Timezone usAZ(usMST, usMST);

In fact the WorldClock example does this for Arizona:

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

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

Still there should be little harm in the additional constructor, so I think I will add it.

In reality there are more than 24 timezones, given that a lot of locales use DST. So to predefine the 24 "standard" time zones, i.e. Alpha, Bravo, Charlie, etc. may not be all that useful. I didn't intend the library to replace the tz database, and thereby be at the whim of numerous governmental decisions :wink:

Thanks again, let me know if you have further suggestions!

  1. should the EEPROM address be checked if it is valid / in range. And that there are 2 x 12 bytes free after it. Or a note in the readme.txt ...

  2. _dst.offset * SECS_PER_MIN (+other) is calculated 8 times, maybe the internal representation could be an long which is calculated only once.
    you would get rid of all multiplications for only 4 bytes extra per TZ. Don't know if and how much smaller the footprint of the lib would become .

Its a pity the max offset does not fit in an int.

Good points, thanks again.

(1) The ReadMe does talk to the size of the TimeChangeRule, and that could be elaborated on. Being very conscious of resources, mostly size in this case, I'm not terribly inclined to check the address. I also see that the Arduino EEPROM library doesn't validate addresses.

(2) Yes that bothered me just a little. Like I said, I didn't want to waste space, but on the other hand I didn't sweat processing overhead terribly, my assumption there being that there just isn't a lot of reason to do time zone conversions very frequently. I'd think once per second would be about it, that being the typical RTC resolution. BUT still a good point that is worth experimenting with. One of those things that I just didn't think through any further at the time.

(2) I did some testing with the worldclock sample yesterday and a version in which the TimeChangeRule was using a long (calculated in the constructor) and no multiply in the functions was larger than the original. If I recall correctly orig==7508 and new==7632 This seems quite an increase as there are only 9*2 TimeChangeRules = 18 rules * 2 bytes = 36 bytes more memory and there should be less math...

(1) No big deal, EEPROM will probably wrap around as the higher addresslines are ignored...

Hmmm, interesting. I haven't had time to play with it. I might look at the assembly code, maybe some functions got inlined?

Hi Jack, thanks for this fine library! I recently wrote decoder DCF77 library (for the atomic time broadcasted by the DCF77 radiostation) (see this thread) which now includes an example of usage together with your TimeZone library.

Before I read the TimeZone README I had not considered that converting from local time to UTC is ambiguous in the hour before switching to winter time. Luckily for us DCF77 users, the DCF77 data package includes info about DST, so in the latest version I added a function to the library that unambiguously converts CET to UTC time.

Hi mrTee, thanks for the feedback, glad you found the library useful! We have WWV here which is similar to DCF77 in that it has a DST/Summer time indicator. Have not tried a WWV receiver myself, although I do have several clocks around that pick up the signal.

Yes daylight and summer time is evil with the ambiguity. Back in the day, every fall we used to have to shut the mainframe computers down and let them sit for an hour, else, the system and applications could generate redundant time stamps in logs and such. I'll have to ask around and see if they ever fixed that! My preference is to always use UTC internally and convert it to local time for human consumption.

Hi Jack,

I am trying to work on a DCF77 radio controlled clock with timezome support but it guess I need a bit of help with finding the timezone selection.
What I am trying to do is the following :
I would like to be able to select a timezone (with switches on the Arduino board).
At this moment I am stuck with the CET (Central European Time) showing on the LCD.

I was thinking if condition in the setSyncProvider line or unsigned long getDCFTime() {.... But no luck yet.

Could you please help me?

Thank you very much

Here is my code:

#include <LiquidCrystal.h>
#include "MsTimer2.h"
#include "DCF77.h"
#include "Time.h"
#include "Timezone.h"

const int dcf77_sample_pin = A15;
const int dcf77_analog_sample_pin = A15;
const int filtered_dcf77_pin = 2;
const int bit0_pin = 3;
const int bit15_pin = bit0_pin + 15;
const int backlightPin = 6;
const int switch1 = 22;
const int switch2 = 23;
const int switch3 = 24;
const int switch4 = 25;
const int switch5 = 26;

DCF77 DCF = DCF77(filtered_dcf77_pin, 0);
volatile time_t time = 0;

LiquidCrystal lcd(33, 32, 31, 37, 36, 35, 34);

TimeChangeRule *tcr;
Timezone UK(00);
Timezone EET(32);
Timezone CET(64);
Timezone MSK(96);
Timezone FET(128);

void low_pass_filter() {
  const unsigned int decimal_offset = 10000;
  static unsigned long int smoothed = 0*decimal_offset;
  const unsigned long int input = analogRead(dcf77_analog_sample_pin)>200? decimal_offset: 0;
  const unsigned int  N = 72;
  smoothed = ((N-1) * smoothed + input) / N;
  static int square_wave_output = 0;
  if ((square_wave_output == 0) == (smoothed >= decimal_offset/2)) {
    square_wave_output = 1-square_wave_output;
    smoothed = square_wave_output? decimal_offset: 0;
  }
  digitalWrite(filtered_dcf77_pin, square_wave_output);    
}

unsigned long getDCFTime() {
  time_t DCFtime = DCF.getUTCTime();
  if (DCFtime != 0) {
    time_t localTime = CET.toLocal(DCFtime, &tcr);
    return localTime;
  } 
  return 0;
}
    
void printDigits(int digits){
  lcd.print(":");
  if(digits < 10)
    lcd.print('0');
  lcd.print(digits);
}

void digitalClockDisplay() {
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print(hour());
  printDigits(minute());
  printDigits(second());
  lcd.setCursor(9, 0);
  lcd.print(tcr -> abbrev);
  lcd.setCursor(13, 0);
  lcd.print(dayShortStr(weekday()));
  lcd.setCursor(2, 1);
  lcd.print(day());
  lcd.print(" ");
  lcd.print(monthShortStr(month()));
  lcd.print(" ");
  lcd.print(year());
} 

void displayUpdate() {
  static time_t prevTimeDisplay = 0; 
  if (now() != prevTimeDisplay)
  {
    prevTimeDisplay = now();
    digitalClockDisplay();
  }
}

void setup() {
  pinMode(switch1, INPUT);
  pinMode(switch2, INPUT);
  pinMode(switch3, INPUT);
  pinMode(switch4, INPUT);
  pinMode(switch5, INPUT);
  pinMode(filtered_dcf77_pin, OUTPUT);
  pinMode(dcf77_sample_pin, INPUT_PULLUP);
  for (int pin=bit0_pin; pin < bit15_pin; ++pin) {
    pinMode(pin, OUTPUT);
    digitalWrite(pin, HIGH);
  }
  delay(500);
  for (int pin=bit0_pin; pin < bit15_pin; ++pin) {
    digitalWrite(pin, LOW);
  }
  MsTimer2::set(1, low_pass_filter);
  MsTimer2::start();
  DCF.Start();
  setSyncInterval(30);
  setSyncProvider(getDCFTime);
  digitalWrite(backlightPin, HIGH);
  lcd.begin(16, 2);
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("DCF77 Clock");
  delay(1500);
  lcd.clear();
}

void loop() {
  displayUpdate();
}

Moderator edit:
</mark> <mark>[code]</mark> <mark>

</mark> <mark>[/code]</mark> <mark>
tags added.

americo1977:
I would like to be able to select a timezone (with switches on the Arduino board).
At this moment I am stuck with the CET (Central European Time) showing on the LCD.

Hello!

Rather than using a hard-coded Timezone to convert to local time, e.g.

    time_t localTime = CET.toLocal(DCFtime, &tcr);

instead declare a pointer to Timezone and set it to point to the desired Timezone object. Then use the pointer variable to do the conversion to local time:

    Timezone *tz;
    tz = &UK;
    time_t localTime = (*tz).toLocal(DCFtime, &tcr);

I don't see any logic in the sketch to actually read the switches, obviously that will be needed, and it will just need to set the pointer variable to the desired timezone. I think you're just asking about how to handle the timezone, so I'll assume you've got working with the switches figured out.

Here is a comprehensive example that works similarly. It uses US timezones rather than European but still should illustrate the technique :wink:

HTH ... jc

Hello Jack,

Thank you very much for your help, pointers are indeed better than the if or if else conditions.
Actually I already added the switches in the code because they will be used to select the timezone, instead of using a menu on LCD screen (I still have lots to learn, and I am not at this point yet).

Thank you very much!

Kind regards,

Américo

thanks... works very well...

for portugual:

#include <DS1307RTC.h>
#include <Time.h>
#include <Timezone.h>
#include "DS1307.h"

DS1307 clock;

TimeChangeRule PST = {"PST", Last, Sun, Mar, 1, 60};        //Portuguese Summer Time -  "UTC + 1" or GMT + 1
TimeChangeRule GMT = {"GMT", Last, Sun, Oct, 2, 0};         //Portuguese winter Time -  "UTC + 0" or GMT 
Timezone PT(PST, GMT);
TimeChangeRule *tcr;        //pointer to the time change rule, use to get TZ abbrev
time_t utc;
time_t local;


//SET TIME: (GROOVE CLOCK)
clock.begin();
  clock.fillByYMD(2013,10,29);
  clock.fillByHMS(10,52,00);  //--> GMT (Winter)
  clock.fillDayOfWeek(SUN);
  clock.setTime();//write time to the RTC chip

 char GMTtime[15];
 GMTtime[0]='\0';
Serial.println(GetGMTtime(GMTtime));
 GMTtime[0]='\0';

char* GetGMTtime(char* timeGMT){

  utc = now();

  local = PT.toLocal(utc, &tcr);

  char buffHH[6];
  buffHH[0]='\0';

  char buffMM[6];
  buffMM[0]='\0';


  timeGMT[0] = '\0';

  if(hour(local)<10){
    strcat(timeGMT,"0");
    strcat(timeGMT,itoa(hour(local),buffHH,10));
  }
  else{
    strcat(timeGMT,itoa(hour(local),buffHH,10));
  }

  strcat(timeGMT,":");

  if(minute(local)<10){
    strcat(timeGMT,"0");
    strcat(timeGMT,itoa(minute(local),buffMM,10));
  }
  else{
    strcat(timeGMT,itoa(minute(local),buffMM,10));
  }

  buffMM[0]='\0';
  buffHH[0]='\0';

  //    strcat(timeGMT,":");
  //    strcat(timeGMT,itoa(second(local),bufftemp,10));
  //    Serial.println(timeGMT);
  return timeGMT;



}

jot4p:
thanks... works very well...

You're very welcome, thanks for the feedback. Unfortunately, it's that time of year :frowning:

Hi,

this library works only with RTC's?
Is there something like "Auto Daylight Savings Time" for ntp?

MrGlasspoole:
Hi,
this library works only with RTC's?

Hello! The Timezone library is unaware of RTCs; it will work with or without one. It simply converts time values stored in time_t variables. Include the Arduino Time library to define the time_t data type.

Is there something like "Auto Daylight Savings Time" for ntp?

NTP sends UTC only, without adjustments for daylight time or time zone.

[quote author=Jack Christensen link=topic=96891.msg1449873#msg1449873 date=1383232148]
NTP sends UTC only, without adjustments for daylight time or time zone.
[/quote]Thats the reason i was asking. So i can do it like my router with your library.
There i can set the Time Zone and NTP Time Server and have a Auto Daylight Savings Time checkbox.

Now i see "the Time library will function as a software RTC without additional hardware" in the readme.
So the first sentence "A primary aim of this library is to allow a Real Time Clock (RTC)" did confuse me.

MrGlasspoole:
So the first sentence "A primary aim of this library is to allow a Real Time Clock (RTC)" did confuse me.

Hmmm, yes, thanks for pointing that out. I'll give some thought to clarifying that.

I like the library and I may use it for an upcomming project but it really makes my head hurt
because I have 30 years of using unix time functions burned into my head.
With the unix time functions you never calculate a time_t value for a local time.
There is typically a single system time_t (which is always utc) and then you convert that to a struct tm
using localtime() where you apply the timezone offsets and any potential DST offset to get
the broken down time values.

This library takes a different approach of converting from the normal/system time_t to a local time_t.

Then there is the differences that Michael did when he created the Time library
Michael deviated from the unix time functions by changing some of the paramters from being zero
based to being one based.
His tmElements_t wDay and Month are 1 based wherease the struct tm tm_wday and tm_mon
are zero based.

While it call can be mad to work, it always makes my head hurt when I use the Arduino Time stuff...

But nice job on the library, given the environment.

--- bill