You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ezTime/src/ezTime.cpp

1114 lines
32 KiB

7 years ago
#include <ezTime.h>
#include <Preferences.h> // 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();
ezTime._time_status = timeSet;
7 years ago
}
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