commit 225358bb79c66a8b4978d1547b959da5293cb30a Author: Rop Date: Sat Aug 25 14:10:50 2018 +0200 Initial commit diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f5548d2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ + +.development diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bde60ce --- /dev/null +++ b/LICENSE @@ -0,0 +1,165 @@ +GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..b41b358 --- /dev/null +++ b/README.md @@ -0,0 +1,81 @@ +# ezTime Documentation + +## A brief history of ezTime + +## Getting started + +## Some quick examples + +# ezTime: complete documentation + +## NTP, staying in sync + +**`void ezTime.setServer(String ntp_server = NTP_SERVER)`** + +**`void ezTime.setInterval(uint16_t seconds = 0)`** + +**`bool ezTime.waitForSync(uint16_t timeout = 0)`** + +**`void ezTime.updateNow()`** + +**`timeStatus_t ezTime.timeStatus()`** + +**`bool ezTime.queryNTP(String server, time_t &t, unsigned long &measured_at)`** + + + +**`time_t ezTime.now()`** + +**`void ezTime.breakTime(time_t time, tmElements_t &tm)`** + +**`time_t ezTime.makeTime(tmElements_t &tm)`** + +**`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.makeUmpteenthTime(uint8_t hour, uint8_t minute, uint8_t second, uint8_t umpteenth, uint8_t wday, uint8_t month, int16_t year)`** + +## Timezones + + bool setPosix(String posix); + String getPosix(); + bool setLocation(String location, bool force_lookup = false); + String getOlsen(); + 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); + 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(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(); + +**`void ezTime.clearCache()`** + +## Error handling, debug information + +**`ezError_t ezTime.error()`** + +**`String ezTime.errorString(ezError_t err)`** + +**`void ezTime.debug(ezDebugLevel_t level)`** + + +## Free extras + + 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); \ No newline at end of file diff --git a/component.mk b/component.mk new file mode 100644 index 0000000..3bd5ac1 --- /dev/null +++ b/component.mk @@ -0,0 +1,7 @@ +# +# Main Makefile. This is basically the same as a component makefile. +# +# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.) + +COMPONENT_SRCDIRS := src +COMPONENT_ADD_INCLUDEDIRS := src diff --git a/examples/TimeFormats/TimeFormats.ino b/examples/TimeFormats/TimeFormats.ino new file mode 100644 index 0000000..430c134 --- /dev/null +++ b/examples/TimeFormats/TimeFormats.ino @@ -0,0 +1,32 @@ +#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() { + +} diff --git a/examples/Timezones/Timezones.ino b/examples/Timezones/Timezones.ino new file mode 100644 index 0000000..3e2f885 --- /dev/null +++ b/examples/Timezones/Timezones.ino @@ -0,0 +1,33 @@ +#include +#include + +void setup() { + + Serial.begin(115200); + WiFi.begin("your-ssid", "your-password"); + + // Uncomment the line below to see what it does behind the scenes + // ezTime.debug(INFO); + + ezTime.waitForSync(); + + Serial.println(); + Serial.println("UTC: " + UTC.dateTime()); + + + Timezone myTZ; + + myTZ.setLocation("Pacific/Auckland"); // Anything with a slash in it is interpreted as an official timezone name + Serial.println("Auckland: " + myTZ.dateTime()); // https://en.wikipedia.org/wiki/List_of_tz_database_time_zones + + myTZ.setLocation("Paris, Texas"); // Anything else is parsed as an address to see if that resolves + Serial.println("Paris, Texas: " + myTZ.dateTime()); + + myTZ.setLocation(""); // The empty string is resolved to the GeoIP location of your IP-address + Serial.println("Your local time: " + myTZ.dateTime()); + +} + +void loop() { + +} diff --git a/examples/milliseconds/milliseconds.ino b/examples/milliseconds/milliseconds.ino new file mode 100644 index 0000000..65e558f --- /dev/null +++ b/examples/milliseconds/milliseconds.ino @@ -0,0 +1,36 @@ +#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("H:i:s.v")); + } + + Serial.println(); + Serial.println("Those milliseconds between the first and the last line ..."); + Serial.println(); + Serial.println(" ... that's how long it took to get them out 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); +} diff --git a/library.json b/library.json new file mode 100644 index 0000000..4369b4f --- /dev/null +++ b/library.json @@ -0,0 +1,20 @@ +{ + "name": "ezTime", + "description": "An ESP32 time library, drop-in replacement for Arduino TimeLib, does NTP, supports timezones", + "keywords": "esp32", + "authors": { + "name": "Rop Gonggrijp", + "url": "https://github.com/ropg" + "maintainer": true + }, + "repository": { + "type": "git", + "url": "https://github.com/ropg/ezTime" + }, + "version": "0.0.1", + "framework": "arduino", + "platforms": "espressif32" + "build": { + "libArchive": false + } +} diff --git a/library.properties b/library.properties new file mode 100644 index 0000000..a7aecae --- /dev/null +++ b/library.properties @@ -0,0 +1,10 @@ +name=ezTime +version=0.0.1 +author=Rop Gonggrijp +maintainer=Rop Gonggrijp +sentence=An ESP32 time library, drop-in replacement for Arduino TimeLib, does NTP, supports timezones. +paragraph=See more on https://github.com/ropg/ezTime +category=Timing +url=https://github.com/ropg/ezTime +architectures=esp32 +includes=ezTime.h diff --git a/src/ezTime.cpp b/src/ezTime.cpp new file mode 100644 index 0000000..993653b --- /dev/null +++ b/src/ezTime.cpp @@ -0,0 +1,1113 @@ +#include + +#include // For timezone lookup cache + + +EZtime::EZtime() { + ezError_t _last_error = NO_ERROR; + ezDebugLevel_t _debug_level = NONE; + _time_status = timeNotSet; + _ntp_enabled = true; + _ntp_server = NTP_SERVER; + _ntp_local_port = NTP_LOCAL_PORT; + _update_due = 0; + _update_interval = UPDATE_INTERVAL; +} + + +////////// Error handing + +String EZtime::errorString(ezError_t err) { + 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"; + default: return "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"; + } +} + +ezError_t EZtime::error() { + ezError_t tmp = _last_error; + _last_error = NO_ERROR; + return tmp; +} + +void EZtime::error(ezError_t err) { + _last_error = err; + 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); + } +} + +void EZtime::debugln(ezDebugLevel_t level, String str) { + if (_debug_level >= level) { + Serial.println(str); + } +} + +//////////////////////// + + +timeStatus_t EZtime::timeStatus() { return _time_status; } + +// 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; +} + +void EZtime::setInterval(uint16_t seconds /* = 0 */) { _update_interval = seconds; } + +time_t EZtime::now() { + debugln(DEBUG, "now() entered"); + unsigned long m = millis(); + time_t t; + + if (_update_interval && (m >= _update_due) ) { + if (m - _update_due > 3600000) _time_status = timeNeedsSync; // If unable to sync for an hour, timeStatus = timeNeedsSync + unsigned long start = millis(); + 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; + _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; + _update_due = m + _update_interval * 1000; + _last_read_ms = new_ms; + 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)" ) ) ); + } else { + debugln(INFO, ""); + } + _time_status = timeSet; + } + } + t = _last_sync_time + ((m - _last_sync_millis) / 1000); + _last_read_ms = (m - _last_sync_millis) % 1000; + if (m < ezTime._last_sync_millis) t += 0xFFFFFFFF / 1000; // millis() rolled over, we're assuming just once :) + return t; +} + +void EZtime::setServer(String ntp_server /* = NTP_SERVER */) { _ntp_server = ntp_server; } + +void EZtime::updateNow() { _update_due = now(); now(); } + +bool EZtime::waitForSync(uint16_t timeout /* = 0 */) { + + unsigned long start = millis(); + + if (!WiFi.isConnected()) { + debug(INFO, "Waiting for WiFi ... "); + while (!WiFi.isConnected()) { + if ( timeout && (millis() - start) / 1000 > timeout ) { error(TIMEOUT); return false;}; + } + 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"); + } + +} + +void EZtime::breakTime(time_t timeInput, tmElements_t &tm){ + debugln(DEBUG, "breakTime entered"); + // break the given time_t into time components + // this is a more compact version of the C library localtime function + // note that year is offset from 1970 !!! + + uint8_t year; + uint8_t month, monthLength; + uint32_t time; + unsigned long days; + + time = (uint32_t)timeInput; + tm.Second = time % 60; + time /= 60; // now it is minutes + tm.Minute = time % 60; + time /= 60; // now it is hours + tm.Hour = time % 24; + time /= 24; // now it is days + tm.Wday = ((time + 4) % 7) + 1; // Sunday is day 1 + + year = 0; + days = 0; + while((unsigned)(days += (LEAP_YEAR(year) ? 366 : 365)) <= time) { + year++; + } + tm.Year = year; // year is offset from 1970 + + days -= LEAP_YEAR(year) ? 366 : 365; + time -= days; // now it is days in this year, starting at 0 + + days=0; + month=0; + monthLength=0; + for (month=0; month<12; month++) { + if (month==1) { // february + if (LEAP_YEAR(year)) { + monthLength=29; + } else { + monthLength=28; + } + } else { + monthLength = monthDays[month]; + } + + if (time >= monthLength) { + time -= monthLength; + } else { + break; + } + } + tm.Month = month + 1; // jan is month 1 + 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) { + tmElements_t tm; + tm.Hour = hour; + tm.Minute = minute; + tm.Second = second; + tm.Day = day; + tm.Month = month; + tm.Year = year - 1970; + return makeTime(tm); +} + +time_t EZtime::makeTime(tmElements_t &tm){ + debugln(DEBUG, "makeTime entered"); +// assemble time elements into time_t +// note year argument is offset from 1970 (see macros in time.h to convert to other formats) +// previous version used full four digit year (or digits since 2000),i.e. 2009 was 2009 or 9 + + int i; + uint32_t seconds; + + // seconds from 1970 till 1 jan 00:00:00 of the given year + seconds= tm.Year * (3600 * 24 * 365); + for (i = 0; i < tm.Year; i++) { + if (LEAP_YEAR(i)) { + seconds += 3600 * 24; // add extra days for leap years + } + } + + // add days for this year, months start from 1 + for (i = 1; i < tm.Month; i++) { + if ( (i == 2) && LEAP_YEAR(tm.Year)) { + seconds += SECS_PER_DAY * 29; + } else { + seconds += SECS_PER_DAY * monthDays[i-1]; //monthDay array starts from 0 + } + } + seconds+= (tm.Day-1) * SECS_PER_DAY; + seconds+= tm.Hour * 3600; + seconds+= tm.Minute * 60; + seconds+= tm.Second; + return (time_t)seconds; +} + +// 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) { + debugln(DEBUG, "makeUmpteenthTime entered"); + if (umpteenth == 5) umpteenth = 0; + uint8_t m = month; + uint8_t w = umpteenth; + if (w == 0) { // is this a "Last week" rule? + if (++m > 12) { // yes, for "Last", go to the next month + m = 1; + ++year; + } + w = 1; // and treat as first week of next month, subtract 7 days later + } + 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; + // 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) { + debugln(DEBUG, "parsePosix entered"); + + 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; + char code0; + char code1; + char code2; + for (int i = 0; i < str.length(); i++) { + c = str.charAt(i); + if (c == ' ') { + encodedString += '+'; + } else if (isalnum(c)) { + encodedString += c; + } else { + code1 = (c & 0xf)+'0'; + if ((c & 0xf) >9){ + code1 = (c & 0xf) - 10 + 'A'; + } + c = (c >> 4) & 0xf; + code0 = c + '0'; + if (c > 9) { + code0 = c - 10 + 'A'; + } + encodedString += '%'; + encodedString += code0; + encodedString += code1; + } + } + return encodedString; +} + +void EZtime::clearCache() { + Preferences preferences; + preferences.begin("ezTime", false); // read-write + preferences.clear(); + preferences.end(); +} + + +bool EZtime::timezoneAPI(String location, String &olsen, String &posix) { + + if (!WiFi.isConnected()) { error(NO_NETWORK); return false; } + + 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"; + } + + 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"); + + // 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; + } +} + +String EZtime::zeropad(uint32_t number, uint8_t length) { + String 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); +} + +EZtime ezTime; + + + + + + +// +// Timezone class +// + +Timezone::Timezone(bool locked_to_UTC /* = false */) { + _locked_to_UTC = locked_to_UTC; + _tzdata.std_tzname = "UTC"; + _tzdata.std_offset = 0; +} + +bool Timezone::setPosix(String posix) { + _tzdata = ezTime.parsePosix(posix, UTC.year()); + _olsen = ""; // Might be manually set, so delete _olsen as to not suggest a link +} + +String Timezone::getPosix() { return _posix;} + +bool Timezone::setLocation(String location, bool force_lookup /* = false */) { + + ezTime.debugln(INFO, "Timezone lookup for: " + location); + + // Cache strings: "location:olsen:year:posix" (location can be same as olsen) + + 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; + } + } + } + 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; + } + } + + // 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; + } + + 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 ""; + 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; + } else { + ezTime.error(ezTime.error()); // This throws the last error (as generated by timezoneAPI) again. + return false; + } + } +} + +String Timezone::getOlsen() { return _olsen;} + +void Timezone::setDefault() { + defaultTZ = this; +} + +bool Timezone::isDST() { + ezTime.debugln(DEBUG, "isDST entered"); + 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 + } +} + +bool Timezone::isDST_UTC(time_t t /*= TIME_NOW */) { + ezTime.debugln(DEBUG, "isDST_UTC entered"); + if (t == TIME_NOW) t = ezTime.now(); + + tzData_t tz; + if (year(t) != year()) { + tz = ezTime.parsePosix(_posix, year(t)); + } else { + tz = _tzdata; + } + + 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 + } +} + +bool Timezone::isDST_local(time_t t /*= TIME_NOW */) { + ezTime.debugln(DEBUG, "isDST_local entered"); + if (t == TIME_NOW) return isDST_UTC(TIME_NOW); //Prevent loops where timezone's now() tries to find offset + + 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 + } +} + + +String Timezone::getTimezoneName(time_t t /*= TIME_NOW */) { + if (isDST_local(t)) { + return _tzdata.dst_tzname; + } else { + return _tzdata.std_tzname; + } +} + +int32_t Timezone::getOffset(time_t t /*= TIME_NOW */) { + ezTime.debugln(DEBUG, "getOffset entered"); + + if (isDST_local(t)) { + return _tzdata.dst_offset; + } else { + return _tzdata.std_offset; + } +} + +time_t Timezone::now(bool update_last_read /* = true */) { + ezTime.debugln(DEBUG, "TZ's now() entered"); + 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; +} + +time_t Timezone::_readTime(time_t t) { + ezTime.debugln(DEBUG, "_readTime entered"); + switch (t) { + case TIME_NOW: return now(); + case LAST_READ: return _last_read_t; + default: return (t); + } +} + +void Timezone::setTime(time_t t) { + ezTime.debugln(DEBUG, "setTime entered"); + t += getOffset(t); + ezTime._last_sync_time = t; + ezTime._last_sync_millis = millis(); +} + +void Timezone::setTime(const uint8_t hr, const uint8_t min, const uint8_t sec, const uint8_t day, const uint8_t mnth, uint16_t yr) { + ezTime.debugln(DEBUG, "setTime entered"); + tmElements_t tm; + // year can be given as full four digit year or two digts (2010 or 10 for 2010); + // it is converted to years since 1970 + if( yr > 99) { + yr = yr - 1970; + } else { + yr += 30; + } + tm.Year = yr; + tm.Month = mnth; + tm.Day = day; + tm.Hour = hr; + tm.Minute = min; + tm.Second = sec; + setTime(ezTime.makeTime(tm)); +} + +String Timezone::dateTime(String format /* = DEFAULT_TIMEFORMAT */) { + return dateTime(TIME_NOW, format); +} + +String Timezone::dateTime(time_t t, String format /* = DEFAULT_TIMEFORMAT */) { + ezTime.debugln(DEBUG, "dateTime entered"); + t = _readTime(t); + + String tmpstr; + uint8_t tmpint8; + String out = ""; + + tmElements_t tm; + ezTime.breakTime(t, tm); + + int8_t hour12 = tm.Hour % 12; + if (hour12 == 0) hour12 = 12; + + int32_t o; + + bool escape_char = false; + + for (int8_t n = 0; n < format.length(); n++) { + + char c = (char) format.substring(n, n + 1).c_str()[0]; + + if (escape_char) { + out += String(c); + escape_char = false; + } else { + + switch (c) { + + case '\\': // Escape character, ignore this one, and let next through as literal character + case '~': // Same but easier without all the double escaping + escape_char = true; + break; + case 'd': // Day of the month, 2 digits with leading zeros + out += ezTime.zeropad(tm.Day, 2); + break; + case 'D': // A textual representation of a day, three letters + tmpstr = english_days[tm.Wday - 1]; + out += tmpstr.substring(0,3); + break; + case 'j': // Day of the month without leading zeros + out += String(tm.Day); + break; + case 'l': // (lowercase L) A full textual representation of the day of the week + out += english_days[tm.Wday - 1]; + break; + case 'N': // ISO-8601 numeric representation of the day of the week. ( 1 = Monday, 7 = Sunday ) + tmpint8 = tm.Wday - 1; + if (tmpint8 == 0) tmpint8 = 7; + out += String(tmpint8); + break; + case 'S': // English ordinal suffix for the day of the month, 2 characters (st, nd, rd, th) + switch (tm.Day) { + case 1: + case 21: + case 31: + out += "st"; break; + case 2: + case 22: + out += "nd"; break; + case 3: + case 23: + out += "rd"; break; + default: + out += "th"; break; + } + break; + case 'w': // Numeric representation of the day of the week ( 0 = Sunday ) + out += String(tm.Wday); + break; + case 'F': // A full textual representation of a month, such as January or March + out += english_months[tm.Month - 1]; + break; + case 'm': // Numeric representation of a month, with leading zeros + out += ezTime.zeropad(tm.Month, 2); + break; + case 'M': // A short textual representation of a month, three letters + tmpstr = english_months[tm.Month - 1]; + out += tmpstr.substring(0,3); + break; + case 'n': // Numeric representation of a month, without leading zeros + out += String(tm.Month); + break; + case 't': // Number of days in the given month + out += String(monthDays[tm.Month - 1]); + break; + case 'Y': // A full numeric representation of a year, 4 digits + out += String(tm.Year + 1970); + break; + case 'y': // A two digit representation of a year + out += ezTime.zeropad((tm.Year + 1970) % 100, 2); + break; + case 'a': // am or pm + out += (tm.Hour < 12) ? "am" : "pm"; + break; + case 'A': // AM or PM + out += (tm.Hour < 12) ? "AM" : "PM"; + break; + case 'g': // 12-hour format of an hour without leading zeros + out += String(hour12); + break; + case 'G': // 24-hour format of an hour without leading zeros + out += String(tm.Hour); + break; + case 'h': // 12-hour format of an hour with leading zeros + out += ezTime.zeropad(hour12, 2); + break; + case 'H': // 24-hour format of an hour with leading zeros + out += ezTime.zeropad(tm.Hour, 2); + break; + case 'i': // Minutes with leading zeros + out += ezTime.zeropad(tm.Minute, 2); + break; + case 's': // Seconds with leading zeros + out += ezTime.zeropad(tm.Second, 2); + break; + case 'T': // abbreviation for timezone + out += getTimezoneName(LAST_READ); + 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 '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) ? "+" : "-"; + if (o < 0) o = 0 - o; + out += ezTime.zeropad(o / 3600, 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)); + 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 + + } + } + } + + return out; +} + +uint8_t Timezone::hour(time_t t /* = TIME_NOW */) { + t = _readTime(t); + return t / 3600 % 24; +} + +uint8_t Timezone::minute(time_t t /*= TIME_NOW */) { + t = _readTime(t); + return t / 60 % 60; +} + +uint8_t Timezone::second(time_t t /* = TIME_NOW */) { + t = _readTime(t); + return t % 60; +} + +uint16_t Timezone::ms(time_t t /* = TIME_NOW */) { + // Note that here passing anything but TIME_NOW or LAST_READ is pointless + t = _readTime(t); + return ezTime._last_read_ms; +} + +uint8_t Timezone::day(time_t t /* = TIME_NOW */) { + tmElements_t tm; + ezTime.breakTime(t, tm); + return tm.Day; +} + +uint8_t Timezone::weekday(time_t t /* = TIME_NOW */) { + t = _readTime(t); + tmElements_t tm; + ezTime.breakTime(t, tm); + return tm.Wday; +} + +uint8_t Timezone::month(time_t t /* = TIME_NOW */) { + t = _readTime(t); + tmElements_t tm; + ezTime.breakTime(t, tm); + return tm.Month; +} + +uint16_t Timezone::year(time_t t /* = TIME_NOW */) { + t = _readTime(t); + 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; +} + +bool Timezone::minuteChanged() { + time_t t = now(false); + if (_last_read_t / 60 != t / 60) return true; + return false; +} + +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 english_months[month - 1]; } +String monthShortStr(const uint8_t month) { String tmp = english_months[month - 1]; return tmp.substring(0,3); } +String dayStr(const uint8_t day) { return english_days[day - 1]; } +String dayShortStr(const uint8_t day) { String tmp = english_days[day - 1]; 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) { ezTime.makeTime(tm); } +time_t makeTime(uint8_t hour, uint8_t minute, uint8_t second, uint8_t day, uint8_t month, int16_t year) { ezTime.makeTime(hour, minute, second, day, month, year); } + +timeStatus_t timeStatus() { ezTime.timeStatus(); } + +#endif //ARDUINO_TIMELIB_COMPATIBILITY \ No newline at end of file diff --git a/src/ezTime.h b/src/ezTime.h new file mode 100644 index 0000000..73fd709 --- /dev/null +++ b/src/ezTime.h @@ -0,0 +1,250 @@ +#ifndef _EZTIME_H_ +#define _EZTIME_H_ + +#include +#include +#include +#include + + +////////// Error handing + +typedef enum { + NO_ERROR, + LAST_ERROR, // Pseudo error: replaced by last error + NO_NETWORK, + TIMEOUT, + CONNECT_FAILED, + DATA_NOT_FOUND +} ezError_t; + +typedef enum { + NONE, + ERROR, + INFO, + DEBUG +} ezDebugLevel_t; + +//////////////////////// + + +#define LEAP_YEAR(Y) ( ((1970+Y)>0) && !((1970+Y)%4) && ( ((1970+Y)%100) || !((1970+Y)%400) ) ) +#define SECS_PER_DAY (86400UL) + +typedef struct { + uint8_t Second; + uint8_t Minute; + uint8_t Hour; + uint8_t Wday; // day of week, sunday is day 1 + uint8_t Day; + uint8_t Month; + uint8_t Year; // offset from 1970; +} tmElements_t; + +typedef enum { + timeNotSet, + timeNeedsSync, + timeSet +} 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; + +#define ARDUINO_TIMELIB_COMPATIBILITY + +#define TIME_NOW 0xFFFFFFFF +#define LAST_READ 0xFFFFFFFE +#define NTP_PACKET_SIZE 48 +#define NTP_LOCAL_PORT 2342 +#define NTP_SERVER "pool.ntp.org" +#define NTP_TIMEOUT 1500 // milliseconds +#define UPDATE_INTERVAL 600 // default update interval in seconds + +// Various date-time formats +#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 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 + +static const char * english_months[] PROGMEM = { "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" }; +static const char * english_days[] PROGMEM = { "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday" , "Saturday" }; +static 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 + +class EZtime { + + friend class Timezone; + + ////////// 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 + 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; + /////////// + + public: + EZtime(); + 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 + timeStatus_t timeStatus(); + 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(); + time_t now(); + void clearCache(); + 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); + + private: + uint16_t _update_interval; // in seconds + tzData_t parsePosix(String posix, int16_t year); + time_t _last_sync_time, _update_due; + unsigned long _last_sync_millis; + bool _debug_enabled, _ntp_enabled; + uint16_t _ntp_local_port, _ntp_interval, _ntp_max_drift, _last_read_ms; + String _ntp_server; + timeStatus_t _time_status; + + ////////// 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); + ////////// + +}; + +extern EZtime ezTime; + + +class Timezone { + + public: + Timezone(bool locked_to_UTC = false); + bool setPosix(String posix); + String getPosix(); + bool setLocation(String location, bool force_lookup = false); + String getOlsen(); + 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); + 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(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(); + + private: + time_t _readTime(time_t t); + bool _locked_to_UTC; + tzData_t _tzdata; + String _posix, _olsen; + time_t _last_read_t; + +}; + +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 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 + + +#endif //_EZTIME_H_ \ No newline at end of file