diff --git a/README.md b/README.md
index aae1a5d..b27a83d 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,14 @@
-# ezTime Documentation
+# ezTime, an Arduino library for all of time *
-**ezTime, pronounced "Easy Time", is a very easy to use Arduino time and date library that provides NTP network time lookups, extensive timezone support, formatted time and date strings, millisecond precision and more.**
+**ezTime - pronounced "Easy Time" - is a very easy to use Arduino time and date library that provides NTP network time lookups, extensive timezone support, formatted time and date strings, millisecond precision and more.**
+
+> * limitations may apply, see "2036 and 2038" chapter
+
+
## A brief history of ezTime
-I was working on M5ez, an interface library to easily make cool-looking programs for the "M5Stack" ESP32 hardware. The status bar of that needed to display the time. I figured I would use [Time](https://github.com/PaulStoffregen/Time), Paul Stoffregen's library to do time things on Arduino. Then I needed to sync that to an NTP server, so I figured I would use [NTPclient](https://github.com/arduino-libraries/NTPClient), one of the existing NTP client libraries. And then I wanted it to show the local time, so I would need some way for the user to set an offset between UTC and local time.
+I was working on [M5ez](https://github.com/ropg/M5ez), an interface library to easily make cool-looking programs for the "M5Stack" ESP32 hardware. The status bar of that needed to display the time. That was all, I swear. I figured I would use [Time](https://github.com/PaulStoffregen/Time), Paul Stoffregen's library to do time things on Arduino. Then I needed to sync that to an NTP server, so I figured I would use [NTPclient](https://github.com/arduino-libraries/NTPClient), one of the existing NTP client libraries. And then I wanted it to show the local time, so I would need some way for the user to set an offset between UTC and local time.
So far, so good.
@@ -12,27 +16,31 @@ Then I remembered how annoyed I always am when daylight savings time comes or go
And then I also wanted to print time in various formats. Wouldn't it be nice to have some function to print formatted time like many programming languages offer them?
-Overlooking the battlefield after implementing some of this, it seemed like there had to be a better way. Especially, some way in which all this work would benefit more people. I decided to make the mother of all time libraries.
+Overlooking the battlefield after implementing some part of this, it seemed like there had to be a better way. Especially, some way in which all this work would benefit more people. This is how ezTime, the project that was once only going to take a few days, came to be.
-## ezTime is:
+## ezTime is ...
-**self-contained**: It only depends on other libraries for networking (to do NTP and timezone data lookups). And if it is running on an ESP32, it will use the `Preferences` library to store cached timezone data to minimize lookups. It uses [timezoneapi.io](https://timezoneapi.io/) to get its timezone data, they provide 50 free lookups per user per day. (You can turn all of this off at compile time, e.g. if you have no networking and/or another time source.)
+**self-contained**: It only depends on other libraries to get online, but then it doesn't need other libraries for NTP and timezone data lookups.
-**precise**: An NTP request to pool.ntp.org only takes 40ms round-trip on my home DSL, so adding sub-second precision to a time library makes sense. ezTime reads the fractional seconds and tries to account for network latency to give you precise time.
+**precise**: Unlike other libraries, ezTime does not throw away or mangle the fractional second information from the NTP server. An NTP request to pool.ntp.org only takes 40ms round-trip on home DSL these days, so adding sub-second precision to a time library makes sense. ezTime reads the fractional seconds and tries to account for network latency to give you precise time.
**backwards compatible**: Anything written for the existing Arduino time library will still work. You can set which timezone the sketch should be in, or have it be in UTC which is the default. But you can also set and express time referring to multiple timezones, all very easy and intuitive.
-**robust**: On ESP32 it doesn't fail if the timezone api goes away: it will cache the data for any timezones used. If that server is unreachable it will not initialise new timezones or do a yearly update of the rules, but it will still work on timezones you have already used.
+**robust**: It doesn't fail if the timezone api goes away: it can use cached data, which ezTime can store in EEPROM (AVR Arduinos) or NVS (ESP32 through Preferences library).
-**informative**: No need to guess while you're working on something, ezTime can print messages to the serial port at your desired level of detail, telling you when it gets an NTP update and how much your internal clock was off, for instance.
+**informative**: No need to guess while you're working on something, ezTime can print messages to the serial port at your desired level of detail, telling you about the timezone's daylight savings info it receives or when it gets an NTP update and by how much your internal clock was off, for instance.
**time-saving**: No more time spent on writing code to print date or time in some nicer way. Print things like "8:20 PM" or "Saturday the 23rd of August 2018" with ease. Prevent display-flicker with minuteChanged() and secondChanged() functions without storing any values to compare.
+**Small enough**: Works with all features and full debugging information on an Arduino Uno with an Ethernet Shield, leaving 2/3 of RAM but not too much flash for you to work with. (Even on a good day there isn't that much left if you want to do anything that involves networking.) Various `#define` options let you leave parts of it out if you want to make it smaller: you can even leave out the networking altogether if you have a different time source.
+
**easy to use**: Don't believe it until you see it. Have a look at some of these examples to see how easy it is to use.
-### Timezones
+### Timezones
+
+(a complete sketch to show how simple it is)
```
#include
@@ -44,33 +52,19 @@ void setup() {
ezTime.waitForSync();
- Serial.println("UTC: " + UTC.dateTime());
+ Serial.println("UTC: " + UTC.dateTime());
- Timezone myTZ;
-
- // Anything with a slash in it is interpreted as an official timezone name
- // https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
- myTZ.setLocation("Pacific/Auckland");
- Serial.println("Auckland: " + myTZ.dateTime());
-
- // Anything else is parsed as an address to see if that resolves
- myTZ.setLocation("Paris, Texas");
- Serial.println("Paris, Texas: " + myTZ.dateTime());
-
- // The empty string is resolved to the GeoIP location of your IP-address
- myTZ.setLocation("");
- Serial.println("Your local time: " + myTZ.dateTime());
+ Timezone NewZealand;
+ NewZealand.setLocation("Pacific/Auckland");
+ Serial.println("New Zealand time: " + NewZealand.dateTime());
}
-void loop() {
-}
+void loop() { }
```
```
-UTC: Saturday, 25-Aug-18 14:13:03 UTC
-Auckland: Sunday, 26-Aug-18 03:13:04 NZST
-Paris, Texas: Saturday, 25-Aug-18 09:13:04 CDT
-Your local time: Saturday, 25-Aug-18 16:13:04 CEST
+UTC: Friday, 07-Sep-2018 11:25:10 UTC
+New Zealand time: Friday, 07-Sep-2018 23:25:11 NZST
```
@@ -78,56 +72,27 @@ Your local time: Saturday, 25-Aug-18 16:13:04 CEST
### Formatted date and time
```
-#include
-#include
-
-void setup() {
-
- Serial.begin(115200);
- WiFi.begin("your-ssid", "your-password");
-
- ezTime.waitForSync();
-
- Serial.println();
- Serial.println("Time in various internet standard formats ...");
- Serial.println();
- Serial.println("ATOM: " + UTC.dateTime(ATOM));
Serial.println("COOKIE: " + UTC.dateTime(COOKIE));
Serial.println("IS8601: " + UTC.dateTime(ISO8601));
Serial.println("RFC822: " + UTC.dateTime(RFC822));
Serial.println("RFC850: " + UTC.dateTime(RFC850));
- Serial.println("RFC1036: " + UTC.dateTime(RFC1036));
- Serial.println("RFC1123: " + UTC.dateTime(RFC1123));
- Serial.println("RFC2822: " + UTC.dateTime(RFC2822));
Serial.println("RFC3339: " + UTC.dateTime(RFC3339));
Serial.println("RFC3339_EXT: " + UTC.dateTime(RFC3339_EXT));
Serial.println("RSS: " + UTC.dateTime(RSS));
- Serial.println("W3C: " + UTC.dateTime(W3C));
Serial.println();
- Serial.println(" ... and any other format, like \"" + UTC.dateTime("l ~t~h~e jS ~o~f F Y, g:i A") + "\"");
-}
-
-void loop() {
-}
+ Serial.println("or like " + UTC.dateTime("l ~t~h~e jS ~o~f F Y, g:i A") );
```
```
-Time in various internet standard formats ...
-
-ATOM: 2018-08-25T14:23:45+00:00
COOKIE: Saturday, 25-Aug-2018 14:23:45 UTC
IS8601: 2018-08-25T14:23:45+0000
RFC822: Sat, 25 Aug 18 14:23:45 +0000
RFC850: Saturday, 25-Aug-18 14:23:45 UTC
-RFC1036: Sat, 25 Aug 18 14:23:45 +0000
-RFC1123: Sat, 25 Aug 2018 14:23:45 +0000
-RFC2822: Sat, 25 Aug 2018 14:23:45 +0000
RFC3339: 2018-08-25T14:23:45+00:00
RFC3339_EXT: 2018-08-25T14:23:45.846+00:00
RSS: Sat, 25 Aug 2018 14:23:45 +0000
-W3C: 2018-08-25T14:23:45+00:00
- ... and any other format, like "Saturday the 25th of August 2018, 2:23 PM"
+or like Saturday the 25th of August 2018, 2:23 PM
```
@@ -135,39 +100,9 @@ W3C: 2018-08-25T14:23:45+00:00
### milliseconds
```
-#include
-#include
-
-void setup() {
- Serial.begin(115200);
- WiFi.begin("your-ssid", "your-password");
-
- ezTime.setInterval(60);
- ezTime.waitForSync();
-
- Serial.println();
-
for (int n = 0; n < 10; n++) {
Serial.println(UTC.dateTime("l, d-M-y H:i:s.v T"));
}
-
- Serial.println();
- Serial.println("Those milliseconds between the first and the last line ...");
- Serial.println();
- Serial.println(" ... most of that is spent sending to the serial port.");
- Serial.println();
- Serial.println();
- Serial.println();
- Serial.println("And ezTime is not making those milliseconds up either.");
- Serial.println();
- Serial.println(" ... Stick around as we do an NTP request every minute.");
- ezTime.debug(INFO);
-}
-
-void loop() {
- now();
- delay(1000);
-}
```
```
@@ -181,20 +116,28 @@ Saturday, 25-Aug-18 14:32:53.293 UTC
Saturday, 25-Aug-18 14:32:53.297 UTC
Saturday, 25-Aug-18 14:32:53.300 UTC
Saturday, 25-Aug-18 14:32:53.303 UTC
+```
-Those milliseconds between the first and the last line ...
-
- ... most of that is spent sending to the serial port.
+> *This is on my ESP32. See how it fills up the serial buffer real fast at first, and then has to wait for the characters to be sent before it can return?*
+
+### Rich information and *... oh my just look at these NTP updates*
-And ezTime is not making those milliseconds up either.
+```
+[...]
+ ezTime.setInterval(60);
+ ezTime.setDebugLevel(INFO);
+}
- ... Stick around as we do an NTP request every minute.
+void loop() {
+ now();
+ delay(1000);
+}
+```
+```
ezTime debug level set to INFO
-Querying pool.ntp.org ... success (round trip 47 ms)
-Received time: Saturday, 25-Aug-18 14:33:53.363 UTC (internal clock was 31 ms slow)
Querying pool.ntp.org ... success (round trip 42 ms)
Received time: Saturday, 25-Aug-18 14:34:53.410 UTC (internal clock was 1 ms fast)
Querying pool.ntp.org ... success (round trip 43 ms)
@@ -220,102 +163,78 @@ ezTime is an Arduino library. To start using it with the Arduino IDE:
* Click the row to select the library.
* Click the Install button to install the library.
-in File -> Examples you will now see an ezTime heading down under "Examples from custom libraries". You can try running some of these examples to see if it all works. ezTime is made to be, as the name implies, quite easy to use. So you'll probably understand a lot of how things work from just looking at the examples.
+in File -> Examples you will now see an ezTime heading down under "Examples from custom libraries". You can try running some of these examples to see if it all works. ezTime is made to be, as the name implies, quite easy to use. So you'll probably understand a lot of how things work from just looking at the examples. Now either just play with those and use the rest of this documentation only when you get stuck, or keep reading to see how things work in ezTime.
-When you include the ezTime library, it creates two objects for you to interact with. One object is called `ezTime`, and its methods allow you to change defaults like which NTP server is used, the interval at which it is polled as well as many other more general time related things. Then it also creates an instance of the `Timezone` class called `UTC`. UTC's methods allow you to do things that refer to time as specified in UTC. So for instance, `UTC.dateTime()` returns a string representation of the current date and time in UTC.
+#
-If your code says
-```
-Timezone CEST;
-CEST.setPosix("CST-2");
-```
-it will create a timezone with a very simple rule: it is called CST and it is two hours later there than at UTC. Now you can use `CEST.dateTime()`, to get the current time in CEST. But this will not change to and from Daylight Saving Time if your timezone has that. The more powerful way of initialising a timezone is using 'getLocation' like:
-```
-Timezone berlin;
-berlin.setLocation("Europe/Berlin");
-```
+# ezTime User Manual
+> *If it's not in here, it's wrong...*
-#
+## About this manual
-#
+### Semi-internal functions
+
+Some functions are not necessarily useful for everyday users of this library, but might be useful to someone someday. For instance, this library checks with the NTP servers automatically, there should be no need to ever "manually" get an NTP response. But the function to do that is still exposed to the user. Even some functions that have nothing to do with time, like `urlEncode` are there for you to use, simply because they *might* be useful to someone, and the library needed them internally so they come at no extra cost in terms of size. In this manual, the names of these functions are printed in *italics* in their chapter headings, just to make it a easier for you to see which functions are core functionality and which are really not needed in everyday use.
+
+### Specifying time
+
+I hate documentation that still makes me reach for for the source code, so this manual supplies the function prototype with each function so you can see what types or arguments each function takes and what type the return value is. I took one shortcut though. A lot of functions allow you to specify a time. In the function prototype this looks like:
+
+`time_t t = TIME_NOW, ezLocalOrUTC_t local_or_utc = LOCAL`
+
+Throughout this manual, we replace these two optional arguments in the funtion definitions with:
+
+`TIME`
+
+That's because the prior is just a little too long to be repeating a thousand times, and it also makes things look more complicated than they need to be. In most places where you specify a time in ezTime, you are most likely to mean "right now". This can be done by supplying no arguments at all, or `TIME_NOW`. You might make a number of requests in a row, and want to make sure that the time didn't change between them. No need to stick the time value in a variable. After you have made a call specifying no time (meaning `TIME_NOW`), you can specify `LAST_READ` to use the time from the exact moment you made that first call.
+
+Otherwise, you can specify a `time_t` value, a well-known 32-bit signed integer way of specifying time in seconds elapsed since 00:00 Jan 1st 1970. If you specify a value other than `TIME_NOW` or `LAST_READ`, you can then specify whether you mean in UTC or local time, by following it with a second argument that is either `UTC_TIME` or `LOCAL_TIME`.
+
+For example, if you have set up a timezone called Berlin, `Berlin.isDST(1536314299, UTC_TIME)` tells you whether Daylight Savings Time is in effect on that time, as seconds from 00:00 Jan 1st 1970 UTC, as opposed to that many seconds from that time in Berlin (which would be the default). There will be some examples later on, showing you how to create and process such timestamps. Mostly though, you don't need specify anything at all because you just want something time-related about "right now".
+
+> *Time-geek sidenote: ezTime does not have historical information about the daylight davings rules of the past or future, it only applies the rules it has now as if they also applied in the past or future. Check [here](https://www.timeanddate.com/) for historical records for timezones.*
+
-# ezTime: complete documentation
-
-## Index
-
-* ezTime object
- * breakTime
- * clearCache
- * compileTime
- * debug
- * error
- * errorString
- * getBetween
- * makeTime
- * makeUmpteenthTime
- * now
- * queryNTP
- * setInterval
- * setServer
- * timeStatus
- * timezoneAPI
- * updateNow
- * urlEncode
- * waitForSync
- * zeropad
-* Timezone class
- * dateTime
- * day
- * getOffset
- * getPosix
- * getTimezoneName
- * hour
- * isDST
- * isDST_UTC
- * isDST_local
- * minute
- * minuteChanged
- * month
- * ms
- * now
- * second
- * secondChanged
- * setDefault
- * setPosix
- * setTime
- * weekday
- * year
-
-
-## Documentation
-
-### Starting it all
-
-It all starts when you include the library with `#include `. From that point forward there is an object instance called `ezTime` with methods to control the behaviour of ezTime, as well as a timezone object called `UTC`. But nothing happens until you ask the library what time it is. This can be done by using any of the methods that tell time, like the `.dateTime` method of the timezone object. For instance, your code could do
-`Serial.println(UTC.dateTime());` to print a complete textual representation of date and time to the serial port. The library would find out that time had not been synchronized yet, and it would send off an NTP request to one of the NTP servers that `pool.ntp.org` resolves to. If your Arduino has just woken up, it is probably not connected to the WiFi network just yet, and so the call to `.dateTime` would return a String with the date and time just after midnight on the 1st of January 1970: the zero-point for the unix-style time counter used by ezTime.
+## How it all works
+
+### What happens when you include the library
+
+It all starts when you include the library with `#include `. From that point forward there is an object instance called `ezTime` with methods to control the behaviour of ezTime, as well as a timezone object called `UTC`, and a reference to this object called `defaultTZ` (which you may point to a different timezone later).
+
+### No daemons here
+
+It is important to understand what ezTime does NOT do. It does not somehow create a background process that keeps time, contacts servers, or whatever. The Arduino does the timekeeping for us with its `millis()` counter, which keeps the time in milliseconds since the Arduino started. All ezTime does when it synchronizes time is to store a time (in seconds since 1970) and the position of the millis counter when that was. By seeing how much the millis counter has advanced and adding that starting point since 1970, ezTime tells time. But that internal clock isn't perfect, it may - very slowly - drift away from the actual time. So periodically when you ask it something, it will discover that it's time to re-synchronize its own clock with the NTP server, and then it'll go out, do that and take, say, 50 ms longer to respond back to your code. But it all happens only when you ask for it.
+
+### But I only just woke up !
+
+Your code might call `Serial.println(UTC.dateTime());` to print a complete textual representation of date and time in the default format to the serial port. The library would find out that time had not been synchronized yet, and it would send off an NTP request to one of the NTP servers that `pool.ntp.org` resolves to. If your Arduino has just woken up, it probably hasn't gotten its DHCP information, or is not connected to the WiFi network just yet. And so the time lookup would fail and the call to `.dateTime` would return a String with the date and time just after midnight on the 1st of January 1970: the zero-point for the unix-style time counter used by ezTime. It would later correct to the real time, but that's not pretty.
+
+Worse is when you set up a timezone for which you would like to retrieve the daylight savings rules from the server: it can't do that if the connection isn't up yet. So that's why there's a function called `ezTime.waitForSync` that simply requests the time until it is synchronized (or until a set number of seconds passes, see below).
## Setting and synchronising time
-The NTP request from the scenario above failed because the network wasn't up yet, so the clock would still not be synchronized. Subsequent requests will retry the NTP query, but only if they happen at least 3 seconds later. (These 3 seconds are settable with the `NTP_RETRY` define from `ezTime.h`.)
+The NTP request from the scenario above failed because the network wasn't up yet, so the clock would still not be synchronized. Subsequent requests will retry the NTP query, but only if they happen at least 5 seconds later.
### ezTime.timeStatus
`timeStatus_t ezTime.timeStatus();`
-Returns what state the clock is in. `ezTime.timeStatus()` will return either `timeNotSet`, `timeNeedsSync` or `timeSet`.
+Returns what state the clock is in. `ezTime.timeStatus()` will return one of:
-* `timeNotSet` means no NTP update or other setting of the clock (with the `.settime` method) has taken place
-* `timeSet` means the clock should have the current time
-* `timeNeedsSync` means a scheduled NTP request has been due for more than an hour. (The time an update needs to be due before `timeNeedsSync` is set is configured by the `NTP_STALE_AFTER` define in the `ezTime.h` file.)
+| timeStatus | meaning |
+|----|----|
+| `timeNotSet` | No NTP update or manual setting of the clock (by calling the `.setTime` method of a timezone) has taken place |
+| `timeSet` | The clock should have the current time |
+| `timeNeedsSync` | A scheduled NTP request has been due for more than an hour. (The time an update needs to be due before `timeNeedsSync` is set is configured by the `NTP_STALE_AFTER` define in the `ezTime.h` file.) |
### ezTime.waitForSync
`bool ezTime.waitForSync(uint16_t timeout = 0);`
-If your code uses timezones other than UTC, it might want to wait to initialise them until there is a valid time to see if the cached timezone definitions are still current. And if you are displaying a calendar or clock, it might look silly if it first says midnight on January 1st 1970 before showing the real time. `ezTime.waitForSync` will wait for the network to connect, and then for the time to be synchronized before returning `true`. If you specify a timeout (in seconds), it will return after that many seconds even if the clock is not in sync yet, returning `false`.
+If your code uses timezones other than UTC, it might want to wait to initialise them until there is a valid time to see if the cached timezone definitions are still current. And if you are displaying a calendar or clock, it might look silly if it first says midnight on January 1st 1970 before showing the real time. `ezTime.waitForSync` will wait for the network to connect, and then for the time to be synchronized before returning `true`. If you specify a timeout (in seconds), it will return after that many seconds even if the clock is not in sync yet, returning `false`. (ezTime error `TIMEOUT`, see the chapter on error and debug messages further down)
-### ezTime.setServer and ezTime.setInterval
+### *ezTime.setServer and ezTime.setInterval*
`void ezTime.setServer(String ntp_server = NTP_SERVER);`
@@ -323,13 +242,13 @@ If your code uses timezones other than UTC, it might want to wait to initialise
By default, ezTime is set to poll `pool.ntp.org` every 10 minutes. These defaults should work for most people, but you can change them by specifying a new server with `ezTime.setServer` or a new interval (in seconds) with ezTime.setInterval. If you call setInterval with an interval of 0 seconds or call it as `ezTime.setInterval()`, no more NTP queries will be made.
-### ezTime.updateNow
+### *ezTime.updateNow*
`void ezTime.updateNow();`
-Schedules the next update to happen immediately, and then tries to query the NTP server. If that fails, it will keep retrying every 3 seconds.
+Schedules the next update to happen immediately, and then tries to query the NTP server. If that fails, it will keep retrying when you ask for new time information, but not more often than once every 5 seconds. (Defined by `NTP_RETRY` in `ezTime.h`.)
-### ezTime.queryNTP
+### *ezTime.queryNTP*
`bool ezTime.queryNTP(String server, time_t &t, unsigned long &measured_at);`
@@ -339,63 +258,157 @@ If the time server answers, `ezTime.queryNTP` returns `true`. If `false` is retu
Note that this function is used internally by ezTime, but does not by itself set the time ezTime keeps. You will likely never need to call this from your code.
-## Errors and debug information
+## Timezones, caching and timezoneapi.com
-`void ezTime.debug(ezDebugLevel_t level);`
+### On timezones in ezTime
-`ezError_t ezTime.error();`
+TODO, also explain that we refer to timezones as `yourTZ` in rest of this document
+
+### yourTZ.setDefault
+
+`void yourTZ.setDefault()`
+
+`#include ` includes the library, creates `ezTime` object and `UTC` instance of `Timezone` class, as well as `defaultTZ`, which is a reference to UTC unless you set it to another timzone by calling `yourTZ.setDefault()`. The functions in the root namespace like `hour()` and `minute()` - without a timezone in front - are interpreted as if passed to the default timezone. So if you have ezisting code, just setting up a timezone and making it the default should cause that code to work as if the time was set in local time.
+
+### On timezone data in ezTime
+
+Internally, ezTime stores everything it knows about a timezone as two strings. One is the official name of the timezone in "Olsen" format (like `Europe/Berlin`). That name is used to then update, at least once per year, all the other information needed to represent time in that timezone. This is another string, in so-called "posix" format. It's a little longer and for Berlin it is `CET-1CEST,M3.4.0/2,M10.4.0/3`. The elements of this string are as follows:
+
+| Element | meaning |
+| ---- | ---- |
+| `CET` | Name of timezone in standard time (CET = Central European Time in this case.)
+| `-1` | Hours offset from UTC, meaning subtract one hour from this time to get to UTC. (Note offset is often written elsewhere the other way around (so +1 in this case), just to confuse things.) Could also specify minutes, like `-05:30` for India. |
+| `CEST` | Name of timezone in Daylight Saving Time (DST), CEST stands for Central European Summer Time |
+| `,M3` | DST starts in March |
+| `.4` | On the fourth occurence of
+| `.0` | a Sunday |
+| `/2` | at 02:00 local time |
+| `,M10` | DST ends in October |
+| `.4` | on the fourth ocurrence of |
+| `.0` | a Sunday |
+| `/3` | at 03:00 local time |
+
+### yourTZ.setPosix
-`String ezTime.errorString(ezError_t err);`
+`bool yourTZ.setPosix(String posix)`
-## Timezones
+Allows you to directly enter the posix information for a timezone. For simple timezones, you could set things up manually. For example for India, a mere
-`#include ` includes the library, creates `ezTime` object and `UTC` instance of `Timezone` class, as well as `defaultTZ`, which is a reference to UTC unless you set it to another timzone by calling `someTZ.setDefault()`.
+```
+Timezone India
+India.setPosix("IST-5:30")
+Serial.println(India.dateTime());
+```
+
+is enough, because the time in India doesn't go back and forth with the coming and going of Daylight Saving Time (even though the half hour offset to UTC is pretty weird.)
+
+### yourTZ.getPosix
+
+`String yourTZ.getPosix()`
+
+`getPosix` does what you would expect and simply returns the posix string stored in ezTime for a given timezone.
+
+### yourTZ.isDST
+
+`bool yourTZ.isDST(time_t t = TIME_NOW, bool local_time = true);`
-`bool someTZ.setPosix(String posix)`
+Tells you whether DST is in effect at a given time in this timezone. If you do not provide arguments, it's interpreted as 'right now'. You can also specify a time (in seconds since 1970, we'll get back to that) in the first argument. If you want to know for a time in UTC, you can set the second argument to `false`, otherwise you are assumed to mean in local time.
-`String someTZ.getPosix()`
+### yourTZ.getTimezoneName
-`void someTZ.setDefault()`
+`String getTimezoneName(time_t t = TIME_NOW, bool local_time = true);`
-`bool someTZ.isDST_local(time_t t = TIME_NOW)`
+Provides the short code for the timezone, like `IST` for India, or `CET` (during standard time) or `CEST` (during Daylight Saving Time) for most of Europe.
-`bool someTZ.isDST_UTC(time_t t = TIME_NOW)`
+### yourTZ.getOffset
-`bool someTZ.isDST()`
+`int16_t yourTZ.getOffset(time_t t = TIME_NOW, bool local_time = true)`
-`String someTZ.getTimezoneName(time_t t = TIME_NOW)`
+Provide the offset from UTC in minutes at the indicated time (or now if you do not specify anything). The offset here is in the same direction as the posix information, that means -120 would mean 2 hours east of UTC.
-`int32_t someTZ.getOffset(time_t t = TIME_NOW)`
+### *yourTZ.tzTime*
-`bool ezTime.timezoneAPI(String location, String &olsen, String &posix);`
+`time_t yourTZ.tzTime(time_t t = TIME_NOW, bool local_time = true, tzTimeData_t *tztd = NULL);`
-## Getting/setting date and time
+This will tell you the time in seconds since 00:00 on Jan 1st 1970 in a timezone when you give it a timne in UTC, or vice versa.
-`time_t someTZ.now(bool update_last_read = true)`
+TODO: explain better and also explain tztd
-`void someTZ.setTime(time_t t)`
+## Getting date and time
-`void someTZ.setTime(const uint8_t hr, const uint8_t min, const uint8_t sec, const uint8_t day, const uint8_t mnth, uint16_t yr)`
+### yourTZ.dateTime
```
-String someTZ.dateTime(String format = DEFAULT_TIMEFORMAT);
-String someTZ.dateTime(time_t t, String format = DEFAULT_TIMEFORMAT);
+String yourTZ.dateTime(String format = DEFAULT_TIMEFORMAT);
+String yourTZ.dateTime(time_t t, String format = DEFAULT_TIMEFORMAT);
+```
+
+We'll start with one of the most powerful functions of ezTime. With datetime you can represent a date and/or a time in any way you want. You do this in the same way you do in many programming languages: by providing a special formatting string. Many characters in this string have special meanings and will be replaced. What this means is that `utc.dateTime("l, d-M-y H:i:s.v T")` might return `Saturday, 25-Aug-18 14:32:53.282 UTC`. Here's the list of characters and what they are replaced by.
+
+| char | replaced by |
+| ----- | :----- |
+| `d` | Day of the month, 2 digits with leading zeros |
+| `D` | First three letters of day in English, like `Tue` |
+| `j` | Day of the month without leading zeros |
+| `l` | (lowercase L) Day of the week in English, like `Tuesday` |
+| `N` | // ISO-8601 numeric representation of the day of the week. (1 = Monday, 7 = Sunday)
+| `S` | English ordinal suffix for the day of the month, 2 characters (st, nd, rd, th) |
+| `w` | Numeric representation of the day of the week (0 = Sunday) |
+| `F` | A month's name, such as `January` |
+| `m` | Numeric representation of a month, with leading zeros |
+| `M` | Three first letters of a monthin English, like `Apr` |
+| `n` | Numeric representation of a month, without leading zeros |
+| `t` | Number of days in the given month |
+| `Y` | A full numeric representation of the year, 4 digits |
+| `y` | Last two digits of the year |
+| `a` | am or pm |
+| `A` | AM or PM |
+| `g` | 12-hour format of an hour without leading zeros |
+| `G` | 24-hour format of an hour without leading zeros |
+| `h` | 12-hour format of an hour with leading zeros |
+| `H` | 24-hour format of an hour with leading zeros |
+| `i` | Minutes with leading zeros |
+| `s` | Seconds with leading zero |
+| `T` | abbreviation for timezone, like `CEST` |
+| `v` | milliseconds as three digits |
+| `e` | Timezone identifier (Olsen name), like `Europe/Berlin` |
+| `O` | Difference to Greenwich time (GMT) in hours and minutes written together, like `+0200`. Here a positive offset means east of UTC. |
+| `P` | Same as O but with a colon between hours and minutes, like `+02:00` |
+| `Z` | Timezone offset in seconds. West of UTC is negative, east of UTC is positive. |
+| `z` | The day of the year (starting from 0) |
+| `W` | ISO-8601 week number. See right below for explanation link.
+| `X` | ISO-8601 year for year-week notation as four digit year. Warning: Not guaranteed to be same as current year, may be off by one at start or end of year. See [here](https://en.wikipedia.org/wiki/ISO_week_date) |
+
+`time_t yourTZ.now(bool update_last_read = true)`
+
+`void yourTZ.setTime(time_t t, uint16_t ms = 0);`
+
+`void yourTZ.setTime(const uint8_t hr, const uint8_t min, const uint8_t sec, const uint8_t day, const uint8_t mnth, uint16_t yr)`
+
+```
+String yourTZ.dateTime(String format = DEFAULT_TIMEFORMAT);
+String yourTZ.dateTime(time_t t, String format = DEFAULT_TIMEFORMAT);
```
```
-uint8_t someTZ.hour(time_t t = TIME_NOW);
-uint8_t someTZ.minute(time_t t = TIME_NOW);
-uint8_t someTZ.second(time_t t = TIME_NOW);
-uint16_t someTZ.ms(time_t t = TIME_NOW);
-uint8_t someTZ.day(time_t t = TIME_NOW);
-uint8_t someTZ.weekday(time_t t = TIME_NOW);
-uint8_t someTZ.month(time_t t = TIME_NOW);
-uint16_t someTZ.year(time_t t = TIME_NOW);
+uint8_t yourTZ.hour(time_t t = TIME_NOW);
+uint8_t yourTZ.minute(time_t t = TIME_NOW);
+uint8_t yourTZ.second(time_t t = TIME_NOW);
+uint16_t yourTZ.ms(time_t t = TIME_NOW);
+uint8_t yourTZ.day(time_t t = TIME_NOW);
+uint8_t yourTZ.weekday(time_t t = TIME_NOW);
+uint8_t yourTZ.month(time_t t = TIME_NOW);
+uint16_t yourTZ.year(time_t t = TIME_NOW);
```
```
-bool someTZ.secondChanged();
-bool someTZ.minuteChanged();
+uint8_t weekISO(time_t t = TIME_NOW);
+uint16_t yearISO(time_t t = TIME_NOW);
+```
+
+```
+bool yourTZ.secondChanged();
+bool yourTZ.minuteChanged();
```
## Compatibility with Arduino `Time` library
@@ -406,6 +419,37 @@ bool someTZ.minuteChanged();
`void ezTime.breakTime(time_t time, tmElements_t &tm)`
+## Errors and debug information
+
+### ezTime.debugLevel
+
+`void ezTime.debugLevel(ezDebugLevel_t level);`
+
+Sets the level of detail at which ezTime outputs messages on the serial port. Can be set to one of:
+
+| debugLevel | effect |
+|---|---|
+| `NONE` | ezTime does not output anything on the serial port |
+| `ERROR` | ezTime will show when errors occur. Note that these may be transient errros that ezTime recovers from, such as NTP timeouts. |
+| `INFO` | Essentially shows you what ezTime is doing in the background. Includes messages about NTP updates, initialising timezones, etc etc. |
+| `DEBUG` | Detailed debugging information unlikely to be of much use unless you are trying to get to the bottom of certian internal behaviour of ezTime. |
+
+*Note:* you can specify which level of debug information whould be compiled into the library. This is especially significant for AVR Arduino users that need to limit the flashindia and RAM footprint of ezTtime. See the "Running on small Arduinos" chapter further down. TODO
+
+### ezTime.error
+
+`ezError_t ezTime.error();`
+
+A number of functions in ezTime are booleans, meaning they return `true` or `false` as their return value, where `false` means some error ocurred. `ezTime.error` will return an `ezError_t` enumeration, something like `NO_NETWORK` (obvious) or `LOCKED_TO_UTC` (when you try to load some new timezone info to the UTC object). You can test for these specific errors and this document will mention which errors night happen in what functions.
+
+When you call `error()`, it will also reset the error, so you can clear the last error stored.
+
+### ezTime.errorString
+
+`String ezTime.errorString(ezError_t err = LAST_ERROR);`
+
+This will give you a string representation of the error specified. The pseudo-error `LAST_ERROR`, which is the default, will give you the textual representation of the last error. This will not reset the last error stored.
+
## Various functions
`time_t ezTime.makeUmpteenthTime(uint8_t hour, uint8_t minute, uint8_t second, uint8_t umpteenth, uint8_t wday, uint8_t month, int16_t year);`
@@ -418,4 +462,8 @@ bool someTZ.minuteChanged();
`String zeropad(uint32_t number, uint8_t length);`
-`void ezTime.clearCache();`
\ No newline at end of file
+`void ezTime.clearCache();`
+
+## 2036 and 2038
+
+The NTP timestamps used here run until the 7th of February 2036. NTP itself has 128 bits of time precision, I haven't looked into it much. Didn't have to, because just a little later, on the 19th of January 2038, the time_t 32 bit signed integer overflows. This is 20 years from today, in 2018. The Arduino world, if it still exists around then, will have come together around some solution that probably involves 64-bit time like in many operating systems of 2018. If you use this library in your nuclear generating station (**NOOOOO!**), make sure you're not around when these timers wrap around.
\ No newline at end of file
diff --git a/examples/Timezones/Timezones.ino b/examples/Timezones/Timezones.ino
index 5358f92..a48ff62 100644
--- a/examples/Timezones/Timezones.ino
+++ b/examples/Timezones/Timezones.ino
@@ -7,7 +7,7 @@ void setup() {
WiFi.begin("your-ssid", "your-password");
// Uncomment the line below to see what it does behind the scenes
- // ezTime.debug(INFO);
+ // ezTime.debugLevel(INFO);
ezTime.waitForSync();
@@ -19,15 +19,7 @@ void setup() {
// Anything with a slash in it is interpreted as an official timezone name
// https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
myTZ.setLocation("Pacific/Auckland");
- Serial.println("Auckland: " + myTZ.dateTime());
-
- // Anything else is parsed as an address to see if that resolves
- myTZ.setLocation("Paris, Texas");
- Serial.println("Paris, Texas: " + myTZ.dateTime());
-
- // The empty string is resolved to the GeoIP location of your IP-address
- myTZ.setLocation("");
- Serial.println("Your local time: " + myTZ.dateTime());
+ Serial.println("Auckland: " + myTZ.dateTime());
}
diff --git a/examples/milliseconds/milliseconds.ino b/examples/milliseconds/milliseconds.ino
index ae43749..36ceb57 100644
--- a/examples/milliseconds/milliseconds.ino
+++ b/examples/milliseconds/milliseconds.ino
@@ -26,7 +26,7 @@ void setup() {
Serial.println("And ezTime is not making those milliseconds up either.");
Serial.println();
Serial.println(" ... Stick around as we do an NTP request every minute.");
- ezTime.debug(INFO);
+ ezTime.debugLevel(INFO);
}
diff --git a/library.json b/library.json
index 8ece598..1a52776 100644
--- a/library.json
+++ b/library.json
@@ -11,9 +11,9 @@
"type": "git",
"url": "https://github.com/ropg/ezTime"
},
- "version": "0.0.1",
+ "version": "0.6.0",
"framework": "arduino",
- "platforms": "espressif32"
+ "platforms": "*"
"build": {
"libArchive": false
}
diff --git a/library.properties b/library.properties
index fb0784e..e9fa96b 100644
--- a/library.properties
+++ b/library.properties
@@ -1,5 +1,5 @@
name=ezTime
-version=0.0.2
+version=0.6.0
author=Rop Gonggrijp
maintainer=Rop Gonggrijp
sentence=Does NTP, datetime formatted strings, milliseconds and timezones. Drop-in replacement for Arduino Time Library
diff --git a/src/ezTime.cpp b/src/ezTime.cpp
index 270a800..ee5dfe1 100644
--- a/src/ezTime.cpp
+++ b/src/ezTime.cpp
@@ -1,65 +1,75 @@
-
-
#include
#include
#ifdef EZTIME_NETWORK_ENABLE
+ #ifdef EZTIME_CACHE_NVS
+ #include // For timezone lookup cache
+ #endif
+ #ifdef EZTIME_CACHE_EEPROM
+ #include
+ #endif
+ #if defined(ESP8266)
+ #include
+ #include
+ #elif defined(EZTIME_ETHERNET)
+ #include
+ #include
+ #include
+ #else
+ #include
+ #endif
+#endif
-// Caching of namezone data is only available on EZTIME_NVS_ENABLE
-#ifdef EZTIME_NVS_ENABLE
-#include // For timezone lookup cache
-#endif // EZTIME_NVS_ENABLE
-#ifdef ESP8266
-#include
-#include
-#else
-#include
-#endif
+const uint8_t monthDays[]={31,28,31,30,31,30,31,31,30,31,30,31}; // API starts months from 1, this array starts from 0
-#endif // EZTIME_NETWORK_ENABLE
+ezError_t EZtime::_last_error = NO_ERROR;
+ezDebugLevel_t EZtime::_debug_level = NONE;
+time_t EZtime::_last_sync_time = 0;
+time_t EZtime::_last_read_t = 0;
+uint32_t EZtime::_last_sync_millis = 0;
+uint16_t EZtime::_last_read_ms;
+timeStatus_t EZtime::_time_status = timeNotSet;
-const uint8_t monthDays[]={31,28,31,30,31,30,31,31,30,31,30,31}; // API starts months from 1, this array starts from 0
+#ifdef EZTIME_NETWORK_ENABLE
+ bool EZtime::_ntp_enabled = true;
+ uint32_t EZtime::_update_due = 0;
+ uint16_t EZtime::_update_backoff = 0; // seconds to add to _update_due to retry NTP
+ uint16_t EZtime::_update_interval = NTP_INTERVAL;
+ String EZtime::_ntp_server = NTP_SERVER;
+#endif
EZtime::EZtime() {
- ezError_t _last_error = NO_ERROR;
- ezDebugLevel_t _debug_level = NONE;
- _time_status = timeNotSet;
-#ifdef EZTIME_NETWORK_ENABLE
- _ntp_enabled = true;
- _ntp_server = NTP_SERVER;
- _ntp_local_port = NTP_LOCAL_PORT;
- _update_due = 0;
- _update_interval = NTP_INTERVAL;
- _update_backoff = 0;
-#endif // ifdef EZTIME_NETWORK_ENABLE
+
}
////////// Error handing
-String EZtime::errorString(ezError_t err) {
+String EZtime::errorString(ezError_t err /* = LAST_ERROR */) {
switch (err) {
- case NO_ERROR: return "OK";
- case LAST_ERROR: return errorString(_last_error);
- case NO_NETWORK: return "No network";
- case TIMEOUT: return "Timeout";
- case CONNECT_FAILED: return "Connect Failed";
- case DATA_NOT_FOUND: return "Data not found";
- case LOCKED_TO_UTC: return "Locked to UTC";
- default: return "Unkown error";
+ case NO_ERROR: return F("OK");
+ case LAST_ERROR: return errorString(_last_error);
+ case NO_NETWORK: return F("No network");
+ case TIMEOUT: return F("Timeout");
+ case CONNECT_FAILED: return F("Connect Failed");
+ case DATA_NOT_FOUND: return F("Data not found");
+ case LOCKED_TO_UTC: return F("Locked to UTC");
+ case NO_CACHE_SET: return F("No cache set");
+ case CACHE_TOO_SMALL: return F("Cache too small");
+ default: return F("Unkown error");
}
}
String EZtime::debugLevelString(ezDebugLevel_t level) {
switch (level) {
- case NONE: return "NONE";
- case ERROR: return "ERROR";
- case INFO: return "INFO";
- case DEBUG: return "DEBUG";
+ case NONE: return F("NONE");
+ case ERROR: return F("ERROR");
+ case INFO: return F("INFO");
+ case DEBUG: return F("DEBUG");
}
}
@@ -71,55 +81,47 @@ ezError_t EZtime::error() {
void EZtime::error(ezError_t err) {
_last_error = err;
- if (_last_error) debugln(ERROR, "ERROR: " + errorString(err));
-}
-
-void EZtime::debug(ezDebugLevel_t level) {
- _debug_level = level;
- debugln(INFO, "\r\nezTime debug level set to " + debugLevelString(level));
-}
-
-void EZtime::debug(ezDebugLevel_t level, String str) {
- if (_debug_level >= level) {
- Serial.print(str);
+ if (_last_error) {
+ err(F("ERROR: "));
+ errln(errorString(err));
}
}
-void EZtime::debugln(ezDebugLevel_t level, String str) {
- if (_debug_level >= level) {
- Serial.println(str);
- }
+void EZtime::debugLevel(ezDebugLevel_t level) {
+ _debug_level = level;
+ info(F("\r\nezTime debug level set to "));
+ infoln(debugLevelString(level));
}
////////////////////////
String EZtime::monthString(uint8_t month) {
switch(month) {
- case 1: return "January";
- case 2: return "February";
- case 3: return "March";
- case 4: return "April";
- case 5: return "May";
- case 6: return "June";
- case 7: return "July";
- case 8: return "August";
- case 9: return "September";
- case 10: return "October";
- case 11: return "November";
- case 12: return "December";
+ case 1: return F("January");
+ case 2: return F("February");
+ case 3: return F("March");
+ case 4: return F("April");
+ case 5: return F("May");
+ case 6: return F("June");
+ case 7: return F("July");
+ case 8: return F("August");
+ case 9: return F("September");
+ case 10: return F("October");
+ case 11: return F("November");
+ case 12: return F("December");
}
return "";
}
String EZtime::dayString(uint8_t day) {
switch(day) {
- case 1: return "Sunday";
- case 2: return "Monday";
- case 3: return "Tuesday";
- case 4: return "Wednesday";
- case 5: return "Thursday";
- case 6: return "Friday";
- case 7: return "Saturday";
+ case 1: return F("Sunday");
+ case 2: return F("Monday");
+ case 3: return F("Tuesday");
+ case 4: return F("Wednesday");
+ case 5: return F("Thursday");
+ case 6: return F("Friday");
+ case 7: return F("Saturday");
}
return "";
}
@@ -127,57 +129,20 @@ String EZtime::dayString(uint8_t day) {
timeStatus_t EZtime::timeStatus() { return _time_status; }
-time_t EZtime::now() {
- unsigned long m = millis();
+time_t EZtime::now(bool update_last_read /* = true */) {
+ #ifdef EZTIME_NETWORK_ENABLE
+ updateIfNeeded();
+ #endif
time_t t;
-#ifdef EZTIME_NETWORK_ENABLE
- if (_update_interval && (m >= _update_due + (_update_backoff * 1000)) ) {
- if (m - _update_due > NTP_STALE_AFTER * 1000) _time_status = timeNeedsSync; // If unable to sync for an hour, timeStatus = timeNeedsSync
- unsigned long measured_at;
- if (queryNTP(_ntp_server, t, measured_at)) {
- time_t old_time = _last_sync_time + ((m - _last_sync_millis) / 1000); //
- uint16_t old_ms = (m - _last_sync_millis) % 1000; //
-// time_t old_time = _last_sync_time + ( millisElapsed(m) / 1000 );
-// uint16_t old_ms = millisElapsed(m) % 1000;
- _last_sync_time = t;
- _last_sync_millis = measured_at;
- uint16_t new_ms = (m - measured_at) % 1000;
- int32_t correction = (t - old_time) * 1000 + new_ms - old_ms;
- _last_read_ms = new_ms;
- _update_due = m + _update_interval * 1000;
- _update_backoff = 0;
- debug(INFO, "Received time: " + UTC.dateTime(_last_sync_time, "l, d-M-y H:i:s.v T"));
- if (_time_status != timeNotSet) {
- debugln(INFO, " (internal clock was " + ( correction == 0 ? "spot on)" : String(abs(correction)) + " ms " + ( correction > 0 ? "slow)" : "fast)" ) ) );
-// _fudge = 0 - correction; // If we are corrected forward we have to fudge backward again to where we were and then slowly diminish fudgeing in millisElapsed
- } else {
- debugln(INFO, "");
-// _fudge = 0;
- }
- _time_status = timeSet;
- } else {
- _update_backoff += NTP_RETRY;
- }
+ uint32_t m = millis();
+ t = _last_sync_time + ((m - _last_sync_millis) / 1000);
+ if (update_last_read) {
+ _last_read_t = t;
+ _last_read_ms = (m - _last_sync_millis) % 1000;
}
-#endif // EZTIME_NETWORK_ENABLE
- t = _last_sync_time + ((m - _last_sync_millis) / 1000); //
- _last_read_ms = (m - _last_sync_millis) % 1000; //
-// int32_t elapsed = millisElapsed(m);
-// t = _last_sync_time + (elapsed / 1000);
-// _last_read_ms = elapsed % 1000;
- if (m < ezTime._last_sync_millis) t += 0xFFFFFFFF / 1000; // millis() rolled over, we're assuming just once :)
return t;
}
-int32_t EZtime::millisElapsed(unsigned long m) { // can return negative value just after NTP update if fudging
- unsigned long elapsed = m - _last_sync_millis;
- int32_t fudge_remaining = abs(_fudge) - (elapsed / 100); // one ms every 100 ms, i.e. 1% clock speed change
- if (fudge_remaining < 0) return elapsed;
- if (_fudge < 0) fudge_remaining = 0 - fudge_remaining;
- elapsed += fudge_remaining;
- return elapsed;
-}
-
void EZtime::breakTime(time_t timeInput, tmElements_t &tm){
// break the given time_t into time components
// this is a more compact version of the C library localtime function
@@ -231,14 +196,18 @@ void EZtime::breakTime(time_t timeInput, tmElements_t &tm){
tm.Day = time + 1; // day of month
}
-time_t EZtime::makeTime(uint8_t hour, uint8_t minute, uint8_t second, uint8_t day, uint8_t month, int16_t year) {
+time_t EZtime::makeTime(uint8_t hour, uint8_t minute, uint8_t second, uint8_t day, uint8_t month, uint16_t year) {
tmElements_t tm;
tm.Hour = hour;
tm.Minute = minute;
tm.Second = second;
tm.Day = day;
tm.Month = month;
- tm.Year = year - 1970;
+ if (year > 68) { // time_t cannot reach beyond 68 + 1970 anyway, so if bigger user means actual years
+ tm.Year = year - 1970;
+ } else {
+ tm.Year = year;
+ }
return makeTime(tm);
}
@@ -279,7 +248,8 @@ time_t EZtime::makeTime(tmElements_t &tm){
// makeUmpteenthTime allows you to resolve "second thursday in September in 2018" into a number of seconds since 1970
// (Very useful for the timezone calculations that ezTime does internally)
// If umpteenth is 0 or 5 it is taken to mean "the last $wday in $month"
-time_t EZtime::makeUmpteenthTime(uint8_t hour, uint8_t minute, uint8_t second, uint8_t umpteenth, uint8_t wday, uint8_t month, int16_t year) {
+time_t EZtime::makeUmpteenthTime(uint8_t hour, uint8_t minute, uint8_t second, uint8_t umpteenth, uint8_t wday, uint8_t month, uint16_t year) {
+ if (year <= 68 ) year = 1970 + year; // fix user intent
if (umpteenth == 5) umpteenth = 0;
uint8_t m = month;
uint8_t w = umpteenth;
@@ -292,231 +262,12 @@ time_t EZtime::makeUmpteenthTime(uint8_t hour, uint8_t minute, uint8_t second, u
}
time_t t = makeTime(hour, minute, second, 1, m, year);
// add offset from the first of the month to weekday, and offset for the given week
- t += ( (wday - weekday(t) + 7) % 7 + (w - 1) * 7 ) * SECS_PER_DAY;
+ t += ( (wday - UTC.weekday(t) + 7) % 7 + (w - 1) * 7 ) * SECS_PER_DAY;
// back up a week if this is a "Last" rule
if (umpteenth == 0) t -= 7 * SECS_PER_DAY;
return t;
}
-tzData_t EZtime::parsePosix(String posix, int16_t year) {
-
- tzData_t r;
-
- r.dst_start_in_local = 0;
- r.dst_end_in_local = 0;
- r.dst_start_in_utc = 0;
- r.dst_end_in_utc = 0;
-
- int8_t offset_hr = 0;
- uint8_t offset_min = 0;
- int8_t dst_shift_hr = 1;
- uint8_t dst_shift_min = 0;
-
- uint8_t start_month = 0, start_week = 0, start_dow = 0, start_time_hr = 2, start_time_min = 0;
- uint8_t end_month = 0, end_week = 0, end_dow = 0, end_time_hr = 2, end_time_min = 0;
-
- enum posix_state_e {STD_NAME, OFFSET_HR, OFFSET_MIN, DST_NAME, DST_SHIFT_HR, DST_SHIFT_MIN, START_MONTH, START_WEEK, START_DOW, START_TIME_HR, START_TIME_MIN, END_MONTH, END_WEEK, END_DOW, END_TIME_HR, END_TIME_MIN};
- posix_state_e state = STD_NAME;
-
- uint8_t offset = 0;
- String cur_str = "";
- bool ignore_nums = false;
-
- for (uint8_t offset = 0; offset < posix.length(); offset++) {
- String newchar = posix.substring(offset, offset + 1);
-
- // Do not replace the code below with switch statement: evaluation of state that
- // changes while this runs. (Only works because this state can only go forward.)
-
- if (state == STD_NAME) {
- if (newchar == "<") ignore_nums = true;
- if (newchar == ">") ignore_nums = false;
- if (!ignore_nums && (isDigit((int)newchar.c_str()[0]) || newchar == "-" || newchar == "+")) {
- state = OFFSET_HR;
- cur_str = "";
- } else {
- cur_str += newchar;
- r.std_tzname = cur_str;
- }
- }
- if (state == OFFSET_HR) {
- if (newchar == "+") {
- newchar = "";
- } else if (newchar == ":") {
- state = OFFSET_MIN;
- cur_str = "";
- newchar = ""; // ignore the ":"
- } else if (newchar != "-" && !isDigit((int)newchar.c_str()[0])) {
- state = DST_NAME;
- cur_str = "";
- } else {
- cur_str += newchar;
- offset_hr = cur_str.toInt();
- }
- }
- if (state == OFFSET_MIN) {
- if (newchar != "" && !isDigit((int)newchar.c_str()[0])) {
- state = DST_NAME;
- ignore_nums = false;
- cur_str = "";
- } else {
- cur_str += newchar;
- offset_min = cur_str.toInt();
- }
- }
- if (state == DST_NAME) {
- if (newchar == "<") ignore_nums = true;
- if (newchar == ">") ignore_nums = false;
- if (newchar == ",") {
- state = START_MONTH;
- cur_str = "";
- newchar = ""; // ignore the ","
- } else if (!ignore_nums && (newchar == "-" || isDigit((int)newchar.c_str()[0]))) {
- state = DST_SHIFT_HR;
- cur_str = "";
- } else {
- cur_str += newchar;
- r.dst_tzname = cur_str;
- }
- }
- if (state == DST_SHIFT_HR) {
- if (newchar == ":") {
- state = DST_SHIFT_MIN;
- cur_str = "";
- newchar = ""; // ignore the ":"
- } else if (newchar == ",") {
- state = START_MONTH;
- cur_str = "";
- newchar="";
- } else {
- cur_str += newchar;
- dst_shift_hr = cur_str.toInt();
- }
- }
- if (state == DST_SHIFT_MIN) {
- if (newchar == ",") {
- state = START_MONTH;
- cur_str = "";
- newchar="";
- } else {
- cur_str += newchar;
- dst_shift_min = cur_str.toInt();
- }
- }
- if (state == START_MONTH) {
- if (newchar == ".") {
- state = START_WEEK;
- cur_str = "";
- newchar = "";
- } else if (newchar != "M") {
- cur_str += newchar;
- start_month = cur_str.toInt();
- }
- }
- if (state == START_WEEK) {
- if (newchar == ".") {
- state = START_DOW;
- cur_str = "";
- newchar = "";
- } else {
- cur_str += newchar;
- start_week = cur_str.toInt();
- }
- }
- if (state == START_DOW) {
- if (newchar == "/") {
- state = START_TIME_HR;
- cur_str = "";
- newchar = "";
- } else if (newchar == ",") {
- state = END_MONTH;
- cur_str = "";
- newchar = "";
- } else {
- cur_str += newchar;
- start_dow = cur_str.toInt();
- }
- }
- if (state == START_TIME_HR) {
- if (newchar == ":") {
- state = START_TIME_MIN;
- cur_str = "";
- newchar = ""; // ignore the ":"
- } else if (newchar == ",") {
- state = END_MONTH;
- cur_str = "";
- newchar = ""; // ignore the ":"
- } else {
- cur_str += newchar;
- start_time_hr = cur_str.toInt();
- }
- }
- if (state == START_TIME_MIN) {
- if (newchar == ",") {
- state = END_MONTH;
- cur_str = "";
- newchar = "";
- } else {
- cur_str += newchar;
- start_time_min = cur_str.toInt();
- }
- }
- if (state == END_MONTH) {
- if (newchar == ".") {
- state = END_WEEK;
- cur_str = "";
- newchar = "";
- } else if (newchar != "M") {
- cur_str += newchar;
- end_month = cur_str.toInt();
- }
- }
- if (state == END_WEEK) {
- if (newchar == ".") {
- state = END_DOW;
- cur_str = "";
- newchar = "";
- } else {
- cur_str += newchar;
- end_week = cur_str.toInt();
- }
- }
- if (state == END_DOW) {
- if (newchar == "/") {
- state = END_TIME_HR;
- cur_str = "";
- newchar = "";
- } else {
- cur_str += newchar;
- end_dow = cur_str.toInt();
- }
- }
- if (state == END_TIME_HR) {
- if (newchar == ":") {
- state = END_TIME_MIN;
- cur_str = "";
- newchar = ""; // ignore the ":"
- } else {
- cur_str += newchar;
- end_time_hr = cur_str.toInt();
- }
- }
- if (state == END_TIME_MIN) {
- cur_str += newchar;
- end_time_min = cur_str.toInt();
- }
- }
- r.std_offset = (offset_hr < 0) ? offset_hr * 3600 - offset_min * 60 : offset_hr * 3600 + offset_min * 60;
- r.dst_offset = r.std_offset - dst_shift_hr * 3600 - dst_shift_min * 60;
- if (start_month) {
- r.dst_start_in_local = ezTime.makeUmpteenthTime(start_time_hr, start_time_min, 0, start_week, start_dow, start_month, year);
- r.dst_end_in_local = ezTime.makeUmpteenthTime(end_time_hr, end_time_min, 0, end_week, end_dow, end_month, year);
- r.dst_start_in_utc = r.dst_start_in_local - r.std_offset;
- r.dst_end_in_utc = r.dst_end_in_local - r.dst_offset;
- }
- return r;
-}
-
String EZtime::urlEncode(String str) {
String encodedString="";
char c;
@@ -548,21 +299,13 @@ String EZtime::urlEncode(String str) {
}
String EZtime::zeropad(uint32_t number, uint8_t length) {
- String out = String(number);
+ String out;
+ out.reserve(length);
+ out = String(number);
while (out.length() < length) out = "0" + out;
return out;
}
-String EZtime::getBetween(String &haystack, String before_needle, String after_needle /* = "" */) {
- int16_t start = haystack.indexOf(before_needle);
- if (start == -1) return "";
- start += before_needle.length();
- if (after_needle == "") return haystack.substring(start);
- int16_t end = haystack.indexOf(after_needle, start);
- if (end == -1) return "";
- return haystack.substring(start, end);
-}
-
time_t EZtime::compileTime(String compile_date /* = __DATE__ */, String compile_time /* = __TIME__ */) {
uint8_t hrs = compile_time.substring(0,2).toInt();
@@ -580,133 +323,153 @@ time_t EZtime::compileTime(String compile_date /* = __DATE__ */, String compile_
return 0;
}
-
-#ifdef EZTIME_NETWORK_ENABLE
-
-// This is a nice self-contained NTP routine if you need one: feel free to use it.
-// It gives you the seconds since 1970 (unix epoch) and the millis() on your system when
-// that happened (by deducting fractional seconds and estimated network latency).
-bool EZtime::queryNTP(String server, time_t &t, unsigned long &measured_at) {
- debug(INFO, "Querying " + server + " ... ");
-
- if (!WiFi.isConnected()) { error(NO_NETWORK); return false; }
-
- WiFiUDP udp;
- udp.begin(_ntp_local_port);
-
- // Send NTP packet
- byte buffer[NTP_PACKET_SIZE];
- memset(buffer, 0, NTP_PACKET_SIZE);
- buffer[0] = 0b11100011; // LI, Version, Mode
- buffer[1] = 0; // Stratum, or type of clock
- buffer[2] = 6; // Polling Interval
- buffer[3] = 0xEC; // Peer Clock Precision
- // 8 bytes of zero for Root Delay & Root Dispersion
- buffer[12] = 'X'; // "kiss code", see RFC5905
- buffer[13] = 'E'; // (codes starting with 'X' are not interpreted)
- buffer[14] = 'Z';
- buffer[15] = 'T';
- udp.beginPacket(_ntp_server.c_str(), 123); //NTP requests are to port 123
- udp.write(buffer, NTP_PACKET_SIZE);
- udp.endPacket();
-
- // Wait for packet or return false with timed out
- unsigned long started = millis();
- while (!udp.parsePacket()) {
- delay (1);
- if (millis() - started > NTP_TIMEOUT) { error(TIMEOUT); return false; }
- }
-
- // Set the t and measured_at variables that were passed by reference
- unsigned long done = millis();
- debugln(INFO, "success (round trip " + String(done - started) + " ms)");
- udp.read(buffer, NTP_PACKET_SIZE);
- unsigned long secsSince1900 = buffer[40] << 24 | buffer[41] << 16 | buffer[42] << 8 | buffer[43];
- t = secsSince1900 - 2208988800UL; // Subtract 70 years to get seconds since 1970
- unsigned long fraction = buffer[44] << 24 | buffer[45] << 16 | buffer[46] << 8 | buffer[47];
- uint16_t ms = fraction / 4294967UL; // Turn 32 bit fraction into ms by dividing by 2^32 / 1000
- measured_at = done - ((done - started) / 2) - ms; // Assume symmetric network latency and return when we think the whole second was.
- return true;
+bool EZtime::secondChanged() {
+ time_t t = now(false);
+ if (_last_read_t != t) return true;
+ return false;
}
-void EZtime::setInterval(uint16_t seconds /* = 0 */) { _update_interval = seconds; }
-
-void EZtime::setServer(String ntp_server /* = NTP_SERVER */) { _ntp_server = ntp_server; }
-
-void EZtime::updateNow() { _update_due = millis(); now(); }
+bool EZtime::minuteChanged() {
+ time_t t = now(false);
+ if (_last_read_t / 60 != t / 60) return true;
+ return false;
+}
-bool EZtime::waitForSync(uint16_t timeout /* = 0 */) {
- unsigned long start = millis();
+#ifdef EZTIME_NETWORK_ENABLE
- if (!WiFi.isConnected()) {
- debug(INFO, "Waiting for WiFi ... ");
- while (!WiFi.isConnected()) {
- if ( timeout && (millis() - start) / 1000 > timeout ) { error(TIMEOUT); return false;};
- delay(25);
+ void EZtime::updateIfNeeded() {
+ if (_update_interval && (millis() >= _update_due + (_update_backoff * 1000)) ) {
+ if (millis() - _update_due > NTP_STALE_AFTER * 1000) _time_status = timeNeedsSync; // If unable to sync for an hour, timeStatus = timeNeedsSync
+ time_t t;
+ unsigned long measured_at;
+ if (queryNTP(_ntp_server, t, measured_at)) {
+ int32_t correction = ( (t - _last_sync_time) * 1000 ) - ( measured_at - _last_sync_millis );
+ _last_sync_time = t;
+ _last_sync_millis = measured_at;
+ _last_read_ms = ( millis() - measured_at) % 1000;
+ info(F("Received time: "));
+ info(UTC.dateTime(_last_sync_time, F("l, d-M-y H:i:s.v T")));
+ if (_time_status != timeNotSet) {
+ info(F(" (internal clock was "));
+ if (!correction) {
+ infoln(F("spot on)"));
+ } else {
+ info(String(abs(correction)));
+ if (correction > 0) {
+ infoln(F(" ms fast)"));
+ } else {
+ infoln(F(" ms slow)"));
+ }
+ }
+ } else {
+ infoln("");
+ }
+ _update_due = millis() + (uint32_t)_update_interval * 1000;
+ _update_backoff = 0;
+ _time_status = timeSet;
+ } else {
+ _update_backoff += NTP_RETRY;
+ }
}
- debugln(INFO, "connected");
}
- if (!_time_status != timeSet) {
- debugln(INFO, "Waiting for time sync");
- while (_time_status != timeSet) {
- if ( timeout && (millis() - start) / 1000 > timeout ) { error(TIMEOUT); return false;};
- delay(250);
- now();
- }
- debugln(INFO, "Time is in sync");
- }
+ // This is a nice self-contained NTP routine if you need one: feel free to use it.
+ // It gives you the seconds since 1970 (unix epoch) and the millis() on your system when
+ // that happened (by deducting fractional seconds and estimated network latency).
+ bool EZtime::queryNTP(String server, time_t &t, unsigned long &measured_at) {
+ info(F("Querying "));
+ info(server);
+ info(F(" ... "));
+
+ #ifndef EZTIME_ETHERNET
+ if (!WiFi.isConnected()) { error(NO_NETWORK); return false; }
+ WiFiUDP udp;
+ #else
+ EthernetUDP udp;
+ #endif
-}
+ udp.flush();
+ udp.begin(NTP_LOCAL_TIME_PORT);
+
+ // Send NTP packet
+ byte buffer[NTP_PACKET_SIZE];
+ memset(buffer, 0, NTP_PACKET_SIZE);
+ buffer[0] = 0b11100011; // LI, Version, Mode
+ buffer[1] = 0; // Stratum, or type of clock
+ buffer[2] = 9; // Polling Interval (9 = 2^9 secs = ~9 mins, close to our 10 min default)
+ buffer[3] = 0xEC; // Peer Clock Precision
+ // 8 bytes of zero for Root Delay & Root Dispersion
+ buffer[12] = 'X'; // "kiss code", see RFC5905
+ buffer[13] = 'E'; // (codes starting with 'X' are not interpreted)
+ buffer[14] = 'Z';
+ buffer[15] = 'T';
+ udp.beginPacket(_ntp_server.c_str(), 123); //NTP requests are to port 123
+ udp.write(buffer, NTP_PACKET_SIZE);
+ udp.endPacket();
+
+ // Wait for packet or return false with timed out
+ unsigned long started = millis();
+ uint16_t packetsize = 0;
+ while (!udp.parsePacket()) {
+ delay (1);
+ if (millis() - started > NTP_TIMEOUT) {
+ udp.stop();
+ error(TIMEOUT);
+ return false;
+ }
+ }
+ udp.read(buffer, NTP_PACKET_SIZE);
+ uint32_t highWord, lowWord;
+ highWord = ( buffer[40] << 8 | buffer[41] ) & 0x0000FFFF; // Must be done in two steps on AVR
+ lowWord = ( buffer[42] << 8 | buffer[43] ) & 0x0000FFFF;
+ uint32_t secsSince1900 = highWord << 16 | lowWord;
+ // Set the t and measured_at variables that were passed by reference
+ uint32_t done = millis();
+ info(F("success (round trip ")); info(done - started); infoln(F(" ms)"));
+ t = secsSince1900 - 2208988800UL; // Subtract 70 years to get seconds since 1970
+ highWord = ( buffer[44] << 8 | buffer[45] ) & 0x0000FFFF;
+ lowWord = ( buffer[46] << 8 | buffer[47] ) & 0x0000FFFF;
+ uint32_t fraction = highWord << 16 | lowWord; // Must be done via two words on AVR
+ uint16_t ms = fraction / 4294967UL; // Turn 32 bit fraction into ms by dividing by 2^32 / 1000
+ measured_at = done - ((done - started) / 2) - ms; // Assume symmetric network latency and return when we think the whole second was.
+ udp.stop(); // On AVR there's only very limited sockets, we want to free them when done.
+ return true;
+ }
-#ifdef EZTIME_NVS_ENABLE
-void EZtime::clearCache() {
- Preferences preferences;
- preferences.begin("ezTime", false); // read-write
- preferences.clear();
- preferences.end();
-}
-#endif // EZTIME_NVS_ENABLE
+ void EZtime::setInterval(uint16_t seconds /* = 0 */) { _update_interval = seconds; }
-bool EZtime::timezoneAPI(String location, String &olsen, String &posix) {
+ void EZtime::setServer(String ntp_server /* = NTP_SERVER */) { _ntp_server = ntp_server; }
- if (!WiFi.isConnected()) { error(NO_NETWORK); return false; }
+ void EZtime::updateNow() { _update_due = millis(); now(); }
- String path;
- if (location.indexOf("/") != -1) {
- path = "/api/timezone/?" + ezTime.urlEncode(location);
- } else if (location != "") {
- path = "/api/address/?" + ezTime.urlEncode(location);
- } else {
- path = "/api/ip";
- }
+ bool EZtime::waitForSync(uint16_t timeout /* = 0 */) {
+
+ unsigned long start = millis();
- WiFiClient client;
- if (!client.connect("timezoneapi.io", 80)) { error(CONNECT_FAILED); return false; }
-
- client.println("GET " + path + " HTTP/1.1");
- client.println("Host: timezoneapi.io");
- client.println("Connection: close");
- client.println();
- client.setTimeout(3000);
- String reply = client.readString();
- debugln(DEBUG, "Sent request for http://timezoneapi.io" + path);
- debugln(DEBUG, "Reply from server in full:\r\n\r\n" + reply + "\r\n\r\n");
+ #ifndef EZTIME_ETHERNET
+ if (!WiFi.isConnected()) {
+ info(F("Waiting for WiFi ... "));
+ while (!WiFi.isConnected()) {
+ if ( timeout && (millis() - start) / 1000 > timeout ) { error(TIMEOUT); return false;};
+ delay(25);
+ }
+ infoln(F("connected"));
+ }
+ #endif
+
+ if (!_time_status != timeSet) {
+ infoln(F("Waiting for time sync"));
+ while (_time_status != timeSet) {
+ if ( timeout && (millis() - start) / 1000 > timeout ) { error(TIMEOUT); return false;};
+ delay(250);
+ now();
+ }
+ infoln(F("Time is in sync"));
+ }
- // The below should not be mistaken for Json parsing...
- posix = getBetween(reply, "\"tz_string\":\"", "\"");
- posix.replace("\\/", "/");
- olsen = getBetween(reply, "\"id\":\"", "\"");
- olsen.replace("\\/", "/");
- if (olsen != "" && posix != "") {
- return true;
- } else {
- error(DATA_NOT_FOUND);
- return false;
}
-}
-
+
#endif // EZTIME_NETWORK_ENABLE
@@ -723,218 +486,575 @@ EZtime ezTime;
Timezone::Timezone(bool locked_to_UTC /* = false */) {
_locked_to_UTC = locked_to_UTC;
- _tzdata.std_tzname = "UTC";
- _tzdata.std_offset = 0;
+ _posix = "UTC";
+ #ifdef EZTIME_CACHE_EEPROM
+ _cache_month = 0;
+ _eeprom_address = -1;
+ #endif
+ #ifdef EZTIME_CACHE_NVS
+ _cache_month = 0;
+ _nvs_name = "";
+ _nvs_key = "";
+ #endif
}
bool Timezone::setPosix(String posix) {
if (_locked_to_UTC) { ezTime.error(LOCKED_TO_UTC); return false; }
- _tzdata = ezTime.parsePosix(posix, UTC.year());
- _olsen = ""; // Might be manually set, so delete _olsen as to not suggest a link
+ _posix = posix;
}
-String Timezone::getPosix() { return _posix;}
+time_t Timezone::tzTime(time_t t /* = TIME_NOW */, ezLocalOrUTC_t local_or_utc /* = LOCAL_TIME */, tzTimeData_t *tztd /* = NULL */) {
+ if (t == TIME_NOW) {
+ t = ezTime.now();
+ local_or_utc = UTC_TIME;
+ } else if (t == LAST_READ) {
+ t = ezTime._last_read_t;
+ local_or_utc = UTC_TIME;
+ }
+
+ int8_t offset_hr = 0;
+ uint8_t offset_min = 0;
+ int8_t dst_shift_hr = 1;
+ uint8_t dst_shift_min = 0;
+
+ uint8_t start_month = 0, start_week = 0, start_dow = 0, start_time_hr = 2, start_time_min = 0;
+ uint8_t end_month = 0, end_week = 0, end_dow = 0, end_time_hr = 2, end_time_min = 0;
+
+ enum posix_state_e {STD_NAME, OFFSET_HR, OFFSET_MIN, DST_NAME, DST_SHIFT_HR, DST_SHIFT_MIN, START_MONTH, START_WEEK, START_DOW, START_TIME_HR, START_TIME_MIN, END_MONTH, END_WEEK, END_DOW, END_TIME_HR, END_TIME_MIN};
+ posix_state_e state = STD_NAME;
-#ifdef EZTIME_NETWORK_ENABLE
+ bool ignore_nums = false;
+ char c = 1; // Dummy value to get while(newchar) started
+ uint8_t strpos = 0;
+ char (*std_tzname)[8];
+ char (*dst_tzname)[8];
+ if (tztd) {
+ tztd->stdname_end = _posix.length() - 1;
+ tztd->dstname_begin = _posix.length();
+ tztd->dstname_end = _posix.length();
+ }
+ while (strpos < _posix.length()) {
+ c = (char)_posix[strpos];
-bool Timezone::setLocation(String location, bool force_lookup /* = false */) {
- ezTime.debugln(INFO, "Timezone lookup for: " + location);
- if (_locked_to_UTC) { ezTime.error(LOCKED_TO_UTC); return false; }
-#ifdef EZTIME_NVS_ENABLE
- // Caching only on EZTIME_NVS_ENABLE
-
- Preferences preferences;
-
- location.replace(":", "_"); // ":" is our record separator, cannot be in location
-
- String cache, cache_location;
- String hit_olsen = "";
- String hit_posix = "";
- int16_t x, hit_year;
- uint8_t first_free_entry = 0;
- uint8_t cache_number = 0;
- preferences.begin("ezTime", true); // read-only
- for (uint8_t n = 1; n < 100; n++) {
- char key[] = "lookupcache-xx";
- key[12] = '0' + (n / 10);
- key[13] = '0' + (n % 10);
- cache = preferences.getString(key);
- if (cache == "") {
- if(!first_free_entry) first_free_entry = n;
- } else {
- x = cache.indexOf(":"); cache_location = cache.substring(0, x); cache = cache.substring(x + 1);
- if (cache_location == location) {
- cache_number = n;
- x = cache.indexOf(":"); hit_olsen = cache.substring(0, x); cache = cache.substring(x + 1);
- x = cache.indexOf(":"); hit_year = cache.substring(0, x).toInt();
- hit_posix = cache.substring(x + 1);
- break;
+ // Do not replace the code below with switch statement: evaluation of state that
+ // changes while this runs. (Only works because this state can only go forward.)
+
+ if (c && state == STD_NAME) {
+ if (c == '<') ignore_nums = true;
+ if (c == '>') ignore_nums = false;
+ if (!ignore_nums && (isDigit(c) || c == '-' || c == '+')) {
+ state = OFFSET_HR;
+ if (tztd) tztd->stdname_end = strpos - 1;
}
}
- }
- preferences.end();
- if (!cache_number) {
- if (first_free_entry) {
- cache_number = first_free_entry;
- } else {
- // Instead of writing whole logic for expiring cache, just pick a random location.
- // If there's only a few used entries, odds of it overwriting something that's still
- // used are low-ish, and re-fetching is not that expensive.
- ezTime.debugln(INFO, "Cache full, next write will be at random location.");
- cache_number = ( esp_random() % 100 ) + 1;
+ if (c && state == OFFSET_HR) {
+ if (c == '+') {
+ // Ignore the plus
+ } else if (c == ':') {
+ state = OFFSET_MIN;
+ c = 0;
+ } else if (c != '-' && !isDigit(c)) {
+ state = DST_NAME;
+ if (tztd) tztd->dstname_begin = strpos;
+ } else {
+ if (!offset_hr) offset_hr = atoi(_posix.c_str() + strpos);
+ }
+ }
+ if (c && state == OFFSET_MIN) {
+ if (!isDigit(c)) {
+ state = DST_NAME;
+ ignore_nums = false;
+ } else {
+ if (!offset_min) offset_min = atoi(_posix.c_str() + strpos);
+ }
+ }
+ if (c && state == DST_NAME) {
+ if (c == '<') ignore_nums = true;
+ if (c == '>') ignore_nums = false;
+ if (c == ',') {
+ state = START_MONTH;
+ c = 0;
+ if (tztd) tztd->dstname_end = strpos - 1;
+ } else if (!ignore_nums && (c == '-' || isDigit(c))) {
+ state = DST_SHIFT_HR;
+ if (tztd) tztd->dstname_end = strpos - 1;
+ }
+ }
+ if (c && state == DST_SHIFT_HR) {
+ if (c == ':') {
+ state = DST_SHIFT_MIN;
+ c = 0;
+ } else if (c == ',') {
+ state = START_MONTH;
+ c = 0;
+ } else if (dst_shift_hr == 1) dst_shift_hr = atoi(_posix.c_str() + strpos);
+ }
+ if (c && state == DST_SHIFT_MIN) {
+ if (c == ',') {
+ state = START_MONTH;
+ c = 0;
+ } else if (!dst_shift_min) dst_shift_min = atoi(_posix.c_str() + strpos);
+ }
+ if (c && state == START_MONTH) {
+ if (c == '.') {
+ state = START_WEEK;
+ c = 0;
+ } else if (c != 'M' && !start_month) start_month = atoi(_posix.c_str() + strpos);
+ }
+ if (c && state == START_WEEK) {
+ if (c == '.') {
+ state = START_DOW;
+ c = 0;
+ } else start_week = c - '0';
+ }
+ if (c && state == START_DOW) {
+ if (c == '/') {
+ state = START_TIME_HR;
+ c = 0;
+ } else if (c == ',') {
+ state = END_MONTH;
+ c = 0;
+ } else start_dow = c - '0';
+ }
+ if (c && state == START_TIME_HR) {
+ if (c == ':') {
+ state = START_TIME_MIN;
+ c = 0;
+ } else if (c == ',') {
+ state = END_MONTH;
+ c = 0;
+ } else if (start_time_hr == 2) start_time_hr = atoi(_posix.c_str() + strpos);
+ }
+ if (c && state == START_TIME_MIN) {
+ if (c == ',') {
+ state = END_MONTH;
+ c = 0;
+ } else if (!start_time_min) start_time_min = atoi(_posix.c_str() + strpos);
+ }
+ if (c && state == END_MONTH) {
+ if (c == '.') {
+ state = END_WEEK;
+ c = 0;
+ } else if (c != 'M') if (!end_month) end_month = atoi(_posix.c_str() + strpos);
+ }
+ if (c && state == END_WEEK) {
+ if (c == '.') {
+ state = END_DOW;
+ c = 0;
+ } else end_week = c - '0';
+ }
+ if (c && state == END_DOW) {
+ if (c == '/') {
+ state = END_TIME_HR;
+ c = 0;
+ } else end_dow = c - '0';
+ }
+ if (c && state == END_TIME_HR) {
+ if (c == ':') {
+ state = END_TIME_MIN;
+ c = 0;
+ } else if (end_time_hr == 2) end_time_hr = atoi(_posix.c_str() + strpos);
+ }
+ if (c && state == END_TIME_MIN) {
+ if (!end_time_min) end_time_min = atoi(_posix.c_str() + strpos);
+ }
+ strpos++;
+ }
+
+ int16_t std_offset = (offset_hr < 0) ? offset_hr * 60 - offset_min : offset_hr * 60 + offset_min;
+
+ if (_posix.substring(0,3) == "UTC" && std_offset) _posix.replace("UTC", "???");
+
+ if (!start_month) {
+ if (tztd) {
+ tztd->is_dst = false;
+ tztd->offset = std_offset;
}
+ return t - std_offset * 60;
}
- // Return cache hit
- if ( !force_lookup && hit_posix != "" && ( hit_year == year())) {
- ezTime.debugln(INFO, "Cache hit: " + hit_olsen + " (" + hit_posix + ") from " + String(hit_year));
- _posix = hit_posix;
- setPosix(hit_posix);
- _olsen = hit_olsen; // Has to happen after setPosix because that sets _olsen to "";
- return true;
- }
-#endif // ESP32
- ezTime.error(); // Resets last error to OK
- ezTime.debug(INFO, "timezoneapi.io lookup ... ");
- String olsen, posix;
- if (ezTime.timezoneAPI(location, olsen, posix)) {
- ezTime.debugln(INFO, "success");
- ezTime.debugln(INFO, " Olsen: " + olsen);
- ezTime.debugln(INFO, " Posix: " + posix);
- _posix = posix;
- setPosix(posix);
- _olsen = olsen; // Has to happen after setPosix because that sets _olsen to "";
-
-#ifdef EZTIME_NVS_ENABLE
- ezTime.debugln(INFO, "Storing to cache(" + String(cache_number) + "): " + location + ":" + olsen + ":" + String(year()) + ":" + posix);
- char key[] = "lookupcache-xx";
- key[12] = '0' + (cache_number / 10);
- key[13] = '0' + (cache_number % 10);
- preferences.begin("ezTime", false); // read-write
- preferences.putString(key, location + ":" + olsen + ":" + String(year()) + ":" + posix);
- preferences.end();
- return true;
- } else {
- if (!force_lookup && hit_posix != "") {
- ezTime.debugln(INFO, "Using cache hit: " + hit_olsen + " (" + hit_posix + ") from " + String(hit_year));
- _posix = hit_posix;
- setPosix(hit_posix);
- _olsen = hit_olsen; // Has to happen after setPosix because that sets _olsen to "";
- return true;
- }
-#endif // EZTIME_NVS_ENABLE
+ int16_t dst_offset = std_offset - dst_shift_hr * 60 - dst_shift_min;
+ // to find the year
+ tmElements_t tm;
+ ezTime.breakTime(t, tm);
+
+ // in local time
+ time_t dst_start = ezTime.makeUmpteenthTime(start_time_hr, start_time_min, 0, start_week, start_dow, start_month, tm.Year + 1970);
+ time_t dst_end = ezTime.makeUmpteenthTime(end_time_hr, end_time_min, 0, end_week, end_dow, end_month, tm.Year + 1970);
+
+ if (local_or_utc == UTC_TIME) {
+ dst_start -= std_offset;
+ dst_end -= dst_offset;
}
- ezTime.error(ezTime.error()); // This throws the last error (as generated by timezoneAPI) again.
- return false;
+ bool dst;
+ if (dst_end > dst_start) {
+ dst = (t >= dst_start && t < dst_end); // northern hemisphere
+ } else {
+ dst = !(t >= dst_end && t < dst_start); // southern hemisphere
+ }
-}
+ int16_t offset = dst ? dst_offset : std_offset;
-String Timezone::getOlsen() { return _olsen; }
+ if (tztd) {
+ tztd->is_dst = dst ? true : false;
+ tztd->offset = offset;
+ }
-#endif // EZTIME_NETWORK_ENABLE
+ if (local_or_utc == LOCAL_TIME) return t + offset * 60LL;
+ return t - offset * 60LL;
-void Timezone::setDefault() {
- defaultTZ = this;
}
-bool Timezone::isDST() {
- time_t t = ezTime.now();
- if (_tzdata.dst_start_in_utc == _tzdata.dst_end_in_utc) return false; // No DST observed here
-
- if (_tzdata.dst_end_in_utc > _tzdata.dst_start_in_utc) {
- return (t >= _tzdata.dst_start_in_utc && t < _tzdata.dst_end_in_utc); // northern hemisphere
- } else {
- return !(t >= _tzdata.dst_end_in_utc && t < _tzdata.dst_start_in_utc); // southern hemisphere
- }
-}
+String Timezone::getPosix() { return _posix; }
+
+
-bool Timezone::isDST_UTC(time_t t /*= TIME_NOW */) {
- if (t == TIME_NOW) t = ezTime.now();
+#ifdef EZTIME_NETWORK_ENABLE
+
+ bool Timezone::setLocation(String location /* = "" */) {
- tzData_t tz;
- if (year(t) != year()) {
- tz = ezTime.parsePosix(_posix, year(t));
- } else {
- tz = _tzdata;
+ info(F("Timezone lookup for: "));
+ infoln(location);
+ if (_locked_to_UTC) { ezTime.error(LOCKED_TO_UTC); return false; }
+
+ #ifndef EZTIME_ETHERNET
+ if (!WiFi.isConnected()) { ezTime.error(NO_NETWORK); return false; }
+ #endif
+
+ String path;
+ if (location.indexOf("/") != -1) {
+ path = F("/api/timezone/?"); path += ezTime.urlEncode(location);
+ } else if (location != "") {
+ path = F("/api/address/?"); path += ezTime.urlEncode(location);
+ } else {
+ path = F("/api/ip");
+ }
+
+ #ifndef EZTIME_ETHERNET
+ WiFiClient client;
+ #else
+ EthernetClient client;
+ #endif
+
+ if (!client.connect("timezoneapi.io", 80)) { ezTime.error(CONNECT_FAILED); return false; }
+
+ client.print(F("GET "));
+ client.print(path);
+ client.println(F(" HTTP/1.1"));
+ client.println(F("Host: timezoneapi.io"));
+ client.println(F("Connection: close"));
+ client.println();
+ client.setTimeout(3000);
+
+ debug(F("Sent request for http://timezoneapi.io")); debugln(path);
+ debugln(F("Reply from server:\r\n"));
+
+ // This "JSON parser" (bwahaha!) fits in the small memory of the AVRs
+ String tzinfo = "";
+ String needle = "\"id\":\"";
+ uint8_t search_state = 0;
+ uint8_t char_found = 0;
+ uint32_t start = millis();
+ while ( search_state < 4 && millis() - start < TIMEZONEAPI_TIMEOUT) {
+ if (client.available()) {
+ char c = client.read();
+ debug(c);
+ if (c == needle.charAt(char_found)) {
+ char_found++;
+ if (char_found == needle.length()) {
+ search_state++;
+ c = 0;
+ }
+ } else {
+ char_found = 0;
+ }
+ if (search_state == 1 || search_state == 3) {
+ if (c == '"') {
+ search_state++;
+ if (search_state == 2) {
+ needle = "\"tz_string\":\"";
+ tzinfo += ' ';
+ }
+ } else if (c && c != '\\') {
+ tzinfo += c;
+ }
+ }
+ }
+ }
+ debugln(F("\r\n\r\n"));
+ if (search_state != 4 || tzinfo == "") { ezTime.error(DATA_NOT_FOUND); return false; }
+
+ infoln(F("success."));
+ info(F("Found: ")); infoln(tzinfo);
+
+ #if defined(EZTIME_CACHE_EEPROM) || defined(EZTIME_CACHE_NVS)
+ writeCache(tzinfo);
+ #endif
+
+ _posix = tzinfo.substring(tzinfo.indexOf(' ') + 1);
+ return true;
}
- if (tz.dst_start_in_utc == tz.dst_end_in_utc) return false; // No DST observed here
-
- if (tz.dst_end_in_utc > tz.dst_start_in_utc) {
- return (t >= tz.dst_start_in_utc && t < tz.dst_end_in_utc); // northern hemisphere
- } else {
- return !(t >= tz.dst_end_in_utc && t < tz.dst_start_in_utc); // southern hemisphere
- }
-}
+ #ifdef EZTIME_CACHE_EEPROM
+ bool Timezone::setCache(const int16_t address) {
+ if (address + EEPROM_CACHE_LEN > EEPROM.length()) { ezTime.error(CACHE_TOO_SMALL); return false; }
+ _eeprom_address = address;
+ return setCache();
+ }
+ #endif
+
+ #ifdef EZTIME_CACHE_NVS
+ bool Timezone::setCache(const String name, const String key) {
+ _nvs_name = name;
+ _nvs_key = key;
+ return setCache();
+ }
+ #endif
+
+ #if defined(EZTIME_CACHE_EEPROM) || defined(EZTIME_CACHE_NVS)
+
+ bool Timezone::setCache() {
+ String olsen, posix;
+ uint8_t months_since_jan_2018;
+ if (readCache(olsen, posix, months_since_jan_2018)) {
+ _posix = posix;
+ _cache_month = months_since_jan_2018;
+ if ( (year() - 2018) * 12 + month(LAST_READ) - months_since_jan_2018 > MAX_CACHE_AGE_MONTHS) {
+ setLocation(olsen);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ void Timezone::clearCache(bool delete_section /* = false */) {
+
+ #ifdef EZTIME_CACHE_EEPROM
+ if (_eeprom_address < 0) { ezTime.error(NO_CACHE_SET); return; }
+ for (int16_t n = _eeprom_address; n < _eeprom_address + EEPROM_CACHE_LEN; n++) EEPROM.write(n, 0);
+ #endif
+
+ #ifdef EZTIME_CACHE_NVS
+ if (_nvs_name = "" || _nvs_key = "") { ezTime.error(NO_CACHE_SET); return; }
+ Preferences prefs;
+ prefs.begin(_nvs_name, false);
+ if (delete_section) {
+ prefs.clear();
+ } else {
+ prefs.remove(_nvs_key);
+ }
+ prefs.end();
+ #endif
+ }
+
+ String Timezone::getOlsen() {
+ String olsen, posix;
+ uint8_t months_since_jan_2018;
+ if (readCache(olsen, posix, months_since_jan_2018)) return olsen;
+ return "";
+ }
-bool Timezone::isDST_local(time_t t /*= TIME_NOW */) {
- if (t == TIME_NOW) return isDST_UTC(TIME_NOW); //Prevent loops where timezone's now() tries to find offset
+ bool Timezone::writeCache(const String &str) {
+ uint8_t months_since_jan_2018 = 0;
+ if (year() >= 2018) months_since_jan_2018 = (year(LAST_READ) - 2018) * 12 + month(LAST_READ) - 1;
+
+ #ifdef EZTIME_CACHE_EEPROM
+ if (_eeprom_address < 0) return false;
+ info(F("Caching timezone data "));
+ if (str.length() > MAX_CACHE_PAYLOAD) { ezTime.error(CACHE_TOO_SMALL); return false; }
+
+ uint16_t last_byte = _eeprom_address + EEPROM_CACHE_LEN - 1;
+ uint16_t addr = _eeprom_address;
+
+ // First byte is cache age, in months since 2018
+ EEPROM.write(addr++, months_since_jan_2018);
+
+ // Second byte is length of payload
+ EEPROM.write(addr++, str.length());
+
+ // Followed by payload, compressed. Every 4 bytes to three by encoding only 6 bits, ASCII all-caps
+ str.toUpperCase();
+ uint8_t store = 0;
+ for (uint8_t n = 0; n < str.length(); n++) {
+ unsigned char c = str.charAt(n) - 32;
+ if ( c > 63) c = 0;
+ switch (n % 4) {
+ case 0:
+ store = c << 2; //all of 1st
+ break;
+ case 1:
+ store |= c >> 4; //high two of 2nd
+ EEPROM.write(addr++, store);
+ store = c << 4; //low four of 2nd
+ break;
+ case 2:
+ store |= c >> 2; //high four of 3rd
+ EEPROM.write(addr++, store);
+ store = c << 6; //low two of third
+ break;
+ case 3:
+ store |= c; //all of 4th
+ EEPROM.write(addr++, store);
+ store = 0;
+ }
+ }
+ if (store) EEPROM.write(addr++, store);
+
+ // Fill rest of cache (except last byte) with zeroes
+ for (; addr < last_byte; addr++) EEPROM.write(addr, 0);
+
+ // Add all bytes in cache % 256 and add 42, that is the checksum written to last byte.
+ // The 42 is because then checksum of all zeroes then isn't zero.
+ uint8_t checksum = 0;
+ for (uint16_t n = _eeprom_address; n < last_byte; n++) checksum += EEPROM.read(n);
+ checksum += 42;
+ EEPROM.write(last_byte, checksum);
+ infoln();
+ return true;
+ #endif
+
+ #ifdef EZTIME_CACHE_NVS
+ if (_nvs_name = "" || _nvs_key = "") return false;
+ infoln(F("Caching timezone data"));
+ Preferences prefs;
+ prefs.begin(_nvs_name, false);
+ prefs.putString(_nvs_key, String(months_since_jan_2018) + " " + str);
+ prefs.end();
+ return true;
+ #endif
+ }
- t = _readTime(t);
- tzData_t tz;
- if (year(t) != year()) {
- tz = ezTime.parsePosix(_posix, year(t));
- } else {
- tz = _tzdata;
- }
- if (tz.dst_start_in_local == tz.dst_end_in_local) return false; // No DST observed here
-
- if (tz.dst_end_in_utc > tz.dst_start_in_utc) {
- return (t >= tz.dst_start_in_local && t < tz.dst_end_in_local); // northern hemisphere
- } else {
- return !(t >= tz.dst_end_in_local && t < tz.dst_start_in_local); // southern hemisphere
- }
-}
+ bool Timezone::readCache(String &olsen, String &posix, uint8_t &months_since_jan_2018) {
+
+ #ifdef EZTIME_CACHE_EEPROM
+ if (_eeprom_address < 0) { ezTime.error(NO_CACHE_SET); return false; }
+
+ uint16_t last_byte = _eeprom_address + EEPROM_CACHE_LEN - 1;
+
+ for (uint16_t n = _eeprom_address; n <= last_byte; n++) {
+ debug(n);
+ debug(F(" "));
+ debugln(EEPROM.read(n), HEX);
+ }
+
+ // return false if checksum incorrect
+ uint8_t checksum = 0;
+ for (uint16_t n = _eeprom_address; n < last_byte; n++) checksum += EEPROM.read(n);
+ checksum += 42;
+ if (checksum != EEPROM.read(last_byte)) return false;
+ debugln(F("Checksum OK"));
+
+ // Return false if length impossible
+ uint8_t len = EEPROM.read(_eeprom_address + 1);
+ debug("Length: "); debugln(len);
+ if (len > MAX_CACHE_PAYLOAD) return false;
+
+ // OK, we're gonna decompress
+ olsen.reserve(len + 3); // Everything goes in olsen first. Decompression might overshoot 3
+ months_since_jan_2018 = EEPROM.read(_eeprom_address);
+ uint8_t outlen = 0;
+ uint8_t n = 0;
+
+ for (uint8_t n = 0; n < EEPROM_CACHE_LEN - 3; n++) {
+ uint16_t addr = n + _eeprom_address + 2;
+ uint8_t c = EEPROM.read(addr);
+ uint8_t p = EEPROM.read(addr - 1); // previous byte
+ switch (n % 3) {
+ case 0:
+ olsen += (char)( ((c & 0b11111100) >> 2) + 32 );
+ break;
+ case 1:
+ olsen += (char)( ((p & 0b00000011) << 4) + ((c & 0b11110000) >> 4) + 32 );
+ break;
+ case 2:
+ olsen += (char)( ((p & 0b00001111) << 2) + ((c & 0b11000000) >> 6) + 32 );
+ olsen += (char)( (c & 0b00111111) + 32 );
+ }
+ if (olsen.length() >= len) break;
+ }
+
+ uint8_t first_space = olsen.indexOf(' ');
+ posix = olsen.substring(first_space + 1, len);
+ olsen = olsen.substring(0, first_space);
+
+ // Restore case of olsen (best effort)
+ String olsen_lowercase = olsen;
+ olsen_lowercase.toLowerCase();
+ for (uint8_t n = 1; n < olsen.length(); n++) {
+ unsigned char p = olsen.charAt(n - 1); // previous character
+ if (p != '_' && p != '/' && p != '-') {
+ olsen.setCharAt(n, olsen_lowercase[n]);
+ }
+ }
+
+ info(F("Retrieved from cache: ")); info(olsen); info (F(" : ")); infoln(posix);
+
+ return true;
+ #endif
+
+ #ifdef EZTIME_CACHE_NVS
+ if (_nvs_name = "" || _nvs_key = "") { ezTime.error(NO_CACHE_SET); return; }
+
+ Preferences prefs;
+ prefs.begin(_nvs_name, true);
+ String olsen = prefs.getString(_nvs_key);
+ prefs.end();
+ if (!olsen) return false;
+
+ uint8_t first_space = olsen.indexOf(' ');
+ uint8_t second_space = olsen.indexOf(' ', first_space + 1);
+ months_since_jan_2018 = olsen.toInt();
+ posix = olsen.substring(second_space + 1);
+ olsen = substring(olsen, first_space + 1, second_space);
+ return true;
+ #endif
+ }
+
+ #endif // defined(EZTIME_CACHE_EEPROM) || defined(EZTIME_CACHE_NVS)
-String Timezone::getTimezoneName(time_t t /*= TIME_NOW */) {
- if (isDST_local(t)) {
- return _tzdata.dst_tzname;
- } else {
- return _tzdata.std_tzname;
- }
+#endif // EZTIME_NETWORK_ENABLE
+
+
+void Timezone::setDefault() {
+ defaultTZ = this;
}
-int32_t Timezone::getOffset(time_t t /*= TIME_NOW */) {
-
- if (isDST_local(t)) {
- return _tzdata.dst_offset;
- } else {
- return _tzdata.std_offset;
- }
+bool Timezone::isDST(time_t t /*= TIME_NOW */, ezLocalOrUTC_t local_or_utc /* = LOCAL_TIME */) {
+ tzTimeData_t* tztd = (tzTimeData_t*) malloc(sizeof *tztd);
+ t = tzTime(t, local_or_utc, tztd);
+ bool tmp = tztd->is_dst;
+ free(tztd);
+ return tmp;
}
-time_t Timezone::now(bool update_last_read /* = true */) {
+String Timezone::getTimezoneName(time_t t /*= TIME_NOW */, ezLocalOrUTC_t local_or_utc /* = LOCAL_TIME */) {
+ tzTimeData_t* tztd = (tzTimeData_t*) malloc(sizeof *tztd);
+ t = tzTime(t, local_or_utc, tztd);
+ String tmp = tztd->is_dst ? _posix.substring(tztd->dstname_begin, tztd->dstname_end + 1) : _posix.substring(0, tztd->stdname_end + 1);
+ free(tztd);
+ return tmp;
+}
- time_t t;
- t = ezTime.now();
- if (_tzdata.dst_start_in_utc == _tzdata.dst_end_in_utc) {
- t -= _tzdata.std_offset;
- } else {
- if (_tzdata.dst_end_in_utc > _tzdata.dst_start_in_utc) {
- t -= (t >= _tzdata.dst_start_in_utc && t < _tzdata.dst_end_in_utc) ? _tzdata.dst_offset : _tzdata.std_offset;
- } else {
- t -= (t >= _tzdata.dst_end_in_utc && t < _tzdata.dst_start_in_utc) ? _tzdata.dst_offset : _tzdata.std_offset;
- }
- }
- if (update_last_read) _last_read_t = t;
- return t;
+int16_t Timezone::getOffset(time_t t /*= TIME_NOW */, ezLocalOrUTC_t local_or_utc /* = LOCAL_TIME */) {
+ tzTimeData_t* tztd = (tzTimeData_t*) malloc(sizeof *tztd);
+ t = tzTime(t, local_or_utc, tztd);
+ int16_t tmp = tztd->offset;
+ free(tztd);
+ return tmp;
}
-time_t Timezone::_readTime(time_t t) {
- switch (t) {
- case TIME_NOW: return now();
- case LAST_READ: return _last_read_t;
- default: return (t);
- }
-}
+time_t Timezone::now() {
+ return tzTime();
+}
-void Timezone::setTime(time_t t) {
- t += getOffset(t);
- ezTime._last_sync_time = t;
- ezTime._last_sync_millis = millis();
+void Timezone::setTime(time_t t, uint16_t ms /* = 0 */) {
+ int16_t offset;
+ offset = getOffset(t);
+ ezTime._last_sync_time = t + offset * 60;
+ ezTime._last_sync_millis = millis() - ms;
ezTime._time_status = timeSet;
}
@@ -961,8 +1081,10 @@ String Timezone::dateTime(String format /* = DEFAULT_TIMEFORMAT */) {
}
String Timezone::dateTime(time_t t, String format /* = DEFAULT_TIMEFORMAT */) {
- t = _readTime(t);
-
+
+ tzTimeData_t* tztd = (tzTimeData_t*) malloc(sizeof *tztd);
+ t = tzTime(t, LOCAL_TIME, tztd);
+
String tmpstr;
uint8_t tmpint8;
String out = "";
@@ -979,7 +1101,7 @@ String Timezone::dateTime(time_t t, String format /* = DEFAULT_TIMEFORMAT */) {
for (int8_t n = 0; n < format.length(); n++) {
- char c = (char) format.substring(n, n + 1).c_str()[0];
+ char c = (char) format.c_str()[n];
if (escape_char) {
out += String(c);
@@ -996,8 +1118,7 @@ String Timezone::dateTime(time_t t, String format /* = DEFAULT_TIMEFORMAT */) {
out += ezTime.zeropad(tm.Day, 2);
break;
case 'D': // A textual representation of a day, three letters
- tmpstr = ezTime.dayString(tm.Wday);
- out += tmpstr.substring(0,3);
+ out += ezTime.dayString(tm.Wday).substring(0,3);
break;
case 'j': // Day of the month without leading zeros
out += String(tm.Day);
@@ -1015,15 +1136,15 @@ String Timezone::dateTime(time_t t, String format /* = DEFAULT_TIMEFORMAT */) {
case 1:
case 21:
case 31:
- out += "st"; break;
+ out += F("st"); break;
case 2:
case 22:
- out += "nd"; break;
+ out += F("nd"); break;
case 3:
case 23:
- out += "rd"; break;
+ out += F("rd"); break;
default:
- out += "th"; break;
+ out += F("th"); break;
}
break;
case 'w': // Numeric representation of the day of the week ( 0 = Sunday )
@@ -1036,8 +1157,7 @@ String Timezone::dateTime(time_t t, String format /* = DEFAULT_TIMEFORMAT */) {
out += ezTime.zeropad(tm.Month, 2);
break;
case 'M': // A short textual representation of a month, three letters
- tmpstr = ezTime.monthString(tm.Month);
- out += tmpstr.substring(0,3);
+ out += ezTime.monthString(tm.Month).substring(0,3);
break;
case 'n': // Numeric representation of a month, without leading zeros
out += String(tm.Month);
@@ -1052,10 +1172,10 @@ String Timezone::dateTime(time_t t, String format /* = DEFAULT_TIMEFORMAT */) {
out += ezTime.zeropad((tm.Year + 1970) % 100, 2);
break;
case 'a': // am or pm
- out += (tm.Hour < 12) ? "am" : "pm";
+ out += (tm.Hour < 12) ? F("am") : F("pm");
break;
case 'A': // AM or PM
- out += (tm.Hour < 12) ? "AM" : "PM";
+ out += (tm.Hour < 12) ? F("AM") : F("PM");
break;
case 'g': // 12-hour format of an hour without leading zeros
out += String(hour12);
@@ -1076,135 +1196,162 @@ String Timezone::dateTime(time_t t, String format /* = DEFAULT_TIMEFORMAT */) {
out += ezTime.zeropad(tm.Second, 2);
break;
case 'T': // abbreviation for timezone
- out += getTimezoneName(LAST_READ);
+ out += tztd->is_dst ? _posix.substring(tztd->dstname_begin, tztd->dstname_end + 1) : _posix.substring(0, tztd->stdname_end + 1);
break;
case 'v': // milliseconds as three digits
out += ezTime.zeropad(ezTime._last_read_ms, 3);
break;
- case 'e': // Timezone identifier (Olsen or if not available current TZ abbreviation)
- if (_olsen != "") {
- out += _olsen;
- } else {
- out += getTimezoneName(LAST_READ);
- }
- break;
+ case 'e': // Timezone identifier (Olsen)
+ out += getOlsen();
+ break;
case 'O': // Difference to Greenwich time (GMT) in hours and minutes written together (+0200)
case 'P': // Difference to Greenwich time (GMT) in hours and minutes written with colon (+02:00)
- o = getOffset(LAST_READ);
- out += (o >= 0) ? "+" : "-";
+ o = tztd->offset;
+ out += (o < 0) ? "+" : "-"; // reverseed from our offset
if (o < 0) o = 0 - o;
- out += ezTime.zeropad(o / 3600, 2);
- out += (c == 'P') ? ":" : "";
out += ezTime.zeropad(o / 60, 2);
+ out += (c == 'P') ? ":" : "";
+ out += ezTime.zeropad(o % 60, 2);
break;
case 'Z': //Timezone offset in seconds. West of UTC is negative, east of UTC is positive.
- out+= String(0 - getOffset(LAST_READ));
+ out += String(0 - tztd->offset * 60);
+ break;
+ case 'z':
+ out += String(dayOfYear(t)); // The day of the year (starting from 0)
+ break;
+ case 'W':
+ out += ezTime.zeropad(weekISO(t), 2); // ISO-8601 week number of year, weeks starting on Monday
+ break;
+ case 'X':
+ out += String(yearISO(t)); // ISO-8601 year-week notation year, see https://en.wikipedia.org/wiki/ISO_week_date
break;
+
default:
out += String(c);
-
- // z -> The day of the year (starting from 0)
-
- // W -> ISO-8601 week number of year, weeks starting on Monday
-
}
}
}
+ free(tztd);
+
return out;
}
-uint8_t Timezone::hour(time_t t /* = TIME_NOW */) {
- t = _readTime(t);
+uint8_t Timezone::hour(time_t t /*= TIME_NOW */, ezLocalOrUTC_t local_or_utc /* = LOCAL_TIME */) {
+ t = tzTime(t, local_or_utc);
return t / 3600 % 24;
}
-uint8_t Timezone::minute(time_t t /*= TIME_NOW */) {
- t = _readTime(t);
+uint8_t Timezone::minute(time_t t /*= TIME_NOW */, ezLocalOrUTC_t local_or_utc /* = LOCAL_TIME */) {
+ t = tzTime(t, local_or_utc);
return t / 60 % 60;
}
-uint8_t Timezone::second(time_t t /* = TIME_NOW */) {
- t = _readTime(t);
+uint8_t Timezone::second(time_t t /*= TIME_NOW */, ezLocalOrUTC_t local_or_utc /* = LOCAL_TIME */) {
+ t = tzTime(t, local_or_utc);
return t % 60;
}
-uint16_t Timezone::ms(time_t t /* = TIME_NOW */) {
+uint16_t Timezone::ms(time_t t /*= TIME_NOW */, ezLocalOrUTC_t local_or_utc /* = LOCAL_TIME */) {
// Note that here passing anything but TIME_NOW or LAST_READ is pointless
- t = _readTime(t);
- return ezTime._last_read_ms;
+ if (t == TIME_NOW) { ezTime.now(); return ezTime._last_read_ms; }
+ if (t == LAST_READ) return ezTime._last_read_ms;
+ return 0;
}
-uint8_t Timezone::day(time_t t /* = TIME_NOW */) {
+uint8_t Timezone::day(time_t t /*= TIME_NOW */, ezLocalOrUTC_t local_or_utc /* = LOCAL_TIME */) {
+ t = tzTime(t, local_or_utc);
tmElements_t tm;
ezTime.breakTime(t, tm);
return tm.Day;
}
-uint8_t Timezone::weekday(time_t t /* = TIME_NOW */) {
- t = _readTime(t);
+uint8_t Timezone::weekday(time_t t /*= TIME_NOW */, ezLocalOrUTC_t local_or_utc /* = LOCAL_TIME */) {
+ t = tzTime(t, local_or_utc);
tmElements_t tm;
ezTime.breakTime(t, tm);
return tm.Wday;
}
-uint8_t Timezone::month(time_t t /* = TIME_NOW */) {
- t = _readTime(t);
+uint8_t Timezone::month(time_t t /*= TIME_NOW */, ezLocalOrUTC_t local_or_utc /* = LOCAL_TIME */) {
+ t = tzTime(t, local_or_utc);
tmElements_t tm;
ezTime.breakTime(t, tm);
return tm.Month;
}
-uint16_t Timezone::year(time_t t /* = TIME_NOW */) {
- t = _readTime(t);
+uint16_t Timezone::year(time_t t /*= TIME_NOW */, ezLocalOrUTC_t local_or_utc /* = LOCAL_TIME */) {
+ t = tzTime(t, local_or_utc);
tmElements_t tm;
ezTime.breakTime(t, tm);
return tm.Year + 1970;
}
-bool Timezone::secondChanged() {
- time_t t = now(false);
- if (_last_read_t != t) return true;
- return false;
+uint16_t Timezone::dayOfYear(time_t t /*= TIME_NOW */, ezLocalOrUTC_t local_or_utc /* = LOCAL_TIME */) {
+ t = tzTime(t, local_or_utc);
+ time_t jan_1st = ezTime.makeTime(0, 0, 0, 1, 1, year(t));
+ return (t - jan_1st) / SECS_PER_DAY;
}
-bool Timezone::minuteChanged() {
- time_t t = now(false);
- if (_last_read_t / 60 != t / 60) return true;
- return false;
+// Now this is where this gets a little obscure. The ISO year can be different from the
+// actual (Gregorian) year. That is: you can be in january and still be in week 53 of past
+// year, _and_ you can be in december and be in week one of the next. The ISO 8601
+// definition for week 01 is the week with the Gregorian year's first Thursday in it.
+// See https://en.wikipedia.org/wiki/ISO_week_date
+//
+#define startISOyear(args...) ezTime.makeUmpteenthTime(0, 0, 0, 1, 5, 1, args) - 3UL * SECS_PER_DAY;
+uint8_t Timezone::weekISO(time_t t /*= TIME_NOW */, ezLocalOrUTC_t local_or_utc /* = LOCAL_TIME */) {
+ t = tzTime(t, local_or_utc);
+ int16_t yr = year(t);
+ time_t this_year = startISOyear(yr);
+ time_t prev_year = startISOyear(yr - 1);
+ time_t next_year = startISOyear(yr + 1);
+ if (t < this_year) this_year = prev_year;
+ if (t > next_year) this_year = next_year;
+ return (t - this_year) / ( SECS_PER_DAY * 7UL) + 1;
}
+uint16_t Timezone::yearISO(time_t t /*= TIME_NOW */, ezLocalOrUTC_t local_or_utc /* = LOCAL_TIME */) {
+ t = tzTime(t, local_or_utc);
+ int16_t yr = year(LAST_READ);
+ time_t this_year = startISOyear(yr);
+ time_t prev_year = startISOyear(yr - 1);
+ time_t next_year = startISOyear(yr + 1);
+ if (t < this_year) return yr - 1;
+ if (t > next_year) return yr + 1;
+ return yr;
+}
+
+
Timezone UTC;
Timezone& defaultTZ = UTC;
#ifdef ARDUINO_TIMELIB_COMPATIBILITY
-time_t now() { return (defaultTZ.now()); }
-uint8_t second(time_t t = TIME_NOW) { return (defaultTZ.second(t)); }
-uint8_t minute(time_t t = TIME_NOW) { return (defaultTZ.minute(t)); }
-uint8_t hour(time_t t = TIME_NOW) { return (defaultTZ.hour(t)); }
-uint8_t day(time_t t = TIME_NOW) { return (defaultTZ.day(t)); }
-uint8_t weekday(time_t t = TIME_NOW) { return (defaultTZ.weekday(t)); }
-uint8_t month(time_t t = TIME_NOW) { return (defaultTZ.month(t)); }
-uint16_t year(time_t t = TIME_NOW) { return (defaultTZ.year(t)); }
-uint8_t hourFormat12(time_t t = TIME_NOW) { return (defaultTZ.hour(t) % 12); }
-bool isAM(time_t t = TIME_NOW) { return (defaultTZ.hour(t) < 12) ? true : false; }
-bool isPM(time_t t = TIME_NOW) { return (defaultTZ.hour(t) >= 12) ? true : false; }
-
-String monthStr(const uint8_t month) { return ezTime.monthString(month); }
-String monthShortStr(const uint8_t month) { String tmp = ezTime.monthString(month); return tmp.substring(0,3); }
-String dayStr(const uint8_t day) { return ezTime.dayString(day); }
-String dayShortStr(const uint8_t day) { String tmp = ezTime.dayString(day); return tmp.substring(0,3); }
-
-void setTime(time_t t) { defaultTZ.setTime(t); }
-void setTime(const uint8_t hr, const uint8_t min, const uint8_t sec, const uint8_t day, const uint8_t month, const uint16_t yr) { defaultTZ.setTime(hr, min, sec, day, month, yr); }
-void breakTime(time_t t, tmElements_t &tm) { ezTime.breakTime(t, tm); }
-
-time_t makeTime(tmElements_t &tm) { return ezTime.makeTime(tm); }
-time_t makeTime(uint8_t hour, uint8_t minute, uint8_t second, uint8_t day, uint8_t month, int16_t year) { return ezTime.makeTime(hour, minute, second, day, month, year); }
-
-timeStatus_t timeStatus() { ezTime.timeStatus(); }
-
-#endif //ARDUINO_TIMELIB_COMPATIBILITY
\ No newline at end of file
+ // Can't be macros because then it would also expand the explicit class member calls
+
+ time_t now() { return (defaultTZ.now()); }
+ uint8_t second(time_t t = TIME_NOW) { return (defaultTZ.second(t)); }
+ uint8_t minute(time_t t = TIME_NOW) { return (defaultTZ.minute(t)); }
+ uint8_t hour(time_t t = TIME_NOW) { return (defaultTZ.hour(t)); }
+ uint8_t day(time_t t = TIME_NOW) { return (defaultTZ.day(t)); }
+ uint8_t weekday(time_t t = TIME_NOW) { return (defaultTZ.weekday(t)); }
+ uint8_t month(time_t t = TIME_NOW) { return (defaultTZ.month(t)); }
+ uint16_t year(time_t t = TIME_NOW) { return (defaultTZ.year(t)); }
+ uint8_t hourFormat12(time_t t = TIME_NOW) { return (defaultTZ.hour(t) % 12); }
+ bool isAM(time_t t = TIME_NOW) { return (defaultTZ.hour(t) < 12) ? true : false; }
+ bool isPM(time_t t = TIME_NOW) { return (defaultTZ.hour(t) >= 12) ? true : false; }
+ String monthStr(const uint8_t month) { return ezTime.monthString(month); }
+ String monthShortStr(const uint8_t month) { return ezTime.monthString(month).substring(0,3); }
+ String dayStr(const uint8_t day) { return ezTime.dayString(day); }
+ String dayShortStr(const uint8_t day) { return ezTime.dayString(day).substring(0,3); }
+ void setTime(time_t t) { defaultTZ.setTime(t); }
+ void setTime(const uint8_t hr, const uint8_t min, const uint8_t sec, const uint8_t day, const uint8_t month, const uint16_t yr) { defaultTZ.setTime(hr, min, sec, day, month, yr); }
+ void breakTime(time_t t, tmElements_t &tm) { ezTime.breakTime(t, tm); }
+ time_t makeTime(tmElements_t &tm) { return ezTime.makeTime(tm); }
+ time_t makeTime(uint8_t hour, uint8_t minute, uint8_t second, uint8_t day, uint8_t month, int16_t year) { return ezTime.makeTime(hour, minute, second, day, month, year); }
+ timeStatus_t timeStatus() { return ezTime.timeStatus(); }
+
+#endif
\ No newline at end of file
diff --git a/src/ezTime.h b/src/ezTime.h
index fe4057c..282a036 100644
--- a/src/ezTime.h
+++ b/src/ezTime.h
@@ -1,25 +1,38 @@
#ifndef _EZTIME_H_
+#ifdef __cplusplus
#define _EZTIME_H_
+
+// Whether or not to be compatible with Paul Stoffregen's Arduino Time Library.
+// (which makes a lot of functions available in your root namespace, additionally to through the
+// objects created by ezTime.)
#define ARDUINO_TIMELIB_COMPATIBILITY
// Compiles in NTP updating and timezoneapi.io fetching
#define EZTIME_NETWORK_ENABLE
-// On espressif turn on NVS storage (with "Preferences")
-#if defined (ESP32) // This syntax so we can add " || defined (SOMEOTHER"
-#define EZTIME_NVS_ENABLE
-#endif
+// Arduino Ethernet shields
+// #define EZTIME_ETHERNET
+
+// Uncomment one of the below to only put only messages up to a certain level in the compiled code
+// (You still need to turn them on with ezTime.debugLevel(someLevel) to see them)
+// #define EZTIME_MAX_DEBUGLEVEL_NONE
+// #define EZTIME_MAX_DEBUGLEVEL_ERROR
+// #define EZTIME_MAX_DEBUGLEVEL_INFO
+
+// Cache mechanism, either EEPROM or NVS, not both. (See README)
+#define EZTIME_CACHE_EEPROM
+// #define EZTIME_CACHE_NVS
#if !defined(__time_t_defined) // avoid conflict with newlib or other posix libc
-typedef unsigned long time_t;
+ typedef unsigned long time_t;
#endif
#include
#ifndef __AVR__
-#include // for __time_t_defined, but avr libc lacks sys/types.h
+ #include // for __time_t_defined, but avr libc lacks sys/types.h
#endif
#if !defined(__time_t_defined) // avoid conflict with newlib or other posix libc
@@ -27,16 +40,20 @@ typedef unsigned long time_t;
#endif
+extern "C++" {
+
////////// Error handing
typedef enum {
NO_ERROR,
- LAST_ERROR, // Pseudo error: replaced by last error
+ LAST_ERROR, // Pseudo-error: replaced by last error
NO_NETWORK,
TIMEOUT,
CONNECT_FAILED,
DATA_NOT_FOUND,
- LOCKED_TO_UTC
+ LOCKED_TO_UTC,
+ NO_CACHE_SET,
+ CACHE_TOO_SMALL
} ezError_t;
typedef enum {
@@ -46,6 +63,41 @@ typedef enum {
DEBUG
} ezDebugLevel_t;
+typedef enum {
+ LOCAL_TIME,
+ UTC_TIME
+} ezLocalOrUTC_t;
+
+#if defined(EZTIME_MAX_DEBUGLEVEL_NONE)
+ #define err(args...) ""
+ #define errln(args...) ""
+ #define info(args...) ""
+ #define infoln(args...) ""
+ #define debug(args...) ""
+ #define debugln(args...) ""
+#elif defined(EZTIME_MAX_DEBUGLEVEL_ERROR)
+ #define err(args...) if (ezTime._debug_level >= ERROR) Serial.print(args)
+ #define errln(args...) if (ezTime._debug_level >= ERROR) Serial.println(args)
+ #define info(args...) ""
+ #define infoln(args...) ""
+ #define debug(args...) ""
+ #define debugln(args...) ""
+#elif defined(EZTIME_MAX_DEBUGLELEL_INFO)
+ #define err(args...) if (ezTime._debug_level >= ERROR) Serial.print(args)
+ #define errln(args...) if (ezTime._debug_level >= ERROR) Serial.println(args)
+ #define info(args...) if (ezTime._debug_level >= INFO) Serial.print(args)
+ #define infoln(args...) if (ezTime._debug_level >= INFO) Serial.println(args)
+ #define debug(args...) ""
+ #define debugln(args...) ""
+#else // nothing specified compiles everything in.
+ #define err(args...) if (ezTime._debug_level >= ERROR) Serial.print(args)
+ #define errln(args...) if (ezTime._debug_level >= ERROR) Serial.println(args)
+ #define info(args...) if (ezTime._debug_level >= INFO) Serial.print(args)
+ #define infoln(args...) if (ezTime._debug_level >= INFO) Serial.println(args)
+ #define debug(args...) if (ezTime._debug_level >= DEBUG) Serial.print(args)
+ #define debugln(args...) if (ezTime._debug_level >= DEBUG) Serial.println(args)
+#endif
+
////////////////////////
@@ -69,224 +121,244 @@ typedef enum {
} timeStatus_t;
typedef struct {
- String std_tzname;
- String dst_tzname;
- int32_t std_offset;
- int32_t dst_offset;
- time_t dst_start_in_local;
- time_t dst_end_in_local;
- time_t dst_start_in_utc;
- time_t dst_end_in_utc;
-} tzData_t;
+ uint8_t stdname_end; //Positions in _posix String. stdname_begin fixed at 0
+ uint8_t dstname_begin;
+ uint8_t dstname_end;
+ bool is_dst;
+ int16_t offset; // offset from UTC in minutes
+} tzTimeData_t;
#define TIME_NOW 0xFFFFFFFF
#define LAST_READ 0xFFFFFFFE
#define NTP_PACKET_SIZE 48
-#define NTP_LOCAL_PORT 2342
+#define NTP_LOCAL_TIME_PORT 2342
#define NTP_SERVER "pool.ntp.org"
#define NTP_TIMEOUT 1500 // milliseconds
#define NTP_INTERVAL 600 // default update interval in seconds
-#define NTP_RETRY 3 // Retry after this many seconds on failed NTP
+#define NTP_RETRY 5 // Retry after this many seconds on failed NTP
#define NTP_STALE_AFTER 3600 // If update due for this many seconds, set timeStatus to timeNeedsSync
+#define TIMEZONEAPI_TIMEOUT 2000 // milliseconds
+
+#define EEPROM_CACHE_LEN 50
+#define MAX_CACHE_PAYLOAD ((EEPROM_CACHE_LEN - 3) / 3) * 4 + ( (EEPROM_CACHE_LEN - 3) % 3) // 2 bytes for len and date, then 4 to 3 (6-bit) compression on rest
+#define MAX_CACHE_AGE_MONTHS 2
+
// Various date-time formats
+#define ISO8601_YWD "X-\\Ww-N" // Note that ISO-8601 Year/Week/Day notation may be one year + or - one at beginning or end of year
#define ATOM "Y-m-d\\TH:i:sP"
#define COOKIE "l, d-M-Y H:i:s T"
#define ISO8601 "Y-m-d\\TH:i:sO"
#define RFC822 "D, d M y H:i:s O"
-#define RFC850 "l, d-M-y H:i:s T"
-#define RFC1036 "D, d M y H:i:s O"
-#define RFC1123 "D, d M Y H:i:s O"
-#define RFC2822 "D, d M Y H:i:s O"
-#define RFC3339 "Y-m-d\\TH:i:sP"
+#define RFC850 COOKIE
+#define RFC1036 RFC822
+#define RFC1123 RFC822
+#define RFC2822 RFC822
+#define RFC3339 ATOM
#define RFC3339_EXT "Y-m-d\\TH:i:s.vP"
-#define RSS "D, d M Y H:i:s O"
-#define W3C "Y-m-d\\TH:i:sP"
-#define DEFAULT_TIMEFORMAT RFC850
+#define RSS RFC822
+#define W3C ATOM
+#define DEFAULT_TIMEFORMAT COOKIE
+
+
+//
+// E Z t i m e c l a s s
+//
class EZtime {
- friend class Timezone;
+ friend class Timezone; // Allow methods from Timezone class to access private methods of this class
////////// Error handing
public:
- ezError_t error(); // Returns ezError_t enumeration of last error, resets _last_error to OK
- String errorString(ezError_t err); // Human-readable form of last error.
- void debug(ezDebugLevel_t level); // Sets serial printing of debug info to specified ezDebugLevel_t enumeration
+ static ezError_t error(); // Returns ezError_t enumeration of last error, resets _last_error to OK
+ static String errorString(ezError_t err = LAST_ERROR); // Human-readable form of last error.
+ static void debugLevel(ezDebugLevel_t level); // Sets serial printing of debug info to specified ezDebugLevel_t enumeration
private:
- void error(ezError_t err); // Used to set an error
- void debug(ezDebugLevel_t level, String str); // Used to print debug info
- void debugln(ezDebugLevel_t level, String str); // same, just adds \r\n
- String debugLevelString(ezDebugLevel_t level); // Human-readable form of debug level.
- ezError_t _last_error;
- ezDebugLevel_t _debug_level;
+ static void error(ezError_t err); // Used to set an error
+ static String debugLevelString(ezDebugLevel_t level); // Human-readable form of debug level.
+ static ezError_t _last_error;
+ static ezDebugLevel_t _debug_level;
///////////
public:
EZtime();
- timeStatus_t timeStatus();
- time_t now();
- void breakTime(time_t time, tmElements_t &tm); // break time_t into elements
- time_t makeTime(tmElements_t &tm); // convert time elements into time_t
- time_t makeTime(uint8_t hour, uint8_t minute, uint8_t second, uint8_t day, uint8_t month, int16_t year);
- time_t makeUmpteenthTime(uint8_t hour, uint8_t minute, uint8_t second, uint8_t umpteenth, uint8_t wday, uint8_t month, int16_t year);
- time_t compileTime(String compile_date = __DATE__, String compile_time = __TIME__);
- String monthString(uint8_t month);
- String dayString(uint8_t day);
+ static timeStatus_t timeStatus();
+ static time_t now(bool update_last_read = true);
+ static void breakTime(time_t time, tmElements_t &tm); // break time_t into elements
+ static time_t makeTime(tmElements_t &tm); // convert time elements into time_t
+ static time_t makeTime(uint8_t hour, uint8_t minute, uint8_t second, uint8_t day, uint8_t month, uint16_t year);
+ static time_t makeUmpteenthTime(uint8_t hour, uint8_t minute, uint8_t second, uint8_t umpteenth, uint8_t wday, uint8_t month, uint16_t year);
+ static time_t compileTime(String compile_date = __DATE__, String compile_time = __TIME__);
+ static String monthString(uint8_t month);
+ static String dayString(uint8_t day);
+ static bool secondChanged();
+ static bool minuteChanged();
private:
- tzData_t parsePosix(String posix, int16_t year);
- int32_t millisElapsed(unsigned long m);
- time_t _last_sync_time;
- int32_t _fudge;
- unsigned long _last_sync_millis;
- bool _debug_enabled, _ntp_enabled;
- uint16_t _last_read_ms;
- timeStatus_t _time_status;
-
-#ifdef EZTIME_NETWORK_ENABLE
-
- public:
- bool queryNTP(String server, time_t &t, unsigned long &measured_at); // measured_at: millis() at measurement, t is converted to secs since 1970
- void updateNow();
- void setServer(String ntp_server = NTP_SERVER);
- void setInterval(uint16_t seconds = 0); // 0 = no NTP updates
- bool waitForSync(uint16_t timeout = 0); // timeout in seconds
-#ifdef ESP32
- void clearCache();
-#endif // ESP32
-
- private:
- uint16_t _update_interval; // in seconds
- time_t _update_due;
- uint16_t _update_backoff; // seconds to add to _update_due to retry NTP
- uint16_t _ntp_local_port, _ntp_interval, _ntp_max_drift;
- String _ntp_server;
-
-#endif // EZTIME_NETWORK_ENABLE
+ static time_t _last_sync_time, _last_read_t;
+ static uint32_t _last_sync_millis;
+ static bool _ntp_enabled;
+ static uint16_t _last_read_ms;
+ static timeStatus_t _time_status;
+
+ #ifdef EZTIME_NETWORK_ENABLE
+
+ public:
+ static bool queryNTP(String server, time_t &t, unsigned long &measured_at); // measured_at: millis() at measurement, t is converted to secs since 1970
+ static void updateNow();
+ static void setServer(String ntp_server = NTP_SERVER);
+ static void setInterval(uint16_t seconds = 0); // 0 = no NTP updates
+ static bool waitForSync(uint16_t timeout = 0); // timeout in seconds
+
+ private:
+ static void updateIfNeeded();
+ static uint16_t _update_interval; // in seconds
+ static uint32_t _update_due;
+ static uint16_t _update_backoff; // seconds to add to _update_due to retry NTP
+ static String _ntp_server;
+
+ #endif // EZTIME_NETWORK_ENABLE
////////// Free extras ...
public:
- String urlEncode(String str); // Does what you think it does
- String zeropad(uint32_t number, uint8_t length); // Returns number as string of given length, zero-padded on left if needed
- String getBetween(String &haystack, String before_needle, String after_needle = ""); // Returns what's between before_needle and after_needle in haystack, or "" if not found. Returns until end of string if after_needle is empty
- bool timezoneAPI(String location, String &olsen, String &posix);
+ static String urlEncode(String str); // Does what you think it does
+ static String zeropad(uint32_t number, uint8_t length); // Returns number as string of given length, zero-padded on left if needed
//////////
};
-extern EZtime ezTime;
+extern EZtime ezTime; // declares the "ezTime" instance of the "EZtime" class
+//
+// T i m e z o n e c l a s s
+//
+
class Timezone {
public:
Timezone(bool locked_to_UTC = false);
bool setPosix(String posix);
String getPosix();
+ time_t tzTime(time_t t = TIME_NOW, ezLocalOrUTC_t local_or_utc = LOCAL_TIME, tzTimeData_t *tztd = NULL);
void setDefault();
- bool isDST_local(time_t t = TIME_NOW);
- bool isDST_UTC(time_t t = TIME_NOW);
- bool isDST();
- String getTimezoneName(time_t t = TIME_NOW);
- int32_t getOffset(time_t t = TIME_NOW);
- time_t now(bool update_last_read = true);
- void setTime(time_t t);
+ bool isDST(time_t t = TIME_NOW, ezLocalOrUTC_t local_or_utc = LOCAL_TIME);
+ String getTimezoneName(time_t t = TIME_NOW, ezLocalOrUTC_t local_or_utc = LOCAL_TIME);
+ int16_t getOffset(time_t t = TIME_NOW, ezLocalOrUTC_t local_or_utc = LOCAL_TIME);
+ time_t now();
+ void setTime(time_t t, uint16_t ms = 0);
void setTime(const uint8_t hr, const uint8_t min, const uint8_t sec, const uint8_t day, const uint8_t mnth, uint16_t yr);
- String dateTime(String format = DEFAULT_TIMEFORMAT); // http://php.net/manual/en/function.date.php for conversion
+ String dateTime(String format = DEFAULT_TIMEFORMAT);
String dateTime(time_t t, String format = DEFAULT_TIMEFORMAT);
- uint8_t hour(time_t t = TIME_NOW); // 0-23
- uint8_t minute(time_t t = TIME_NOW); // 0-59
- uint8_t second(time_t t = TIME_NOW); // 0-59
- uint16_t ms(time_t t = TIME_NOW); // 0-999
- uint8_t day(time_t t = TIME_NOW); // 1-31
- uint8_t weekday(time_t t = TIME_NOW); // Day of the week (1-7), Sunday is day 1
- uint8_t month(time_t t = TIME_NOW); // 1-12
- uint16_t year(time_t t = TIME_NOW); // four digit year
- bool secondChanged(); // Since last call to something that caused a time read, to avoid excessive calling of, eg, timeString
- bool minuteChanged();
-
+ uint8_t hour(time_t t = TIME_NOW, ezLocalOrUTC_t local_or_utc = LOCAL_TIME); // 0-23
+ uint8_t minute(time_t t = TIME_NOW, ezLocalOrUTC_t local_or_utc = LOCAL_TIME); // 0-59
+ uint8_t second(time_t t = TIME_NOW, ezLocalOrUTC_t local_or_utc = LOCAL_TIME); // 0-59
+ uint16_t ms(time_t t = TIME_NOW, ezLocalOrUTC_t local_or_utc = LOCAL_TIME); // 0-999
+ uint8_t day(time_t t = TIME_NOW, ezLocalOrUTC_t local_or_utc = LOCAL_TIME); // 1-31
+ uint8_t weekday(time_t t = TIME_NOW, ezLocalOrUTC_t local_or_utc = LOCAL_TIME); // Day of the week (1-7), Sunday is day 1
+ uint8_t month(time_t t = TIME_NOW, ezLocalOrUTC_t local_or_utc = LOCAL_TIME); // 1-12
+ uint16_t year(time_t t = TIME_NOW, ezLocalOrUTC_t local_or_utc = LOCAL_TIME); // four digit year
+ uint16_t dayOfYear(time_t t = TIME_NOW, ezLocalOrUTC_t local_or_utc = LOCAL_TIME); // days from start of year, jan 1st = 0
+ uint8_t weekISO(time_t t = TIME_NOW, ezLocalOrUTC_t local_or_utc = LOCAL_TIME); // ISO-8601 week number (weeks starting on Monday)
+ uint16_t yearISO(time_t t = TIME_NOW, ezLocalOrUTC_t local_or_utc = LOCAL_TIME); // ISO-8601 year, can differ from actual year, plus or minus one
private:
- time_t _readTime(time_t t);
+ String _posix;
bool _locked_to_UTC;
- tzData_t _tzdata;
- String _posix, _olsen;
- time_t _last_read_t;
-
-#ifdef EZTIME_NETWORK_ENABLE
-
- public:
- bool setLocation(String location, bool force_lookup = false);
- String getOlsen();
-
-#endif // EZTIME_NETWORK_ENABLE
+
+ #ifdef EZTIME_NETWORK_ENABLE
+
+ public:
+ bool setLocation(String location = "");
+
+ #ifdef EZTIME_CACHE_EEPROM
+ public:
+ bool setCache(const int16_t address);
+ private:
+ int16_t _eeprom_address;
+ #endif
+ #ifdef EZTIME_CACHE_NVS
+ public:
+ bool setCache(const String name, const String key);
+ private:
+ String _nvs_name, _nvs_key;
+ #endif
+ #if defined(EZTIME_CACHE_EEPROM) || defined(EZTIME_CACHE_NVS)
+ public:
+ bool setCache();
+ void clearCache(bool delete_section = false);
+ String getOlsen();
+ private:
+ bool writeCache(const String &str);
+ bool readCache(String &olsen, String &posix, uint8_t &months_since_jan_2018);
+ uint8_t _cache_month;
+ #endif
+
+ #endif
};
extern Timezone UTC;
-
extern Timezone& defaultTZ;
#ifdef ARDUINO_TIMELIB_COMPATIBILITY
-/*==============================================================================*/
-/* Useful Constants */
-#define SECS_PER_MIN (60UL)
-#define SECS_PER_HOUR (3600UL)
-#define DAYS_PER_WEEK (7UL)
-#define SECS_PER_WEEK (SECS_PER_DAY * DAYS_PER_WEEK)
-#define SECS_PER_YEAR (SECS_PER_WEEK * 52UL)
-#define SECS_YR_2000 (946684800UL) // the time at the start of y2k
+ /* Useful Constants */
+ #define SECS_PER_MIN (60UL)
+ #define SECS_PER_HOUR (3600UL)
+ #define DAYS_PER_WEEK (7UL)
+ #define SECS_PER_WEEK (SECS_PER_DAY * DAYS_PER_WEEK)
+ #define SECS_PER_YEAR (SECS_PER_WEEK * 52UL)
+ #define SECS_YR_2000 (946684800UL) // the time at the start of y2k
-/* Useful Macros for getting elapsed time */
-#define numberOfSeconds(_time_) (_time_ % SECS_PER_MIN)
-#define numberOfMinutes(_time_) ((_time_ / SECS_PER_MIN) % SECS_PER_MIN)
-#define numberOfHours(_time_) (( _time_% SECS_PER_DAY) / SECS_PER_HOUR)
-#define dayOfWeek(_time_) ((( _time_ / SECS_PER_DAY + 4) % DAYS_PER_WEEK)+1) // 1 = Sunday
-#define elapsedDays(_time_) ( _time_ / SECS_PER_DAY) // this is number of days since Jan 1 1970
-#define elapsedSecsToday(_time_) (_time_ % SECS_PER_DAY) // the number of seconds since last midnight
-// The following macros are used in calculating alarms and assume the clock is set to a date later than Jan 1 1971
-// Always set the correct time before settting alarms
-#define previousMidnight(_time_) (( _time_ / SECS_PER_DAY) * SECS_PER_DAY) // time at the start of the given day
-#define nextMidnight(_time_) ( previousMidnight(_time_) + SECS_PER_DAY ) // time at the end of the given day
-#define elapsedSecsThisWeek(_time_) (elapsedSecsToday(_time_) + ((dayOfWeek(_time_)-1) * SECS_PER_DAY) ) // note that week starts on day 1
-#define previousSunday(_time_) (_time_ - elapsedSecsThisWeek(_time_)) // time at the start of the week for the given time
-#define nextSunday(_time_) ( previousSunday(_time_)+SECS_PER_WEEK) // time at the end of the week for the given time
-
-
-/* Useful Macros for converting elapsed time to a time_t */
-#define minutesToTime_t ((M)) ( (M) * SECS_PER_MIN)
-#define hoursToTime_t ((H)) ( (H) * SECS_PER_HOUR)
-#define daysToTime_t ((D)) ( (D) * SECS_PER_DAY) // fixed on Jul 22 2011
-#define weeksToTime_t ((W)) ( (W) * SECS_PER_WEEK)
-
-
-time_t now();
-uint8_t second(time_t t /* = TIME_NOW */);
-uint8_t minute(time_t t /* = TIME_NOW */);
-uint8_t hour(time_t t /* = TIME_NOW */);
-uint8_t day(time_t t /* = TIME_NOW */);
-uint8_t weekday(time_t t /* = TIME_NOW */);
-uint8_t month(time_t t /* = TIME_NOW */);
-uint16_t year(time_t t /* = TIME_NOW */);
-uint8_t hourFormat12(time_t t /* = TIME_NOW */);
-bool isAM(time_t t /* = TIME_NOW */);
-bool isPM(time_t t /* = TIME_NOW */);
-String monthStr(const uint8_t month);
-String monthShortStr(const uint8_t month);
-String dayStr(const uint8_t month);
-String dayShortStr(const uint8_t month);
-void setTime(time_t t);
-void setTime(const uint8_t hr, const uint8_t min, const uint8_t sec, const uint8_t day, const uint8_t month, const uint16_t yr);
-void breakTime(time_t t, tmElements_t &tm);
-time_t makeTime(tmElements_t &tm);
-time_t makeTime(uint8_t hour, uint8_t minute, uint8_t second, uint8_t day, uint8_t month, int16_t year);
-timeStatus_t timeStatus();
+ /* Useful Macros for getting elapsed time */
+ #define numberOfSeconds(_time_) (_time_ % SECS_PER_MIN)
+ #define numberOfMinutes(_time_) ((_time_ / SECS_PER_MIN) % SECS_PER_MIN)
+ #define numberOfHours(_time_) (( _time_% SECS_PER_DAY) / SECS_PER_HOUR)
+ #define dayOfWeek(_time_) ((( _time_ / SECS_PER_DAY + 4) % DAYS_PER_WEEK)+1) // 1 = Sunday
+ #define elapsedDays(_time_) ( _time_ / SECS_PER_DAY) // this is number of days since Jan 1 1970
+ #define elapsedSecsToday(_time_) (_time_ % SECS_PER_DAY) // the number of seconds since last midnight
+ // The following macros are used in calculating alarms and assume the clock is set to a date later than Jan 1 1971
+ // Always set the correct time before settting alarms
+ #define previousMidnight(_time_) (( _time_ / SECS_PER_DAY) * SECS_PER_DAY) // time at the start of the given day
+ #define nextMidnight(_time_) ( previousMidnight(_time_) + SECS_PER_DAY ) // time at the end of the given day
+ #define elapsedSecsThisWeek(_time_) (elapsedSecsToday(_time_) + ((dayOfWeek(_time_)-1) * SECS_PER_DAY) ) // note that week starts on day 1
+ #define previousSunday(_time_) (_time_ - elapsedSecsThisWeek(_time_)) // time at the start of the week for the given time
+ #define nextSunday(_time_) ( previousSunday(_time_)+SECS_PER_WEEK) // time at the end of the week for the given time
+
+ /* Useful Macros for converting elapsed time to a time_t */
+ #define minutesToTime_t ((M)) ( (M) * SECS_PER_MIN)
+ #define hoursToTime_t ((H)) ( (H) * SECS_PER_HOUR)
+ #define daysToTime_t ((D)) ( (D) * SECS_PER_DAY) // fixed on Jul 22 2011
+ #define weeksToTime_t ((W)) ( (W) * SECS_PER_WEEK)
+
+ time_t now();
+ uint8_t second(time_t t /* = TIME_NOW */);
+ uint8_t minute(time_t t /* = TIME_NOW */);
+ uint8_t hour(time_t t /* = TIME_NOW */);
+ uint8_t day(time_t t /* = TIME_NOW */);
+ uint8_t weekday(time_t t /* = TIME_NOW */);
+ uint8_t month(time_t t /* = TIME_NOW */);
+ uint16_t year(time_t t /* = TIME_NOW */);
+ uint8_t hourFormat12(time_t t /* = TIME_NOW */);
+ bool isAM(time_t t /* = TIME_NOW */);
+ bool isPM(time_t t /* = TIME_NOW */);
+ String monthStr(const uint8_t month);
+ String monthShortStr(const uint8_t month);
+ String dayStr(const uint8_t month);
+ String dayShortStr(const uint8_t month);
+ void setTime(time_t t);
+ void setTime(const uint8_t hr, const uint8_t min, const uint8_t sec, const uint8_t day, const uint8_t month, const uint16_t yr);
+ void breakTime(time_t t, tmElements_t &tm);
+ time_t makeTime(tmElements_t &tm);
+ time_t makeTime(uint8_t hour, uint8_t minute, uint8_t second, uint8_t day, uint8_t month, int16_t year);
+ timeStatus_t timeStatus();
#endif //ARDUINO_TIMELIB_COMPATIBILITY
-
+} // extern "C++"
+#endif // __cplusplus
#endif //_EZTIME_H_
\ No newline at end of file