From 4b1ffe53797f9e96c6605c1ab2f89d55c82bb6a4 Mon Sep 17 00:00:00 2001 From: Rop Date: Tue, 25 Sep 2018 17:13:33 +0200 Subject: [PATCH] 0.7.4: timezoneapi.io -> timezoned.rop.nl also added EthernetShield example --- README.md | 8 +- examples/EthernetShield/EthernetShield.ino | 88 ++++++++++++++ examples/Timezones/Timezones.ino | 20 ++- library.json | 6 +- library.properties | 2 +- src/ezTime.cpp | 135 +++++++++------------ src/ezTime.h | 40 +++--- 7 files changed, 194 insertions(+), 105 deletions(-) create mode 100644 examples/EthernetShield/EthernetShield.ino diff --git a/README.md b/README.md index 4941022..e8136ef 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@   +> **Newsflash**: *To find the timezone information, ezTime originally used timezoneapi.io, which could be used for free for up to 50 queries a day. They have changed this policy and forced the use of https, both breaking ezTime. As of version 0.7.4, ezTime makes use of its very own online timezone lookup daemon, removing a dependency on some third party that might change their policy just like timezoneapi did. Please see details for [*`setLocation`*](#setlocation) because the interface changed a little. You can now also do GeoIP lookups for automatic local time setting (only in countries which do not span multiple timezones).* + ## A brief history of ezTime I was working on [M5ez](https://github.com/ropg/M5ez), an interface library to easily make cool-looking programs for the "[M5Stack](http://m5stack.com/)" ESP32 hardware. The status bar of M5ez needed to display the time. That was all, I swear. I figured I would use [Time](https://github.com/PaulStoffregen/Time), Michael Margolis' and 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. @@ -368,7 +370,11 @@ Provide the offset from UTC in minutes at the indicated time (or now if you do n `boolsetLocation(String location = "")`    — **MUST** be prefixed with name of a timezone -With `setLocation` you can provide a string to do an internet lookup for a timezone. If the string contains a forward slash, the string is taken to be on Olsen timezone name, like `Europe/Berlin`. If it does not, it is parsed as a free form address, for which the system will try to find a timezone. You can enter "Paris" and get the info for "Europe/Paris", or enter "Paris, Texas" and get the timezone info for "America/Chicago", which is the Central Time timezone that Texas is in. After the information is retrieved, it is loaded in the current timezone, and cached if a cache is set (see below). `setLocation` will return `false` (Setting either `NO_NETWORK`, `CONNECT_FAILED` or `DATA_NOT_FOUND`) if it cannot find the information online. +With `setLocation` you can provide a string to do an internet lookup for a timezone. The string can either be an Olsen timezone name, like `Europe/Berlin` (case-sensitive). ([Here](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) is a complete list of these names.) Or it can be a two-letter country code for any country that does not span multiple timezones, like `NL` or `DE` (but not `US`). After the information is retrieved, it is loaded in the current timezone, and cached if a cache is set (see below). `setLocation` will return `false` (Setting either `NO_NETWORK`, `DATA_NOT_FOUND` or `SERVER_ERROR`) if it cannot get timezone information. + +If you provide no location ( `YourTZ.setLocation()` ), ezTime will attempt to do a GeoIP lookup fo find the country associated with your IP-address. If that is a country that has a single timezone, that timezone will be loaded, otherwise a `SERVER_ERROR` ("Country Spans Multiple Timezones") will result. + +In the case of `SERVER_ERROR`, `errorString()` returns the error from the server, which might be "Country Spans Multiple Timezones", "Country Not Found", "GeoIP Lookup Failed" or "Timezone Not Found".   diff --git a/examples/EthernetShield/EthernetShield.ino b/examples/EthernetShield/EthernetShield.ino new file mode 100644 index 0000000..d7ce1f4 --- /dev/null +++ b/examples/EthernetShield/EthernetShield.ino @@ -0,0 +1,88 @@ +/* + * Note: to use an ethernet shield, You must also set #define EZTIME_ETHERNET in $sketch_dir/libraries/ezTime/src/ezTime.h + * + * Also note that all ezTime examples can be used with an Ethernet shield if you just replace the beginning of the sketch + * with the beginning of this one. + */ + +#include + +#include + +// Enter a MAC address for your controller below. (Or use address below, just make sure it's unique on your network) +// Newer Ethernet shields have a MAC address printed on a sticker on the shield +#define MAC_ADDRESS { 0xBA, 0xDB, 0xAD, 0xC0, 0xFF, 0xEE } + +void setup() { + + // Open serial communications and wait for port to open: + Serial.begin(115200); + while (!Serial) { ; } // wait for serial port to connect. Needed for native USB port only + Serial.println(); + + // You can use Ethernet.init(pin) to configure the CS pin + //Ethernet.init(10); // Most Arduino shields (default if unspecified) + //Ethernet.init(5); // MKR ETH shield + //Ethernet.init(0); // Teensy 2.0 + //Ethernet.init(20); // Teensy++ 2.0 + //Ethernet.init(15); // ESP8266 with Adafruit Featherwing Ethernet + //Ethernet.init(33); // ESP32 with Adafruit Featherwing Ethernet + + Serial.print(F("Ethernet connection ... ")); + byte mac [] = MAC_ADDRESS; + if (Ethernet.begin(mac) == 0) { + Serial.println(F("failed. (Reset to retry.)")); + while (true) { ; }; // Hang + } else { + Serial.print(F("got DHCP IP: ")); + Serial.println(Ethernet.localIP()); + } + // give the Ethernet shield a second to initialize: + delay(1000); + + // OK, we're online... So the part above here is what you swap in before the waitForSync() in the other examples... + + + // Wait for ezTime to get its time synchronized + waitForSync(); + + Serial.println(); + Serial.println("UTC: " + UTC.dateTime()); + + Timezone myTZ; + + // Provide official timezone names + // https://en.wikipedia.org/wiki/List_of_tz_database_time_zones + myTZ.setLocation(F("Pacific/Auckland")); + Serial.print(F("New Zealand: ")); + Serial.println(myTZ.dateTime()); + + // Or country codes for countries that do not span multiple timezones + myTZ.setLocation(F("de")); + Serial.print(F("Germany: ")); + Serial.println(myTZ.dateTime()); + + // See if local time can be obtained (does not work in countries that span multiple timezones) + Serial.print(F("Local (GeoIP): ")); + if (myTZ.setLocation()) { + Serial.println(myTZ.dateTime()); + } else { + Serial.println(errorString()); + } + + Serial.println(); + Serial.println(F("Now ezTime will show an NTP sync every 60 seconds")); + + // Set NTP polling interval to 60 seconds. Way too often, but good for demonstration purposes. + setInterval(60); + + // Make ezTime show us what it is doing + setDebug(INFO); + +} + +void loop() { + + events(); + +} diff --git a/examples/Timezones/Timezones.ino b/examples/Timezones/Timezones.ino index fffbc01..cac2455 100644 --- a/examples/Timezones/Timezones.ino +++ b/examples/Timezones/Timezones.ino @@ -17,10 +17,24 @@ void setup() { Timezone myTZ; - // Anything with a slash in it is interpreted as an official timezone name + // Provide official timezone names // https://en.wikipedia.org/wiki/List_of_tz_database_time_zones - myTZ.setLocation("Pacific/Auckland"); - Serial.println("Auckland: " + myTZ.dateTime()); + myTZ.setLocation(F("Pacific/Auckland")); + Serial.print(F("New Zealand: ")); + Serial.println(myTZ.dateTime()); + + // Or country codes for countries that do not span multiple timezones + mtTZ.setLocation(F("de")); + Serial.print(F("Germany: ")); + Serial.println(myTZ.dateTime()); + + // See if local time can be obtained (does not work in countries that span multiple timezones) + Serial.print(F("Local (GeoIP): ")); + if (myTZ.setLocation()) { + Serial.println(myTZ.dateTime()); + } else { + Serial.println(errorString()); + } } diff --git a/library.json b/library.json index 791c64b..9c2fbf7 100644 --- a/library.json +++ b/library.json @@ -4,16 +4,16 @@ "keywords": "time date ntp timezone events milliseconds", "authors": { "name": "Rop Gonggrijp", - "url": "https://github.com/ropg" + "url": "https://github.com/ropg", "maintainer": true }, "repository": { "type": "git", "url": "https://github.com/ropg/ezTime" }, - "version": "0.7.3", + "version": "0.7.4", "framework": "arduino", - "platforms": "*" + "platforms": "*", "build": { "libArchive": false } diff --git a/library.properties b/library.properties index 61f0056..2db06ee 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=ezTime -version=0.7.3 +version=0.7.4 author=Rop Gonggrijp maintainer=Rop Gonggrijp sentence=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, user events, millisecond precision and more. diff --git a/src/ezTime.cpp b/src/ezTime.cpp index 1575263..06dd18d 100644 --- a/src/ezTime.cpp +++ b/src/ezTime.cpp @@ -22,6 +22,7 @@ #include #else #include + #include #endif #endif @@ -62,6 +63,7 @@ const uint8_t monthDays[]={31,28,31,30,31,30,31,31,30,31,30,31}; // API starts m namespace { ezError_t _last_error = NO_ERROR; + String _server_error = ""; ezDebugLevel_t _debug_level = NONE; Print *_debug_device = (Print *)&Serial; ezEvent_t _events[MAX_EVENTS]; @@ -123,6 +125,7 @@ String errorString(const ezError_t err /* = LAST_ERROR */) { case NO_CACHE_SET: return F("No cache set"); case CACHE_TOO_SMALL: return F("Cache too small"); case TOO_MANY_EVENTS: return F("Too many events"); + case SERVER_ERROR: return _server_error; default: return F("Unkown error"); } } @@ -458,16 +461,14 @@ bool minuteChanged() { info(F(" ... ")); #ifndef EZTIME_ETHERNET - #ifndef ARDUINO_SAMD_MKR1000 - if (!WiFi.isConnected()) { error(NO_NETWORK); return false; } - #endif + if (WiFi.status() != WL_CONNECTED) { error(NO_NETWORK); return false; } WiFiUDP udp; #else EthernetUDP udp; #endif udp.flush(); - udp.begin(NTP_LOCAL_TIME_PORT); + udp.begin(NTP_LOCAL_PORT); // Send NTP packet byte buffer[NTP_PACKET_SIZE]; @@ -526,11 +527,12 @@ bool minuteChanged() { unsigned long start = millis(); - #if !defined(EZTIME_ETHERNET) && !defined(ARDUINO_SAMD_MKR1000) - if (!WiFi.isConnected()) { + #if !defined(EZTIME_ETHERNET) + if (WiFi.status() != WL_CONNECTED) { info(F("Waiting for WiFi ... ")); - while (!WiFi.isConnected()) { + while (WiFi.status() != WL_CONNECTED) { if ( timeout && (millis() - start) / 1000 > timeout ) { error(TIMEOUT); return false;}; + events(); delay(25); } infoln(F("connected")); @@ -797,92 +799,65 @@ String Timezone::getPosix() { return _posix; } #ifdef EZTIME_NETWORK_ENABLE - bool Timezone::setLocation(const String location /* = "" */) { + bool Timezone::setLocation(const String location /* = "GeoIP" */) { info(F("Timezone lookup for: ")); infoln(location); if (_locked_to_UTC) { error(LOCKED_TO_UTC); return false; } - #if !defined(EZTIME_ETHERNET) && !defined(ARDUINO_SAMD_MKR1000) - if (!WiFi.isConnected()) { error(NO_NETWORK); return false; } - #endif - - String path; - if (location.indexOf("/") != -1) { - path = F("/api/timezone/?"); path += urlEncode(location); - } else if (location != "") { - path = F("/api/address/?"); path += urlEncode(location); - } else { - path = F("/api/ip"); - } - #ifndef EZTIME_ETHERNET - WiFiClient client; + if (WiFi.status() != WL_CONNECTED) { error(NO_NETWORK); return false; } + WiFiUDP udp; #else - EthernetClient client; - #endif - - if (!client.connect("timezoneapi.io", 80)) { 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); + EthernetUDP udp; + #endif + + udp.flush(); + udp.begin(TIMEZONED_LOCAL_PORT); - debug(F("Sent request for http://timezoneapi.io")); debugln(path); - debugln(F("Reply from server:\r\n")); + udp.beginPacket(TIMEZONED_REMOTE_HOST, TIMEZONED_REMOTE_PORT); + udp.write((const uint8_t*)location.c_str(), location.length()); + udp.endPacket(); - // 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; - } - } + // 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 > TIMEZONED_TIMEOUT) { + udp.stop(); + error(TIMEOUT); + udp.stop(); + return false; } } - debugln(F("\r\n\r\n")); - if (search_state != 4 || tzinfo == "") { error(DATA_NOT_FOUND); return false; } - - infoln(F("success.")); - info(F("Found: ")); infoln(tzinfo); - - _olsen = tzinfo.substring(0, tzinfo.indexOf(' ')); - _posix = tzinfo.substring(tzinfo.indexOf(' ') + 1); - - #if defined(EZTIME_CACHE_EEPROM) || defined(EZTIME_CACHE_NVS) - writeCache(tzinfo); // caution, byref to save memory, tzinfo mangled afterwards - #endif - - return true; + // Stick result in String recv + String recv; + recv.reserve(60); + while (udp.available()) recv += (char)udp.read(); + udp.stop(); + if (recv.substring(0,6) == "ERROR ") { + _server_error = recv.substring(6); + error (SERVER_ERROR); + return false; + } + if (recv.substring(0,3) == "OK ") { + _olsen = recv.substring(3, recv.indexOf(" ", 4)); + _posix = recv.substring(recv.indexOf(" ", 4) + 1); + infoln(F("success.")); + info(F(" Olsen: ")); infoln(_olsen); + info(F(" Posix: ")); infoln(_posix); + #if defined(EZTIME_CACHE_EEPROM) || defined(EZTIME_CACHE_NVS) + String tzinfo = _olsen + " " + _posix; + writeCache(tzinfo); // caution, byref to save memory, tzinfo mangled afterwards + #endif + return true; + } + error (DATA_NOT_FOUND); + return false; } + String Timezone::getOlsen() { return _olsen; } diff --git a/src/ezTime.h b/src/ezTime.h index 545c3cc..97a9baf 100644 --- a/src/ezTime.h +++ b/src/ezTime.h @@ -1,3 +1,5 @@ +/* Extensive API documentation is at https://github.com/ropg/ezTime */ + #ifndef _EZTIME_H_ #ifdef __cplusplus #define _EZTIME_H_ @@ -57,7 +59,8 @@ typedef enum { LOCKED_TO_UTC, NO_CACHE_SET, CACHE_TOO_SMALL, - TOO_MANY_EVENTS + TOO_MANY_EVENTS, + SERVER_ERROR } ezError_t; typedef enum { @@ -135,14 +138,17 @@ typedef struct { #define LAST_READ (int32_t)0x7FFFFFFE // (So yes, ezTime might malfunction two seconds before everything else...) #define NTP_PACKET_SIZE 48 -#define NTP_LOCAL_TIME_PORT 2342 +#define NTP_LOCAL_PORT 4242 #define NTP_SERVER "pool.ntp.org" #define NTP_TIMEOUT 1500 // milliseconds #define NTP_INTERVAL 600 // default update interval in seconds #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 TIMEZONED_REMOTE_HOST "timezoned.rop.nl" +#define TIMEZONED_REMOTE_PORT 2342 +#define TIMEZONED_LOCAL_PORT 2342 +#define TIMEZONED_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 @@ -204,41 +210,41 @@ class Timezone { String dateTime(const String format = DEFAULT_TIMEFORMAT); String dateTime(time_t t, const String format = DEFAULT_TIMEFORMAT); String dateTime(time_t t, const ezLocalOrUTC_t local_or_utc, const String format = DEFAULT_TIMEFORMAT); - uint8_t day(time_t t = TIME_NOW, const ezLocalOrUTC_t local_or_utc = LOCAL_TIME); // 1-31 - uint16_t dayOfYear(time_t t = TIME_NOW, const ezLocalOrUTC_t local_or_utc = LOCAL_TIME); // days from start of year, jan 1st = 0 + uint8_t day(time_t t = TIME_NOW, const ezLocalOrUTC_t local_or_utc = LOCAL_TIME); + uint16_t dayOfYear(time_t t = TIME_NOW, const ezLocalOrUTC_t local_or_utc = LOCAL_TIME); int16_t getOffset(time_t t = TIME_NOW, const ezLocalOrUTC_t local_or_utc = LOCAL_TIME); String getPosix(); String getTimezoneName(time_t t = TIME_NOW, const ezLocalOrUTC_t local_or_utc = LOCAL_TIME); - uint8_t hour(time_t t = TIME_NOW, const ezLocalOrUTC_t local_or_utc = LOCAL_TIME); // 0-23 - uint8_t hourFormat12(time_t t = TIME_NOW, const ezLocalOrUTC_t local_or_utc = LOCAL_TIME); // 1-12 + uint8_t hour(time_t t = TIME_NOW, const ezLocalOrUTC_t local_or_utc = LOCAL_TIME); + uint8_t hourFormat12(time_t t = TIME_NOW, const ezLocalOrUTC_t local_or_utc = LOCAL_TIME); bool isAM(time_t t = TIME_NOW, const ezLocalOrUTC_t local_or_utc = LOCAL_TIME); bool isDST(time_t t = TIME_NOW, const ezLocalOrUTC_t local_or_utc = LOCAL_TIME); bool isPM(time_t t = TIME_NOW, const ezLocalOrUTC_t local_or_utc = LOCAL_TIME); String militaryTZ(time_t t = TIME_NOW, const ezLocalOrUTC_t local_or_utc = LOCAL_TIME); - uint8_t minute(time_t t = TIME_NOW, const ezLocalOrUTC_t local_or_utc = LOCAL_TIME); // 0-59 - uint8_t month(time_t t = TIME_NOW, const ezLocalOrUTC_t local_or_utc = LOCAL_TIME); // 1-12 - uint16_t ms(time_t t = TIME_NOW); // 0-999 + uint8_t minute(time_t t = TIME_NOW, const ezLocalOrUTC_t local_or_utc = LOCAL_TIME); + uint8_t month(time_t t = TIME_NOW, const ezLocalOrUTC_t local_or_utc = LOCAL_TIME); + uint16_t ms(time_t t = TIME_NOW); time_t now(); - uint8_t second(time_t t = TIME_NOW, const ezLocalOrUTC_t local_or_utc = LOCAL_TIME); // 0-59 + uint8_t second(time_t t = TIME_NOW, const ezLocalOrUTC_t local_or_utc = LOCAL_TIME); void setDefault(); uint8_t setEvent(void (*function)(), const uint8_t hr, const uint8_t min, const uint8_t sec, const uint8_t day, const uint8_t mnth, uint16_t yr); uint8_t setEvent(void (*function)(), time_t t = TIME_NOW, const ezLocalOrUTC_t local_or_utc = LOCAL_TIME); bool setPosix(const String posix); - 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); void setTime(const time_t t, const 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); time_t tzTime(time_t t = TIME_NOW, ezLocalOrUTC_t local_or_utc = LOCAL_TIME); time_t tzTime(time_t t, ezLocalOrUTC_t local_or_utc, String &tzname, bool &is_dst, int16_t &offset); - uint8_t weekISO(time_t t = TIME_NOW, const ezLocalOrUTC_t local_or_utc = LOCAL_TIME); // ISO-8601 week number (weeks starting on Monday) - uint8_t weekday(time_t t = TIME_NOW, const ezLocalOrUTC_t local_or_utc = LOCAL_TIME); // Day of the week (1-7), Sunday is day 1 - uint16_t year(time_t t = TIME_NOW, const ezLocalOrUTC_t local_or_utc = LOCAL_TIME); // four digit year - uint16_t yearISO(time_t t = TIME_NOW, const ezLocalOrUTC_t local_or_utc = LOCAL_TIME); // ISO-8601 year, can differ from actual year, plus or minus one + uint8_t weekISO(time_t t = TIME_NOW, const ezLocalOrUTC_t local_or_utc = LOCAL_TIME); + uint8_t weekday(time_t t = TIME_NOW, const ezLocalOrUTC_t local_or_utc = LOCAL_TIME); + uint16_t year(time_t t = TIME_NOW, const ezLocalOrUTC_t local_or_utc = LOCAL_TIME); + uint16_t yearISO(time_t t = TIME_NOW, const ezLocalOrUTC_t local_or_utc = LOCAL_TIME); private: String _posix, _olsen; bool _locked_to_UTC; #ifdef EZTIME_NETWORK_ENABLE public: - bool setLocation(const String location = ""); + bool setLocation(const String location = "GeoIP"); String getOlsen(); #ifdef EZTIME_CACHE_EEPROM public: