diff --git a/ezTime.cpp b/ezTime.cpp new file mode 100644 index 0000000..5f52af1 --- /dev/null +++ b/ezTime.cpp @@ -0,0 +1,1508 @@ +#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(ARDUINO_SAMD_MKR1000) + #include + #include + #include + #elif defined(EZTIME_ETHERNET) + #include + #include + #include + #else + #include + #include + #endif +#endif + +#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 (_debug_level >= ERROR) _debug_device->print(args) + #define errln(args...) if (_debug_level >= ERROR) _debug_device->println(args) + #define info(args...) "" + #define infoln(args...) "" + #define debug(args...) "" + #define debugln(args...) "" +#elif defined(EZTIME_MAX_DEBUGLEVEL_INFO) + #define err(args...) if (_debug_level >= ERROR) _debug_device->print(args) + #define errln(args...) if (_debug_level >= ERROR) _debug_device->println(args) + #define info(args...) if (_debug_level >= INFO) _debug_device->print(args) + #define infoln(args...) if (_debug_level >= INFO) _debug_device->println(args) + #define debug(args...) "" + #define debugln(args...) "" +#else // nothing specified compiles everything in. + #define err(args...) if (_debug_level >= ERROR) _debug_device->print(args) + #define errln(args...) if (_debug_level >= ERROR) _debug_device->println(args) + #define info(args...) if (_debug_level >= INFO) _debug_device->print(args) + #define infoln(args...) if (_debug_level >= INFO) _debug_device->println(args) + #define debug(args...) if (_debug_level >= DEBUG) _debug_device->print(args) + #define debugln(args...) if (_debug_level >= DEBUG) _debug_device->println(args) +#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 + +// The private things go in an anonymous namespace +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]; + time_t _last_sync_time = 0; + time_t _last_read_t = 0; + uint32_t _last_sync_millis = 0; + uint16_t _last_read_ms; + timeStatus_t _time_status; + bool _initialised = false; + #ifdef EZTIME_NETWORK_ENABLE + uint16_t _ntp_interval = NTP_INTERVAL; + String _ntp_server = NTP_SERVER; + #endif + + void triggerError(const ezError_t err) { + _last_error = err; + if (_last_error) { + err(F("ERROR: ")); + errln(ezt::errorString(err)); + } + } + + String debugLevelString(const ezDebugLevel_t level) { + switch (level) { + case NONE: return F("NONE"); + case ERROR: return F("ERROR"); + case INFO: return F("INFO"); + default: return F("DEBUG"); + } + } + + time_t nowUTC(const bool update_last_read = true) { + time_t t; + 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; + } + return t; + } + +} + + + +namespace ezt { + + ////////// Error handing + + String errorString(const ezError_t err /* = LAST_ERROR */) { + switch (err) { + 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"); + case TOO_MANY_EVENTS: return F("Too many events"); + case SERVER_ERROR: return _server_error; + case INVALID_DATA: return F("Invalid data received from NTP server"); + default: return F("Unkown error"); + } + } + + ezError_t error(const bool reset /* = false */) { + ezError_t tmp = _last_error; + if (reset) _last_error = NO_ERROR; + return tmp; + } + + void setDebug(const ezDebugLevel_t level) { + setDebug(level, *_debug_device); + } + + void setDebug(const ezDebugLevel_t level, Print &device) { + _debug_level = level; + _debug_device = &device; + info(F("\r\nezTime debug level set to ")); + infoln(debugLevelString(level)); + } + + //////////////////////// + + String monthStr(const uint8_t month) { + switch(month) { + 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 dayStr(const uint8_t day) { + switch(day) { + 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 ""; + } + + // Original time lib compatibility + String dayShortStr(const uint8_t day) { return dayStr(day).substring(0,3); } + String monthShortStr(const uint8_t month) { return monthStr(month).substring(0,3); } + + + timeStatus_t timeStatus() { return _time_status; } + + void events() { + if (!_initialised) { + for (uint8_t n = 0; n < MAX_EVENTS; n++) _events[n] = { 0, NULL }; + #ifdef EZTIME_NETWORK_ENABLE + if (_ntp_interval) updateNTP(); // Start the cycle of updateNTP running and then setting an event for its next run + #endif + _initialised = true; + } + // See if any events are due + for (uint8_t n = 0; n < MAX_EVENTS; n++) { + if (_events[n].function && nowUTC(false) >= _events[n].time) { + debug(F("Running event (#")); debug(n + 1); debug(F(") set for ")); debugln(UTC.dateTime(_events[n].time)); + void (*tmp)() = _events[n].function; + _events[n] = { 0, NULL }; // reset the event + (tmp)(); // execute the function + } + } + yield(); + } + + void deleteEvent(const uint8_t event_handle) { + if (event_handle && event_handle <= MAX_EVENTS) { + debug(F("Deleted event (#")); debug(event_handle); debug(F("), set for ")); debugln(UTC.dateTime(_events[event_handle - 1].time)); + _events[event_handle - 1] = { 0, NULL }; + } + } + + void deleteEvent(void (*function)()) { + for (uint8_t n = 0; n< MAX_EVENTS; n++) { + if (_events[n].function == function) { + debug(F("Deleted event (#")); debug(n + 1); debug(F("), set for ")); debugln(UTC.dateTime(_events[n].time)); + _events[n] = { 0, NULL }; + } + } + } + + void breakTime(const 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 + // 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 makeTime(const uint8_t hour, const uint8_t minute, const uint8_t second, const uint8_t day, const uint8_t month, const uint16_t year) { + tmElements_t tm; + tm.Hour = hour; + tm.Minute = minute; + tm.Second = second; + tm.Day = day; + tm.Month = month; + 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); + } + + time_t makeTime(tmElements_t &tm){ + // 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 * SECS_PER_DAY * 365UL; + + for (i = 0; i < tm.Year; i++) { + if (LEAP_YEAR(i)) { + seconds += SECS_PER_DAY; // 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 * 29UL; + } else { + seconds += SECS_PER_DAY * (uint32_t)monthDays[i-1]; //monthDay array starts from 0 + } + } + + seconds+= (tm.Day-1) * SECS_PER_DAY; + seconds+= tm.Hour * 3600UL; + seconds+= tm.Minute * 60UL; + seconds+= tm.Second; + + return (time_t)seconds; + } + + // makeOrdinalTime 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 ordinal is 0 or 5 it is taken to mean "the last $wday in $month" + time_t makeOrdinalTime(const uint8_t hour, const uint8_t minute, uint8_t const second, uint8_t ordinal, const uint8_t wday, const uint8_t month, uint16_t year) { + if (year <= 68 ) year = 1970 + year; // fix user intent + uint8_t m = month; + uint8_t w = ordinal; + if (w == 5) ordinal = 0; + 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 - UTC.weekday(t) + 7) % 7 + (w - 1) * 7 ) * SECS_PER_DAY; + // back up a week if this is a "Last" rule + if (ordinal == 0) t -= 7 * SECS_PER_DAY; + return t; + } + + String zeropad(const uint32_t number, const uint8_t length) { + String out; + out.reserve(length); + out = String(number); + while (out.length() < length) out = "0" + out; + return out; + } + + time_t compileTime(const String compile_date /* = __DATE__ */, const String compile_time /* = __TIME__ */) { + + uint8_t hrs = compile_time.substring(0,2).toInt(); + uint8_t min = compile_time.substring(3,5).toInt(); + uint8_t sec = compile_time.substring(6).toInt(); + uint8_t day = compile_date.substring(4,6).toInt(); + int16_t year = compile_date.substring(7).toInt(); + String iterate_month; + for (uint8_t month = 1; month < 13; month++) { + iterate_month = monthStr(month); + if ( iterate_month.substring(0,3) == compile_date.substring(0,3) ) { + return makeTime(hrs, min, sec, day, month, year); + } + } + return 0; + } + + bool secondChanged() { + time_t t = nowUTC(false); + if (_last_read_t != t) return true; + return false; + } + + bool minuteChanged() { + time_t t = nowUTC(false); + if (_last_read_t / 60 != t / 60) return true; + return false; + } + + + #ifdef EZTIME_NETWORK_ENABLE + + void updateNTP() { + deleteEvent(updateNTP); // Delete any events pointing here, in case called manually + 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(t, 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(""); + } + if (_ntp_interval) UTC.setEvent(updateNTP, t + _ntp_interval); + _time_status = timeSet; + } else { + if ( nowUTC(false) > _last_sync_time + _ntp_interval + NTP_STALE_AFTER ) { + _time_status = timeNeedsSync; + } + UTC.setEvent(updateNTP, nowUTC(false) + NTP_RETRY); + } + } + + // 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 queryNTP(const String server, time_t &t, unsigned long &measured_at) { + info(F("Querying ")); + info(server); + info(F(" ... ")); + + #ifndef EZTIME_ETHERNET + if (WiFi.status() != WL_CONNECTED) { triggerError(NO_NETWORK); return false; } + WiFiUDP udp; + #else + EthernetUDP udp; + #endif + + // 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.flush(); + udp.begin(NTP_LOCAL_PORT); + unsigned long started = millis(); + udp.beginPacket(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 + while (!udp.parsePacket()) { + delay (1); + if (millis() - started > NTP_TIMEOUT) { + udp.stop(); + triggerError(TIMEOUT); + return false; + } + } + udp.read(buffer, NTP_PACKET_SIZE); + udp.stop(); // On AVR there's only very limited sockets, we want to free them when done. + + //print out received packet for debug + int i; + debug(F("Received data:")); + for (i = 0; i < NTP_PACKET_SIZE; i++) { + if ((i % 4) == 0) { + debugln(); + debug(String(i) + ": "); + } + debug(buffer[i], HEX); + debug(F(", ")); + } + debugln(); + + //prepare timestamps + uint32_t highWord, lowWord; + highWord = ( buffer[16] << 8 | buffer[17] ) & 0x0000FFFF; + lowWord = ( buffer[18] << 8 | buffer[19] ) & 0x0000FFFF; + uint32_t reftsSec = highWord << 16 | lowWord; // reference timestamp seconds + + highWord = ( buffer[32] << 8 | buffer[33] ) & 0x0000FFFF; + lowWord = ( buffer[34] << 8 | buffer[35] ) & 0x0000FFFF; + uint32_t rcvtsSec = highWord << 16 | lowWord; // receive timestamp seconds + + highWord = ( buffer[40] << 8 | buffer[41] ) & 0x0000FFFF; + lowWord = ( buffer[42] << 8 | buffer[43] ) & 0x0000FFFF; + uint32_t secsSince1900 = highWord << 16 | lowWord; // transmit timestamp seconds + + highWord = ( buffer[44] << 8 | buffer[45] ) & 0x0000FFFF; + lowWord = ( buffer[46] << 8 | buffer[47] ) & 0x0000FFFF; + uint32_t fraction = highWord << 16 | lowWord; // transmit timestamp fractions + + //check if received data makes sense + //buffer[1] = stratum - should be 1..15 for valid reply + //also checking that all timestamps are non-zero and receive timestamp seconds are <= transmit timestamp seconds + if ((buffer[1] < 1) or (buffer[1] > 15) or (reftsSec == 0) or (rcvtsSec == 0) or (rcvtsSec > secsSince1900)) { + // we got invalid packet + triggerError(INVALID_DATA); + return false; + } + + // 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 + 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 setInterval(const uint16_t seconds /* = 0 */) { + deleteEvent(updateNTP); + _ntp_interval = seconds; + if (seconds) UTC.setEvent(updateNTP, nowUTC(false) + _ntp_interval); + } + + void setServer(const String ntp_server /* = NTP_SERVER */) { _ntp_server = ntp_server; } + + bool waitForSync(const uint16_t timeout /* = 0 */) { + + unsigned long start = millis(); + + #if !defined(EZTIME_ETHERNET) + if (WiFi.status() != WL_CONNECTED) { + info(F("Waiting for WiFi ... ")); + while (WiFi.status() != WL_CONNECTED) { + if ( timeout && (millis() - start) / 1000 > timeout ) { triggerError(TIMEOUT); return false;}; + events(); + 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 ) { triggerError(TIMEOUT); return false;}; + delay(250); + events(); + } + infoln(F("Time is in sync")); + } + return true; + } + + #endif // EZTIME_NETWORK_ENABLE + +} + + +// +// Timezone class +// + +Timezone::Timezone(const bool locked_to_UTC /* = false */) { + _locked_to_UTC = locked_to_UTC; + _posix = "UTC"; + #ifdef EZTIME_NETWORK_ENABLE + #ifdef EZTIME_CACHE_EEPROM + _cache_month = 0; + _eeprom_address = -1; + #endif + #ifdef EZTIME_CACHE_NVS + _cache_month = 0; + _nvs_name = ""; + _nvs_key = ""; + #endif + _olson = ""; + #endif +} + +bool Timezone::setPosix(const String posix) { + if (_locked_to_UTC) { triggerError(LOCKED_TO_UTC); return false; } + _posix = posix; + #ifdef EZTIME_NETWORK_ENABLE + _olson = ""; + #endif + return true; +} + +time_t Timezone::now() { return tzTime(); } + +time_t Timezone::tzTime(time_t t /* = TIME_NOW */, ezLocalOrUTC_t local_or_utc /* = LOCAL_TIME */) { + if (_locked_to_UTC) return nowUTC(); // just saving some time and memory + String tzname; + bool is_dst; + int16_t offset; + return tzTime(t, local_or_utc, tzname, is_dst, offset); +} + +time_t Timezone::tzTime(time_t t, ezLocalOrUTC_t local_or_utc, String &tzname, bool &is_dst, int16_t &offset) { + + if (t == TIME_NOW) { + t = nowUTC(); + local_or_utc = UTC_TIME; + } else if (t == LAST_READ) { + t = _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; + + bool ignore_nums = false; + char c = 1; // Dummy value to get while(newchar) started + uint8_t strpos = 0; + uint8_t stdname_end = _posix.length() - 1; + uint8_t dstname_begin = _posix.length(); + uint8_t dstname_end = _posix.length(); + + while (strpos < _posix.length()) { + c = (char)_posix[strpos]; + + // 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; + stdname_end = strpos - 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; + 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; + dstname_end = strpos - 1; + } else if (!ignore_nums && (c == '-' || isDigit(c))) { + state = DST_SHIFT_HR; + 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; + + tzname = _posix.substring(0, stdname_end + 1); // Overwritten with dstname later if needed + if (!start_month) { + if (tzname == "UTC" && std_offset) tzname = "???"; + is_dst = false; + offset = std_offset; + return t - std_offset * 60; + } + + int16_t dst_offset = std_offset - dst_shift_hr * 60 - dst_shift_min; + // to find the year + tmElements_t tm; + ezt::breakTime(t, tm); + + // in local time + time_t dst_start = ezt::makeOrdinalTime(start_time_hr, start_time_min, 0, start_week, start_dow + 1, start_month, tm.Year + 1970); + time_t dst_end = ezt::makeOrdinalTime(end_time_hr, end_time_min, 0, end_week, end_dow + 1, end_month, tm.Year + 1970); + + if (local_or_utc == UTC_TIME) { + dst_start += std_offset * 60LL; + dst_end += dst_offset * 60LL; + } + + if (dst_end > dst_start) { + is_dst = (t >= dst_start && t < dst_end); // northern hemisphere + } else { + is_dst = !(t >= dst_end && t < dst_start); // southern hemisphere + } + + if (is_dst) { + offset = dst_offset; + tzname = _posix.substring(dstname_begin, dstname_end + 1); + } else { + offset = std_offset; + } + + if (local_or_utc == LOCAL_TIME) { + return t + offset * 60LL; + } else { + return t - offset * 60LL; + } +} + +String Timezone::getPosix() { return _posix; } + +#ifdef EZTIME_NETWORK_ENABLE + + bool Timezone::setLocation(const String location /* = "GeoIP" */) { + + info(F("Timezone lookup for: ")); + info(location); + info(F(" ... ")); + if (_locked_to_UTC) { triggerError(LOCKED_TO_UTC); return false; } + + #ifndef EZTIME_ETHERNET + if (WiFi.status() != WL_CONNECTED) { triggerError(NO_NETWORK); return false; } + WiFiUDP udp; + #else + EthernetUDP udp; + #endif + + udp.flush(); + udp.begin(TIMEZONED_LOCAL_PORT); + unsigned long started = millis(); + udp.beginPacket(TIMEZONED_REMOTE_HOST, TIMEZONED_REMOTE_PORT); + udp.write((const uint8_t*)location.c_str(), location.length()); + udp.endPacket(); + + // Wait for packet or return false with timed out + while (!udp.parsePacket()) { + delay (1); + if (millis() - started > TIMEZONED_TIMEOUT) { + udp.stop(); + triggerError(TIMEOUT); + return false; + } + } + // Stick result in String recv + String recv; + recv.reserve(60); + while (udp.available()) recv += (char)udp.read(); + udp.stop(); + info(F("(round-trip ")); + info(millis() - started); + info(F(" ms) ")); + if (recv.substring(0,6) == "ERROR ") { + _server_error = recv.substring(6); + error (SERVER_ERROR); + return false; + } + if (recv.substring(0,3) == "OK ") { + _olson = recv.substring(3, recv.indexOf(" ", 4)); + _posix = recv.substring(recv.indexOf(" ", 4) + 1); + infoln(F("success.")); + info(F(" Olson: ")); infoln(_olson); + info(F(" Posix: ")); infoln(_posix); + #if defined(EZTIME_CACHE_EEPROM) || defined(EZTIME_CACHE_NVS) + String tzinfo = _olson + " " + _posix; + writeCache(tzinfo); // caution, byref to save memory, tzinfo mangled afterwards + #endif + return true; + } + error (DATA_NOT_FOUND); + return false; + } + + + String Timezone::getOlson() { + return _olson; + } + + String Timezone::getOlsen() { + return _olson; + } + + + #if defined(EZTIME_CACHE_EEPROM) || defined(EZTIME_CACHE_NVS) + + #if defined(ESP32) || defined(ESP8266) + #define eepromBegin() EEPROM.begin(4096) + #define eepromEnd() EEPROM.end() + #define eepromLength() (4096) + #else + #define eepromBegin() "" + #define eepromEnd() "" + #define eepromLength() EEPROM.length() + #endif + + #ifdef EZTIME_CACHE_EEPROM + bool Timezone::setCache(const int16_t address) { + eepromBegin(); + if (address + EEPROM_CACHE_LEN > eepromLength()) { triggerError(CACHE_TOO_SMALL); return false; } + _eeprom_address = address; + eepromEnd(); + 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 + + bool Timezone::setCache() { + String olson, posix; + uint8_t months_since_jan_2018; + if (readCache(olson, posix, months_since_jan_2018)) { + setPosix(posix); + _olson = olson; + _cache_month = months_since_jan_2018; + if ( (year() - 2018) * 12 + month(LAST_READ) - months_since_jan_2018 > MAX_CACHE_AGE_MONTHS) { + infoln(F("Cache stale, getting fresh")); + setLocation(olson); + } + return true; + } + return false; + } + + void Timezone::clearCache(const bool delete_section /* = false */) { + + #ifdef EZTIME_CACHE_EEPROM + eepromBegin(); + if (_eeprom_address < 0) { triggerError(NO_CACHE_SET); return; } + for (int16_t n = _eeprom_address; n < _eeprom_address + EEPROM_CACHE_LEN; n++) EEPROM.write(n, 0); + eepromEnd(); + #endif + + #ifdef EZTIME_CACHE_NVS + if (_nvs_name == "" || _nvs_key == "") { triggerError(NO_CACHE_SET); return; } + Preferences prefs; + prefs.begin(_nvs_name.c_str(), false); + if (delete_section) { + prefs.clear(); + } else { + prefs.remove(_nvs_key.c_str()); + } + prefs.end(); + #endif + } + + bool Timezone::writeCache(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) { triggerError(CACHE_TOO_SMALL); return false; } + + uint16_t last_byte = _eeprom_address + EEPROM_CACHE_LEN - 1; + uint16_t addr = _eeprom_address; + + eepromBegin(); + + // 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); + eepromEnd(); + 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.c_str(), false); + String tmp = String(months_since_jan_2018) + " " + str; + prefs.putString(_nvs_key.c_str(), tmp); + prefs.end(); + return true; + #endif + } + + + bool Timezone::readCache(String &olson, String &posix, uint8_t &months_since_jan_2018) { + + #ifdef EZTIME_CACHE_EEPROM + if (_eeprom_address < 0) { triggerError(NO_CACHE_SET); return false; } + eepromBegin(); + 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)) { eepromEnd(); 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) { eepromEnd(); return false; } + + // OK, we're gonna decompress + olson.reserve(len + 3); // Everything goes in olson first. Decompression might overshoot 3 + months_since_jan_2018 = EEPROM.read(_eeprom_address); + + 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: + olson += (char)( ((c & 0b11111100) >> 2) + 32 ); + break; + case 1: + olson += (char)( ((p & 0b00000011) << 4) + ((c & 0b11110000) >> 4) + 32 ); + break; + case 2: + olson += (char)( ((p & 0b00001111) << 2) + ((c & 0b11000000) >> 6) + 32 ); + olson += (char)( (c & 0b00111111) + 32 ); + } + if (olson.length() >= len) break; + } + + uint8_t first_space = olson.indexOf(' '); + posix = olson.substring(first_space + 1, len); + olson = olson.substring(0, first_space); + + // Restore case of olson (best effort) + String olson_lowercase = olson; + olson_lowercase.toLowerCase(); + for (uint8_t n = 1; n < olson.length(); n++) { + unsigned char p = olson.charAt(n - 1); // previous character + if (p != '_' && p != '/' && p != '-') { + olson.setCharAt(n, olson_lowercase[n]); + } + } + info(F("Cache read. Olson: ")); info(olson); info (F(" Posix: ")); infoln(posix); + eepromEnd(); + return true; + #endif + + #ifdef EZTIME_CACHE_NVS + if (_nvs_name == "" || _nvs_key == "") { triggerError(NO_CACHE_SET); return false; } + + Preferences prefs; + prefs.begin(_nvs_name.c_str(), true); + String read_string = prefs.getString(_nvs_key.c_str()); + read_string.trim(); + prefs.end(); + if (read_string == "") return false; + + uint8_t first_space = read_string.indexOf(' '); + uint8_t second_space = read_string.indexOf(' ', first_space + 1); + if (first_space && second_space) { + months_since_jan_2018 = read_string.toInt(); + posix = read_string.substring(second_space + 1); + olson = read_string.substring(first_space + 1, second_space); + info(F("Cache read. Olson: ")); info(olson); info (F(" Posix: ")); infoln(posix); + return true; + } + return false; + #endif + } + + #endif // defined(EZTIME_CACHE_EEPROM) || defined(EZTIME_CACHE_NVS) + + +#endif // EZTIME_NETWORK_ENABLE + + +void Timezone::setDefault() { + defaultTZ = this; + debug(F("Default timezone set to ")); debug(_olson); debug(F(" "));debugln(_posix); +} + +bool Timezone::isDST(time_t t /*= TIME_NOW */, const ezLocalOrUTC_t local_or_utc /* = LOCAL_TIME */) { + String tzname; + bool is_dst; + int16_t offset; + t = tzTime(t, local_or_utc, tzname, is_dst, offset); + return is_dst; +} + +String Timezone::getTimezoneName(time_t t /*= TIME_NOW */, const ezLocalOrUTC_t local_or_utc /* = LOCAL_TIME */) { + String tzname; + bool is_dst; + int16_t offset; + t = tzTime(t, local_or_utc, tzname, is_dst, offset); + return tzname; +} + +int16_t Timezone::getOffset(time_t t /*= TIME_NOW */, const ezLocalOrUTC_t local_or_utc /* = LOCAL_TIME */) { + String tzname; + bool is_dst; + int16_t offset; + t = tzTime(t, local_or_utc, tzname, is_dst, offset); + return offset; +} + +uint8_t Timezone::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) { + time_t t = ezt::makeTime(hr, min, sec, day, mnth, yr); + return setEvent(function, t); +} + +uint8_t Timezone::setEvent(void (*function)(), time_t t /* = TIME_NOW */, const ezLocalOrUTC_t local_or_utc /* = LOCAL_TIME */) { + t = tzTime(t, local_or_utc); + for (uint8_t n = 0; n < MAX_EVENTS; n++) { + if (!_events[n].function) { + _events[n].function = function; + _events[n].time = t; + debug(F("Set event (#")); debug(n + 1); debug(F(") to trigger on: ")); debugln(UTC.dateTime(t)); + return n + 1; + } + } + triggerError(TOO_MANY_EVENTS); + return 0; +} + +void Timezone::setTime(const time_t t, const uint16_t ms /* = 0 */) { + int16_t offset; + offset = getOffset(t); + _last_sync_time = t + offset * 60; + _last_sync_millis = millis() - ms; + _time_status = timeSet; +} + +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) { + 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(ezt::makeTime(tm)); +} + +String Timezone::dateTime(const String format /* = DEFAULT_TIMEFORMAT */) { + return dateTime(TIME_NOW, format); +} + +String Timezone::dateTime(const time_t t, const String format /* = DEFAULT_TIMEFORMAT */) { + return dateTime(t, LOCAL_TIME, format); +} + +String Timezone::dateTime(time_t t, const ezLocalOrUTC_t local_or_utc, const String format /* = DEFAULT_TIMEFORMAT */) { + + String tzname; + bool is_dst; + int16_t offset; + t = tzTime(t, LOCAL_TIME, tzname, is_dst, offset); + + String tmpstr; + uint8_t tmpint8; + String out = ""; + + tmElements_t tm; + ezt::breakTime(t, tm); + + int8_t hour12 = tm.Hour % 12; + if (hour12 == 0) hour12 = 12; + + int32_t o; + + bool escape_char = false; + + for (uint8_t n = 0; n < format.length(); n++) { + + char c = format.charAt(n); + + 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 += ezt::zeropad(tm.Day, 2); + break; + case 'D': // A textual representation of a day, three letters + out += ezt::dayStr(tm.Wday).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 += ezt::dayStr(tm.Wday); + 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 += F("st"); break; + case 2: + case 22: + out += F("nd"); break; + case 3: + case 23: + out += F("rd"); break; + default: + out += F("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 += ezt::monthStr(tm.Month); + break; + case 'm': // Numeric representation of a month, with leading zeros + out += ezt::zeropad(tm.Month, 2); + break; + case 'M': // A short textual representation of a month, three letters + out += ezt::monthStr(tm.Month).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 += ezt::zeropad((tm.Year + 1970) % 100, 2); + break; + case 'a': // am or pm + out += (tm.Hour < 12) ? F("am") : F("pm"); + break; + case 'A': // AM or PM + out += (tm.Hour < 12) ? F("AM") : F("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 += ezt::zeropad(hour12, 2); + break; + case 'H': // 24-hour format of an hour with leading zeros + out += ezt::zeropad(tm.Hour, 2); + break; + case 'i': // Minutes with leading zeros + out += ezt::zeropad(tm.Minute, 2); + break; + case 's': // Seconds with leading zeros + out += ezt::zeropad(tm.Second, 2); + break; + case 'T': // abbreviation for timezone + out += tzname; + break; + case 'v': // milliseconds as three digits + out += ezt::zeropad(_last_read_ms, 3); + break; + #ifdef EZTIME_NETWORK_ENABLE + case 'e': // Timezone identifier (Olson) + out += getOlson(); + break; + #endif + 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 = offset; + out += (o < 0) ? "+" : "-"; // reversed from our offset + if (o < 0) o = 0 - o; + out += ezt::zeropad(o / 60, 2); + out += (c == 'P') ? ":" : ""; + out += ezt::zeropad(o % 60, 2); + break; + case 'Z': //Timezone offset in seconds. West of UTC is negative, east of UTC is positive. + out += String(0 - offset * 60); + break; + case 'z': + out += String(dayOfYear(t)); // The day of the year (starting from 0) + break; + case 'W': + out += ezt::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; + case 'B': + out += militaryTZ(t); + break; + default: + out += String(c); + + } + } + } + + return out; +} + +String Timezone::militaryTZ(time_t t /*= TIME_NOW */, const ezLocalOrUTC_t local_or_utc /* = LOCAL_TIME */) { + t = tzTime(t, local_or_utc); + int16_t o = getOffset(t); + if (o % 60) return "?"; // If it's not a whole hour from UTC, it's not a timezone with a military letter code + o = o / 60; + if (o > 0) return String((char)('M' + o)); + if (o < 0 && o >= -9) return String((char)('A' - o - 1)); // Minus a negative number == plus 1 + if (o < -9) return String((char)('A' - o)); // Crazy, they're skipping 'J' + return "Z"; +} + + +uint8_t Timezone::hour(time_t t /*= TIME_NOW */, const 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 */, const 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 */, const 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 */) { + // Note that here passing anything but TIME_NOW or LAST_READ is pointless + if (t == TIME_NOW) { nowUTC(); return _last_read_ms; } + if (t == LAST_READ) return _last_read_ms; + return 0; +} + +uint8_t Timezone::day(time_t t /*= TIME_NOW */, const ezLocalOrUTC_t local_or_utc /* = LOCAL_TIME */) { + t = tzTime(t, local_or_utc); + tmElements_t tm; + ezt::breakTime(t, tm); + return tm.Day; +} + +uint8_t Timezone::weekday(time_t t /*= TIME_NOW */, const ezLocalOrUTC_t local_or_utc /* = LOCAL_TIME */) { + t = tzTime(t, local_or_utc); + tmElements_t tm; + ezt::breakTime(t, tm); + return tm.Wday; +} + +uint8_t Timezone::month(time_t t /*= TIME_NOW */, const ezLocalOrUTC_t local_or_utc /* = LOCAL_TIME */) { + t = tzTime(t, local_or_utc); + tmElements_t tm; + ezt::breakTime(t, tm); + return tm.Month; +} + +uint16_t Timezone::year(time_t t /*= TIME_NOW */, const ezLocalOrUTC_t local_or_utc /* = LOCAL_TIME */) { + t = tzTime(t, local_or_utc); + tmElements_t tm; + ezt::breakTime(t, tm); + return tm.Year + 1970; +} + +uint16_t Timezone::dayOfYear(time_t t /*= TIME_NOW */, const ezLocalOrUTC_t local_or_utc /* = LOCAL_TIME */) { + t = tzTime(t, local_or_utc); + time_t jan_1st = ezt::makeTime(0, 0, 0, 1, 1, year(t)); + return (t - jan_1st) / SECS_PER_DAY; +} + +uint8_t Timezone::hourFormat12(time_t t /*= TIME_NOW */, const ezLocalOrUTC_t local_or_utc /* = LOCAL_TIME */) { + t = tzTime(t, local_or_utc); + uint8_t h = t / 3600 % 12; + if (h) return h; + return 12; +} + +bool Timezone::isAM(time_t t /*= TIME_NOW */, const ezLocalOrUTC_t local_or_utc /* = LOCAL_TIME */) { + t = tzTime(t, local_or_utc); + return (t / 3600 % 24 < 12); +} + +bool Timezone::isPM(time_t t /*= TIME_NOW */, const ezLocalOrUTC_t local_or_utc /* = LOCAL_TIME */) { + t = tzTime(t, local_or_utc); + return (t / 3600 % 24 >= 12); +} + + +// 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(year...) ezt::makeOrdinalTime(0, 0, 0, FIRST, THURSDAY, JANUARY, year) - 3UL * SECS_PER_DAY; +uint8_t Timezone::weekISO(time_t t /*= TIME_NOW */, const 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 */, const 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 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; + +namespace ezt { + // All bounce-throughs to defaultTZ + String dateTime(const String format /* = DEFAULT_TIMEFORMAT */) { return (defaultTZ->dateTime(format)); } + String dateTime(time_t t, const String format /* = DEFAULT_TIMEFORMAT */) { return (defaultTZ->dateTime(t, format)); } + String dateTime(time_t t, const ezLocalOrUTC_t local_or_utc, const String format /* = DEFAULT_TIMEFORMAT */) { return (defaultTZ->dateTime(t, local_or_utc, format)); } + uint8_t day(time_t t /* = TIME_NOW */, const ezLocalOrUTC_t local_or_utc /* = LOCAL_TIME */) { return (defaultTZ->day(t, local_or_utc)); } + uint16_t dayOfYear(time_t t /* = TIME_NOW */, const ezLocalOrUTC_t local_or_utc /* = LOCAL_TIME */) { return (defaultTZ->dayOfYear(t, local_or_utc)); } + int16_t getOffset(time_t t /* = TIME_NOW */, const ezLocalOrUTC_t local_or_utc /* = LOCAL_TIME */) { return (defaultTZ->getOffset(t, local_or_utc)); } + String getTimezoneName(time_t t /* = TIME_NOW */, const ezLocalOrUTC_t local_or_utc /* = LOCAL_TIME */) { return (defaultTZ->getTimezoneName(t, local_or_utc)); } + uint8_t hour(time_t t /* = TIME_NOW */, const ezLocalOrUTC_t local_or_utc /* = LOCAL_TIME */) { return (defaultTZ->hour(t, local_or_utc)); } + uint8_t hourFormat12(time_t t /* = TIME_NOW */, const ezLocalOrUTC_t local_or_utc /* = LOCAL_TIME */) { return (defaultTZ->hourFormat12(t, local_or_utc)); } + bool isAM(time_t t /* = TIME_NOW */, const ezLocalOrUTC_t local_or_utc /* = LOCAL_TIME */) { return (defaultTZ->isAM(t, local_or_utc)); } + bool isDST(time_t t /* = TIME_NOW */, const ezLocalOrUTC_t local_or_utc /* = LOCAL_TIME */) { return (defaultTZ->isDST(t, local_or_utc)); } + bool isPM(time_t t /* = TIME_NOW */, const ezLocalOrUTC_t local_or_utc /* = LOCAL_TIME */) { return (defaultTZ->isPM(t, local_or_utc)); } + String militaryTZ(time_t t /* = TIME_NOW */, const ezLocalOrUTC_t local_or_utc /* = LOCAL_TIME */) { return (defaultTZ->militaryTZ(t, local_or_utc)); } + uint8_t minute(time_t t /* = TIME_NOW */, const ezLocalOrUTC_t local_or_utc /* = LOCAL_TIME */) { return (defaultTZ->minute(t, local_or_utc)); } + uint8_t month(time_t t /* = TIME_NOW */, const ezLocalOrUTC_t local_or_utc /* = LOCAL_TIME */) { return (defaultTZ->month(t, local_or_utc)); } + uint16_t ms(time_t t /* = TIME_NOW */) { return (defaultTZ->ms(t)); } + time_t now() { return (defaultTZ->now()); } + uint8_t second(time_t t /* = TIME_NOW */, const ezLocalOrUTC_t local_or_utc /* = LOCAL_TIME */) { return (defaultTZ->second(t, local_or_utc)); } + 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) { return (defaultTZ->setEvent(function,hr, min, sec, day, mnth, yr)); } + uint8_t setEvent(void (*function)(), time_t t /* = TIME_NOW */, const ezLocalOrUTC_t local_or_utc /* = LOCAL_TIME */) { return (defaultTZ->setEvent(function, t, local_or_utc)); } + 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 setTime(time_t t) { defaultTZ->setTime(t); } + uint8_t weekISO(time_t t /* = TIME_NOW */, const ezLocalOrUTC_t local_or_utc /* = LOCAL_TIME */) { return (defaultTZ->weekISO(t, local_or_utc)); } + uint8_t weekday(time_t t /* = TIME_NOW */, const ezLocalOrUTC_t local_or_utc /* = LOCAL_TIME */) { return (defaultTZ->weekday(t, local_or_utc)); } + uint16_t year(time_t t /* = TIME_NOW */, const ezLocalOrUTC_t local_or_utc /* = LOCAL_TIME */) { return (defaultTZ->year(t, local_or_utc)); } + uint16_t yearISO(time_t t /* = TIME_NOW */, const ezLocalOrUTC_t local_or_utc /* = LOCAL_TIME */) { return (defaultTZ->yearISO(t, local_or_utc)); } +} diff --git a/ezTime.h b/ezTime.h new file mode 100644 index 0000000..4669952 --- /dev/null +++ b/ezTime.h @@ -0,0 +1,347 @@ +/* Extensive API documentation is at https://github.com/ropg/ezTime */ + +#ifndef _EZTIME_H_ +#ifdef __cplusplus +#define _EZTIME_H_ + + +// Compiles in NTP updating, timezoned fetching and caching +#define EZTIME_NETWORK_ENABLE + +// 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 setDebug(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 + + +// Warranty void if edited below this point... + + + +#if !defined(__time_t_defined) // avoid conflict with newlib or other posix libc + typedef unsigned long time_t; +#endif + +#include +#ifndef __AVR__ + #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 +typedef unsigned long time_t; +#endif + + +extern "C++" { + +////////// Error handing + +typedef enum { + NO_ERROR, + LAST_ERROR, // Pseudo-error: replaced by last error + NO_NETWORK, + TIMEOUT, + CONNECT_FAILED, + DATA_NOT_FOUND, + LOCKED_TO_UTC, + NO_CACHE_SET, + CACHE_TOO_SMALL, + TOO_MANY_EVENTS, + SERVER_ERROR, + INVALID_DATA +} ezError_t; + +typedef enum { + NONE, + ERROR, + INFO, + DEBUG +} ezDebugLevel_t; + +typedef enum { + LOCAL_TIME, + UTC_TIME +} ezLocalOrUTC_t; + +// Defines that can make your code more readable. For example, if you are looking for the first +// Thursday in a year, you could write: time.makeOrdinalTime(0, 0, 0, JANUARY, FIRST, THURSDAY, year) + +#define SUNDAY 1 +#define MONDAY 2 +#define TUESDAY 3 +#define WEDNESDAY 4 +#define THURSDAY 5 +#define FRIDAY 6 +#define SATURDAY 7 + +#define JANUARY 1 +#define FEBRUARI 2 +#define MARCH 3 +#define APRIL 4 +#define MAY 5 +#define JUNE 6 +#define JULY 7 +#define AUGUST 8 +#define SEPTEMBER 9 +#define OCTOBER 10 +#define NOVEMBER 11 +#define DECEMBER 12 + +#define FIRST 1 +#define SECOND 2 +#define THIRD 3 +#define FOURTH 4 +#define LAST 5 + +//////////////////////// + + +#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 { + time_t time; + void (*function)(); +} ezEvent_t; + +#define MAX_EVENTS 8 + +#define TIME_NOW (int32_t)0x7FFFFFFF // Two special-meaning time_t values ... +#define LAST_READ (int32_t)0x7FFFFFFE // (So yes, ezTime might malfunction two seconds before everything else...) + +#define NTP_PACKET_SIZE 48 +#define NTP_LOCAL_PORT 4242 +#define NTP_SERVER "pool.ntp.org" +#define NTP_TIMEOUT 1500 // milliseconds +#define NTP_INTERVAL 1801 // default update interval in seconds +#define NTP_RETRY 1801 // Retry after this many seconds on failed NTP +#define NTP_STALE_AFTER 3602 // If update due for this many seconds, set timeStatus to timeNeedsSync + +#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 +#define MAX_CACHE_AGE_MONTHS 6 + +// 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 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 RFC822 +#define W3C ATOM +#define ISO8601_YWD "X-\\WW-N" +#define DEFAULT_TIMEFORMAT COOKIE + +namespace ezt { + void breakTime(const time_t time, tmElements_t &tm); + time_t compileTime(const String compile_date = __DATE__, const String compile_time = __TIME__); + String dayShortStr(const uint8_t month); + String dayStr(const uint8_t month); + void deleteEvent(const uint8_t event_handle); + void deleteEvent(void (*function)()); + ezError_t error(const bool reset = false); + String errorString(const ezError_t err = LAST_ERROR); + void events(); + time_t makeOrdinalTime(const uint8_t hour, const uint8_t minute, const uint8_t second, uint8_t ordinal, const uint8_t wday, const uint8_t month, uint16_t year); + time_t makeTime(const uint8_t hour, const uint8_t minute, const uint8_t second, const uint8_t day, const uint8_t month, const uint16_t year); + time_t makeTime(tmElements_t &tm); + bool minuteChanged(); + String monthShortStr(const uint8_t month); + String monthStr(const uint8_t month); + bool secondChanged(); + void setDebug(const ezDebugLevel_t level); + void setDebug(const ezDebugLevel_t level, Print &device); + timeStatus_t timeStatus(); + String urlEncode(const String str); + String zeropad(const uint32_t number, const uint8_t length); + + #ifdef EZTIME_NETWORK_ENABLE + bool queryNTP(const String server, time_t &t, unsigned long &measured_at); + void setInterval(const uint16_t seconds = 0); + void setServer(const String ntp_server = NTP_SERVER); + void updateNTP(); + bool waitForSync(const uint16_t timeout = 0); + #endif +} + +// +// T i m e z o n e c l a s s +// + +class Timezone { + + public: + Timezone(const bool locked_to_UTC = false); + 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); + 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); + 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); + 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); + 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 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); + 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, _olson; + bool _locked_to_UTC; + + #ifdef EZTIME_NETWORK_ENABLE + public: + bool setLocation(const String location = "GeoIP"); + String getOlson(); + String getOlsen(); + #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: + void clearCache(const bool delete_section = false); + private: + bool setCache(); + bool writeCache(String &str); + bool readCache(String &olson, String &posix, uint8_t &months_since_jan_2018); + uint8_t _cache_month; + #endif + #endif + +}; + +extern Timezone UTC; +extern Timezone *defaultTZ; + +namespace ezt { + // These bounce through to same-named methods in defaultTZ + 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); + 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 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); + 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); + 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); + 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); + 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 setTime(time_t t); + 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); +} + +#ifndef EZTIME_EZT_NAMESPACE + using namespace ezt; +#endif + + +// The following defines all copied from the original Time lib to keep existing code working + +/* 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) + + +} // extern "C++" +#endif // __cplusplus +#endif //_EZTIME_H_