/*
support_rtc.ino - Real Time Clock support for Tasmota
Copyright (C) 2021 Theo Arends
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
/*********************************************************************************************\
* Sources: Time by Michael Margolis and Paul Stoffregen (https://github.com/PaulStoffregen/Time)
* Timezone by Jack Christensen (https://github.com/JChristensen/Timezone)
\*********************************************************************************************/
const uint32_t SECS_PER_MIN = 60UL;
const uint32_t SECS_PER_HOUR = 3600UL;
const uint32_t SECS_PER_DAY = SECS_PER_HOUR * 24UL;
const uint32_t MINS_PER_HOUR = 60UL;
#define LEAP_YEAR(Y) (((1970+Y)>0) && !((1970+Y)%4) && (((1970+Y)%100) || !((1970+Y)%400)))
#include
Ticker TickerRtc;
static const uint8_t kDaysInMonth[] PROGMEM = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; // API starts months from 1, this array starts from 0
static const char kMonthNamesEnglish[] PROGMEM = "JanFebMarAprMayJunJulAugSepOctNovDec";
struct RTC {
uint32_t utc_time = 0;
uint32_t local_time = 0;
uint32_t daylight_saving_time = 0;
uint32_t standard_time = 0;
uint32_t midnight = 0;
uint32_t restart_time = 0;
uint32_t nanos = 0;
uint32_t millis = 0;
// uint32_t last_sync = 0;
int32_t time_timezone = 0;
bool time_synced = false;
bool last_synced = false;
bool midnight_now = false;
bool user_time_entry = false; // Override NTP by user setting
} Rtc;
uint32_t UtcTime(void) {
return Rtc.utc_time;
}
uint32_t LocalTime(void) {
return Rtc.local_time;
}
uint32_t Midnight(void) {
return Rtc.midnight;
}
bool MidnightNow(void) {
if (Rtc.midnight_now) {
Rtc.midnight_now = false;
return true;
}
return false;
}
bool IsDst(void) {
return (Rtc.time_timezone == Settings->toffset[1]);
}
String GetBuildDateAndTime(void) {
// "2017-03-07T11:08:02" - ISO8601:2004
char bdt[21];
char *p;
static const char mdate_P[] PROGMEM = __DATE__; // "Mar 7 2017"
char mdate[strlen_P(mdate_P)+1]; // copy on stack first
strcpy_P(mdate, mdate_P);
char *smonth = mdate;
int day = 0;
int year = 0;
// sscanf(mdate, "%s %d %d", bdt, &day, &year); // Not implemented in 2.3.0 and probably too much code
uint8_t i = 0;
for (char *str = strtok_r(mdate, " ", &p); str && i < 3; str = strtok_r(nullptr, " ", &p)) {
switch (i++) {
case 0: // Month
smonth = str;
break;
case 1: // Day
day = atoi(str);
break;
case 2: // Year
year = atoi(str);
}
}
char MonthNamesEnglish[sizeof(kMonthNamesEnglish)];
strcpy_P(MonthNamesEnglish, kMonthNamesEnglish);
int month = (strstr(MonthNamesEnglish, smonth) -MonthNamesEnglish) /3 +1;
snprintf_P(bdt, sizeof(bdt), PSTR("%d" D_YEAR_MONTH_SEPARATOR "%02d" D_MONTH_DAY_SEPARATOR "%02d" D_DATE_TIME_SEPARATOR "%s"), year, month, day, PSTR(__TIME__));
return String(bdt); // 2017-03-07T11:08:02
}
String GetMinuteTime(uint32_t minutes) {
char tm[6];
snprintf_P(tm, sizeof(tm), PSTR("%02d:%02d"), minutes / 60, minutes % 60);
return String(tm); // 03:45
}
String GetTimeZone(void) {
char tz[7];
snprintf_P(tz, sizeof(tz), PSTR("%+03d:%02d"), Rtc.time_timezone / 60, abs(Rtc.time_timezone % 60));
return String(tz); // -03:45
}
String GetDuration(uint32_t time) {
char dt[16];
TIME_T ut;
BreakTime(time, ut);
// "P128DT14H35M44S" - ISO8601:2004 - https://en.wikipedia.org/wiki/ISO_8601 Durations
// snprintf_P(dt, sizeof(dt), PSTR("P%dDT%02dH%02dM%02dS"), ut.days, ut.hour, ut.minute, ut.second);
// "128 14:35:44" - OpenVMS
// "128T14:35:44" - Tasmota
snprintf_P(dt, sizeof(dt), PSTR("%dT%02d:%02d:%02d"), ut.days, ut.hour, ut.minute, ut.second);
return String(dt); // 128T14:35:44
}
String GetDT(uint32_t time) {
// "2017-03-07T11:08:02" - ISO8601:2004
char dt[20];
TIME_T tmpTime;
BreakTime(time, tmpTime);
snprintf_P(dt, sizeof(dt), PSTR("%04d-%02d-%02dT%02d:%02d:%02d"),
tmpTime.year +1970, tmpTime.month, tmpTime.day_of_month, tmpTime.hour, tmpTime.minute, tmpTime.second);
return String(dt); // 2017-03-07T11:08:02
}
/*
* timestamps in https://en.wikipedia.org/wiki/ISO_8601 format
*
* DT_UTC - current data and time in Greenwich, England (aka GMT)
* DT_LOCAL - current date and time taking timezone into account
* DT_RESTART - the date and time this device last started, in local timezone
*
* Format:
* "2017-03-07T11:08:02-07:00" - if DT_LOCAL and SetOption52 = 1
* "2017-03-07T11:08:02" - otherwise
*/
String GetDateAndTime(uint8_t time_type) {
// "2017-03-07T11:08:02-07:00" - ISO8601:2004
uint32_t time = Rtc.local_time;
switch (time_type) {
case DT_UTC:
time = Rtc.utc_time;
break;
// case DT_LOCALNOTZ: // Is default anyway but allows for not appendig timezone
// time = Rtc.local_time;
// break;
case DT_DST:
time = Rtc.daylight_saving_time;
break;
case DT_STD:
time = Rtc.standard_time;
break;
case DT_RESTART:
if (Rtc.restart_time == 0) {
return "";
}
time = Rtc.restart_time;
break;
case DT_ENERGY:
time = Settings->energy_kWhtotal_time;
break;
case DT_BOOTCOUNT:
time = Settings->bootcount_reset_time;
break;
}
String dt = GetDT(time); // 2017-03-07T11:08:02
if (DT_LOCAL_MILLIS == time_type) {
char ms[10];
snprintf_P(ms, sizeof(ms), PSTR(".%03d"), RtcMillis());
dt += ms;
time_type = DT_LOCAL;
}
if (Settings->flag3.time_append_timezone && (DT_LOCAL == time_type)) { // SetOption52 - Append timezone to JSON time
dt += GetTimeZone(); // 2017-03-07T11:08:02-07:00
}
return dt; // 2017-03-07T11:08:02-07:00
}
uint32_t UpTime(void) {
if (Rtc.restart_time) {
return Rtc.utc_time - Rtc.restart_time;
} else {
return TasmotaGlobal.uptime;
}
}
uint32_t MinutesUptime(void) {
return (UpTime() / 60);
}
String GetUptime(void) {
return GetDuration(UpTime());
}
uint32_t MinutesPastMidnight(void) {
uint32_t minutes = 0;
if (RtcTime.valid) {
minutes = (RtcTime.hour *60) + RtcTime.minute;
}
return minutes;
}
uint32_t RtcMillis(void) {
return (millis() - Rtc.millis) % 1000;
}
void BreakNanoTime(uint32_t time_input, uint32_t time_nanos, TIME_T &tm) {
// break the given time_input into time components
// this is a more compact version of the C library localtime function
// note that year is offset from 1970 !!!
time_input += time_nanos / 1000000000U;
tm.nanos = time_nanos % 1000000000U;
uint8_t year;
uint8_t month;
uint8_t month_length;
uint32_t time;
unsigned long days;
time = time_input;
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.days = time;
tm.day_of_week = ((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
tm.day_of_year = time;
for (month = 0; month < 12; month++) {
if (1 == month) { // february
if (LEAP_YEAR(year)) {
month_length = 29;
} else {
month_length = 28;
}
} else {
month_length = pgm_read_byte(&kDaysInMonth[month]);
}
if (time >= month_length) {
time -= month_length;
} else {
break;
}
}
strlcpy(tm.name_of_month, kMonthNames + (month *3), 4);
tm.month = month + 1; // jan is month 1
tm.day_of_month = time + 1; // day of month
tm.valid = (time_input > START_VALID_TIME); // 2016-01-01
}
void BreakTime(uint32_t time_input, TIME_T &tm) {
BreakNanoTime(time_input, 0, tm);
}
uint32_t MakeTime(TIME_T &tm) {
// assemble time elements into time_t
// note year argument is offset from 1970
int i;
uint32_t seconds;
// seconds from 1970 till 1 jan 00:00:00 of the given year
seconds = tm.year * (SECS_PER_DAY * 365);
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 ((2 == i) && LEAP_YEAR(tm.year)) {
seconds += SECS_PER_DAY * 29;
} else {
seconds += SECS_PER_DAY * pgm_read_byte(&kDaysInMonth[i-1]); // monthDay array starts from 0
}
}
seconds+= (tm.day_of_month - 1) * SECS_PER_DAY;
seconds+= tm.hour * SECS_PER_HOUR;
seconds+= tm.minute * SECS_PER_MIN;
seconds+= tm.second;
return seconds;
}
uint32_t RuleToTime(TimeRule r, int yr) {
TIME_T tm;
uint32_t t;
uint8_t m;
uint8_t w; // temp copies of r.month and r.week
m = r.month;
w = r.week;
if (0 == w) { // Last week = 0
if (++m > 12) { // for "Last", go to the next month
m = 1;
yr++;
}
w = 1; // and treat as first week of next month, subtract 7 days later
}
tm.hour = r.hour;
tm.minute = 0;
tm.second = 0;
tm.day_of_month = 1;
tm.month = m;
tm.year = yr - 1970;
t = MakeTime(tm); // First day of the month, or first day of next month for "Last" rules
BreakTime(t, tm);
t += (7 * (w - 1) + (r.dow - tm.day_of_week + 7) % 7) * SECS_PER_DAY;
if (0 == r.week) {
t -= 7 * SECS_PER_DAY; // back up a week if this is a "Last" rule
}
return t;
}
void RtcGetDaylightSavingTimes(uint32_t local_time) {
TIME_T tmpTime;
BreakTime(local_time, tmpTime);
tmpTime.year += 1970;
Rtc.daylight_saving_time = RuleToTime(Settings->tflag[1], tmpTime.year);
Rtc.standard_time = RuleToTime(Settings->tflag[0], tmpTime.year);
}
uint32_t RtcTimeZoneOffset(uint32_t local_time) {
int16_t timezone_minutes = Settings->timezone_minutes;
if (Settings->timezone < 0) { timezone_minutes *= -1; }
uint32_t timezone = (Settings->timezone * SECS_PER_HOUR) + (timezone_minutes * SECS_PER_MIN);
if (99 == Settings->timezone) {
int32_t dstoffset = Settings->toffset[1] * SECS_PER_MIN;
int32_t stdoffset = Settings->toffset[0] * SECS_PER_MIN;
if (Settings->tflag[1].hemis) {
// Southern hemisphere
if ((local_time >= (Rtc.standard_time - dstoffset)) && (local_time < (Rtc.daylight_saving_time - stdoffset))) {
timezone = stdoffset; // Standard Time
} else {
timezone = dstoffset; // Daylight Saving Time
}
} else {
// Northern hemisphere
if ((local_time >= (Rtc.daylight_saving_time - stdoffset)) && (local_time < (Rtc.standard_time - dstoffset))) {
timezone = dstoffset; // Daylight Saving Time
} else {
timezone = stdoffset; // Standard Time
}
}
}
return timezone;
}
void RtcSetTimeOfDay(uint32_t local_time) {
// Sync Core/RTOS time to be used by file system time stamps
struct timeval tv;
tv.tv_sec = local_time;
tv.tv_usec = 0;
settimeofday(&tv, nullptr);
}
void RtcSecond(void) {
static uint32_t last_sync = 0;
static bool mutex = false;
if (mutex) { return; }
if (Rtc.time_synced) {
mutex = true;
Rtc.time_synced = false;
Rtc.last_synced = true;
last_sync = Rtc.utc_time;
if (Rtc.restart_time == 0) {
Rtc.restart_time = Rtc.utc_time - TasmotaGlobal.uptime; // save first synced time as restart time
}
RtcGetDaylightSavingTimes(Rtc.utc_time);
AddLog(LOG_LEVEL_DEBUG, PSTR("RTC: " D_UTC_TIME " %s, " D_DST_TIME " %s, " D_STD_TIME " %s"),
GetDateAndTime(DT_UTC).c_str(), GetDateAndTime(DT_DST).c_str(), GetDateAndTime(DT_STD).c_str());
if (Rtc.local_time < START_VALID_TIME) { // 2016-01-01
TasmotaGlobal.rules_flag.time_init = 1;
} else {
TasmotaGlobal.rules_flag.time_set = 1;
}
} else {
if (Rtc.last_synced) {
Rtc.last_synced = false;
uint32_t nanos = Rtc.nanos + (millis() - Rtc.millis) * 1000000U;
Rtc.utc_time += nanos / 1000000000U;
Rtc.nanos = nanos % 1000000000U;
} else
Rtc.utc_time++; // Increment every second
}
Rtc.millis = millis();
if ((Rtc.utc_time > (2 * 60 * 60)) && (last_sync < Rtc.utc_time - (2 * 60 * 60))) { // Every two hours a warning
AddLog(LOG_LEVEL_DEBUG, PSTR("RTC: Not synced"));
last_sync = Rtc.utc_time;
}
Rtc.local_time = Rtc.utc_time;
if (Rtc.local_time > START_VALID_TIME) { // 2016-01-01
Rtc.time_timezone = RtcTimeZoneOffset(Rtc.utc_time);
Rtc.local_time += Rtc.time_timezone;
Rtc.time_timezone /= 60;
if (!Settings->energy_kWhtotal_time) {
Settings->energy_kWhtotal_time = Rtc.local_time;
}
if (Settings->bootcount_reset_time < START_VALID_TIME) {
Settings->bootcount_reset_time = Rtc.local_time;
}
}
BreakNanoTime(Rtc.local_time, Rtc.nanos, RtcTime);
if (RtcTime.valid) {
if (!Rtc.midnight) {
Rtc.midnight = Rtc.local_time - (RtcTime.hour * 3600) - (RtcTime.minute * 60) - RtcTime.second;
}
if (!RtcTime.hour && !RtcTime.minute && !RtcTime.second) {
Rtc.midnight = Rtc.local_time;
Rtc.midnight_now = true;
}
if (mutex) { // Time is just synced and is valid
// Sync Core/RTOS time to be used by file system time stamps
RtcSetTimeOfDay(Rtc.local_time);
}
}
RtcTime.year += 1970;
mutex = false;
}
void RtcSync(const char* source) {
Rtc.time_synced = true;
RtcSecond();
AddLog(LOG_LEVEL_DEBUG, PSTR("RTC: Synced by %s"), source);
XdrvCall(FUNC_TIME_SYNCED);
}
void RtcSetTime(uint32_t epoch) {
if (epoch < START_VALID_TIME) { // 2016-01-01
Rtc.user_time_entry = false;
TasmotaGlobal.ntp_force_sync = true;
} else {
Rtc.user_time_entry = true;
// Rtc.utc_time = epoch -1; // Will be corrected by RtcSecond
Rtc.utc_time = epoch;
RtcSync("Time");
}
}
void RtcInit(void) {
Rtc.utc_time = 0;
BreakTime(Rtc.utc_time, RtcTime);
TickerRtc.attach(1, RtcSecond);
if (Settings->cfg_timestamp > START_VALID_TIME) {
// Fix file timestamp while utctime is not synced
uint32_t utc_time = Settings->cfg_timestamp;
if (RtcSettings.utc_time > utc_time) {
utc_time = RtcSettings.utc_time;
}
utc_time++;
RtcGetDaylightSavingTimes(utc_time);
uint32_t local_time = utc_time + RtcTimeZoneOffset(utc_time);
RtcSetTimeOfDay(local_time);
// AddLog(LOG_LEVEL_DEBUG, PSTR("RTC: Timestamp %s"), GetDT(local_time).c_str());
}
}
void RtcPreInit(void) {
Rtc.millis = millis();
}