Fork of the espurna firmware for `mhsw` switches
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.

401 lines
10 KiB

8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
6 years ago
8 years ago
  1. /*
  2. NTP MODULE
  3. Based on esp8266 / esp32 configTime and C date and time functions:
  4. - https://github.com/esp8266/Arduino/blob/master/libraries/esp8266/examples/NTP-TZ-DST/NTP-TZ-DST.ino
  5. - https://www.nongnu.org/lwip/2_1_x/group__sntp.html
  6. - man 3 ctime
  7. Copyright (C) 2019 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
  8. */
  9. #include "ntp.h"
  10. #if NTP_SUPPORT && !NTP_LEGACY_SUPPORT
  11. #include <Arduino.h>
  12. #include <coredecls.h>
  13. #include <Ticker.h>
  14. static_assert(
  15. (SNTP_SERVER_DNS == 1),
  16. "lwip must be configured with SNTP_SERVER_DNS"
  17. );
  18. #include "config/buildtime.h"
  19. #include "debug.h"
  20. #include "broker.h"
  21. #include "ws.h"
  22. // Arduino/esp8266 lwip2 custom functions that can be redefined
  23. // Must return time in milliseconds, legacy settings are in seconds.
  24. String _ntp_server;
  25. uint32_t _ntp_startup_delay = (NTP_START_DELAY * 1000);
  26. uint32_t _ntp_update_delay = (NTP_UPDATE_INTERVAL * 1000);
  27. uint32_t sntp_startup_delay_MS_rfc_not_less_than_60000() {
  28. return _ntp_startup_delay;
  29. }
  30. uint32_t sntp_update_delay_MS_rfc_not_less_than_15000() {
  31. return _ntp_update_delay;
  32. }
  33. // We also must shim TimeLib functions until everything else is ported.
  34. // We can't sometimes avoid TimeLib as dependancy though, which would be really bad
  35. static Ticker _ntp_broker_timer;
  36. static bool _ntp_synced = false;
  37. static time_t _ntp_last = 0;
  38. static time_t _ntp_ts = 0;
  39. static tm _ntp_tm_local;
  40. static tm _ntp_tm_utc;
  41. void _ntpTmCache(time_t ts) {
  42. if (_ntp_ts != ts) {
  43. _ntp_ts = ts;
  44. localtime_r(&_ntp_ts, &_ntp_tm_local);
  45. gmtime_r(&_ntp_ts, &_ntp_tm_utc);
  46. }
  47. }
  48. int hour(time_t ts) {
  49. _ntpTmCache(ts);
  50. return _ntp_tm_local.tm_hour;
  51. }
  52. int minute(time_t ts) {
  53. _ntpTmCache(ts);
  54. return _ntp_tm_local.tm_min;
  55. }
  56. int second(time_t ts) {
  57. _ntpTmCache(ts);
  58. return _ntp_tm_local.tm_sec;
  59. }
  60. int day(time_t ts) {
  61. _ntpTmCache(ts);
  62. return _ntp_tm_local.tm_mday;
  63. }
  64. // `tm.tm_wday` range is 0..6, TimeLib is 1..7
  65. int weekday(time_t ts) {
  66. _ntpTmCache(ts);
  67. return _ntp_tm_local.tm_wday + 1;
  68. }
  69. // `tm.tm_mon` range is 0..11, TimeLib range is 1..12
  70. int month(time_t ts) {
  71. _ntpTmCache(ts);
  72. return _ntp_tm_local.tm_mon + 1;
  73. }
  74. int year(time_t ts) {
  75. _ntpTmCache(ts);
  76. return _ntp_tm_local.tm_year + 1900;
  77. }
  78. int utc_hour(time_t ts) {
  79. _ntpTmCache(ts);
  80. return _ntp_tm_utc.tm_hour;
  81. }
  82. int utc_minute(time_t ts) {
  83. _ntpTmCache(ts);
  84. return _ntp_tm_utc.tm_min;
  85. }
  86. int utc_second(time_t ts) {
  87. _ntpTmCache(ts);
  88. return _ntp_tm_utc.tm_sec;
  89. }
  90. int utc_day(time_t ts) {
  91. _ntpTmCache(ts);
  92. return _ntp_tm_utc.tm_mday;
  93. }
  94. int utc_weekday(time_t ts) {
  95. _ntpTmCache(ts);
  96. return _ntp_tm_utc.tm_wday + 1;
  97. }
  98. int utc_month(time_t ts) {
  99. _ntpTmCache(ts);
  100. return _ntp_tm_utc.tm_mon + 1;
  101. }
  102. int utc_year(time_t ts) {
  103. _ntpTmCache(ts);
  104. return _ntp_tm_utc.tm_year + 1900;
  105. }
  106. time_t now() {
  107. return time(nullptr);
  108. }
  109. // -----------------------------------------------------------------------------
  110. #if WEB_SUPPORT
  111. bool _ntpWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
  112. return (strncmp(key, "ntp", 3) == 0);
  113. }
  114. void _ntpWebSocketOnVisible(JsonObject& root) {
  115. root["ntpVisible"] = 1;
  116. root["ntplwipVisible"] = 1;
  117. }
  118. void _ntpWebSocketOnData(JsonObject& root) {
  119. root["ntpStatus"] = ntpSynced();
  120. }
  121. void _ntpWebSocketOnConnected(JsonObject& root) {
  122. root["ntpServer"] = getSetting("ntpServer", F(NTP_SERVER));
  123. root["ntpTZ"] = getSetting("ntpTZ", NTP_TIMEZONE);
  124. }
  125. #endif
  126. // TODO: mention possibility of multiple servers
  127. String _ntpGetServer() {
  128. String server;
  129. server = sntp_getservername(0);
  130. if (!server.length()) {
  131. server = IPAddress(sntp_getserver(0)).toString();
  132. }
  133. return server;
  134. }
  135. void _ntpReport() {
  136. if (!ntpSynced()) {
  137. DEBUG_MSG_P(PSTR("[NTP] Not synced\n"));
  138. return;
  139. }
  140. tm utc_tm;
  141. tm sync_tm;
  142. auto ts = now();
  143. gmtime_r(&ts, &utc_tm);
  144. gmtime_r(&_ntp_last, &sync_tm);
  145. DEBUG_MSG_P(PSTR("[NTP] Server : %s\n"), _ntp_server.c_str());
  146. DEBUG_MSG_P(PSTR("[NTP] Sync Time : %s (UTC)\n"), ntpDateTime(&sync_tm).c_str());
  147. DEBUG_MSG_P(PSTR("[NTP] UTC Time : %s\n"), ntpDateTime(&utc_tm).c_str());
  148. const char* cfg_tz = getenv("TZ");
  149. if ((cfg_tz != nullptr) && (strcmp(cfg_tz, "UTC0") != 0)) {
  150. tm local_tm;
  151. localtime_r(&ts, &local_tm);
  152. DEBUG_MSG_P(PSTR("[NTP] Local Time : %s (%s)\n"),
  153. ntpDateTime(&local_tm).c_str(), cfg_tz
  154. );
  155. }
  156. }
  157. void _ntpConfigure() {
  158. // Note: TZ_... provided by the Core are already wrapped with PSTR(...)
  159. const auto cfg_tz = getSetting("ntpTZ", NTP_TIMEZONE);
  160. const char* active_tz = getenv("TZ");
  161. if (cfg_tz != active_tz) {
  162. setenv("TZ", cfg_tz.c_str(), 1);
  163. tzset();
  164. }
  165. const auto cfg_server = getSetting("ntpServer", F(NTP_SERVER));
  166. const auto active_server = _ntpGetServer();
  167. if (cfg_tz != active_tz) {
  168. _ntp_server = cfg_server;
  169. configTime(cfg_tz.c_str(), _ntp_server.c_str());
  170. DEBUG_MSG_P(PSTR("[NTP] Server: %s, TZ: %s\n"), cfg_server.c_str(), cfg_tz.length() ? cfg_tz.c_str() : "UTC0");
  171. }
  172. }
  173. // -----------------------------------------------------------------------------
  174. bool ntpSynced() {
  175. return _ntp_synced;
  176. }
  177. String ntpDateTime(tm* timestruct) {
  178. char buffer[20];
  179. snprintf_P(buffer, sizeof(buffer),
  180. PSTR("%04d-%02d-%02d %02d:%02d:%02d"),
  181. timestruct->tm_year + 1900,
  182. timestruct->tm_mon + 1,
  183. timestruct->tm_mday,
  184. timestruct->tm_hour,
  185. timestruct->tm_min,
  186. timestruct->tm_sec
  187. );
  188. return String(buffer);
  189. }
  190. String ntpDateTime(time_t ts) {
  191. tm timestruct;
  192. localtime_r(&ts, &timestruct);
  193. return ntpDateTime(&timestruct);
  194. }
  195. String ntpDateTime() {
  196. if (ntpSynced()) {
  197. return ntpDateTime(now());
  198. }
  199. return String();
  200. }
  201. // -----------------------------------------------------------------------------
  202. #if BROKER_SUPPORT
  203. void _ntpBrokerSchedule(int offset);
  204. void _ntpBrokerCallback() {
  205. if (!ntpSynced()) {
  206. _ntpBrokerSchedule(60);
  207. return;
  208. }
  209. const auto ts = now();
  210. // current time and formatter string is in local TZ
  211. tm local_tm;
  212. localtime_r(&ts, &local_tm);
  213. int now_hour = local_tm.tm_hour;
  214. int now_minute = local_tm.tm_min;
  215. static int last_hour = -1;
  216. static int last_minute = -1;
  217. String datetime;
  218. if ((last_minute != now_minute) || (last_hour != now_hour)) {
  219. datetime = ntpDateTime(&local_tm);
  220. }
  221. // notify subscribers about each tick interval (note that both can happen simultaneously)
  222. if (last_hour != now_hour) {
  223. last_hour = now_hour;
  224. NtpBroker::Publish(NtpTick::EveryHour, ts, datetime.c_str());
  225. }
  226. if (last_minute != now_minute) {
  227. last_minute = now_minute;
  228. NtpBroker::Publish(NtpTick::EveryMinute, ts, datetime.c_str());
  229. }
  230. // try to autocorrect each invocation
  231. _ntpBrokerSchedule(60 - local_tm.tm_sec);
  232. }
  233. // XXX: Nonos docs for some reason mention 100 micros as minimum time. Schedule next second in case this is 0
  234. void _ntpBrokerSchedule(int offset) {
  235. _ntp_broker_timer.once_scheduled(offset ?: 1, _ntpBrokerCallback);
  236. }
  237. #endif
  238. void _ntpSetTimeOfDayCallback() {
  239. _ntp_synced = true;
  240. _ntp_last = time(nullptr);
  241. #if BROKER_SUPPORT
  242. static bool once = true;
  243. if (once) {
  244. schedule_function(_ntpBrokerCallback);
  245. once = false;
  246. }
  247. #endif
  248. #if WEB_SUPPORT
  249. wsPost(_ntpWebSocketOnData);
  250. #endif
  251. schedule_function(_ntpReport);
  252. }
  253. void _ntpSetTimestamp(time_t ts) {
  254. timeval tv { ts, 0 };
  255. timezone tz { 0, 0 };
  256. settimeofday(&tv, &tz);
  257. }
  258. // -----------------------------------------------------------------------------
  259. void ntpSetup() {
  260. // Randomized in time to avoid clogging the server with simultaneous requests from multiple devices
  261. // (for example, when multiple devices start up at the same time)
  262. const uint32_t startup_delay = getSetting("ntpStartDelay", NTP_START_DELAY);
  263. const uint32_t update_delay = getSetting("ntpUpdateIntvl", NTP_UPDATE_INTERVAL);
  264. _ntp_startup_delay = secureRandom(startup_delay, startup_delay * 2);
  265. _ntp_update_delay = secureRandom(update_delay, update_delay * 2);
  266. DEBUG_MSG_P(PSTR("[NTP] Startup delay: %us, Update delay: %us\n"),
  267. _ntp_startup_delay, _ntp_update_delay
  268. );
  269. _ntp_startup_delay = _ntp_startup_delay * 1000;
  270. _ntp_update_delay = _ntp_update_delay * 1000;
  271. // start up with some reasonable timestamp already available
  272. _ntpSetTimestamp(__UNIX_TIMESTAMP__);
  273. // will be called every time after ntp syncs AND loop() finishes
  274. settimeofday_cb(_ntpSetTimeOfDayCallback);
  275. // generic configuration, always handled
  276. espurnaRegisterReload(_ntpConfigure);
  277. _ntpConfigure();
  278. // make sure our logic does know about the actual server
  279. // in case dhcp sends out ntp settings
  280. static WiFiEventHandler on_sta = WiFi.onStationModeGotIP([](WiFiEventStationModeGotIP) {
  281. const auto server = _ntpGetServer();
  282. if (sntp_enabled() && (!_ntp_server.length() || (server != _ntp_server))) {
  283. DEBUG_MSG_P(PSTR("[NTP] Updating `ntpServer` setting from DHCP: %s\n"), server.c_str());
  284. _ntp_server = server;
  285. setSetting("ntpServer", server);
  286. }
  287. });
  288. // optional functionality
  289. #if WEB_SUPPORT
  290. wsRegister()
  291. .onVisible(_ntpWebSocketOnVisible)
  292. .onConnected(_ntpWebSocketOnConnected)
  293. .onData(_ntpWebSocketOnData)
  294. .onKeyCheck(_ntpWebSocketOnKeyCheck);
  295. #endif
  296. #if TERMINAL_SUPPORT
  297. terminalRegisterCommand(F("NTP"), [](Embedis* e) {
  298. _ntpReport();
  299. terminalOK();
  300. });
  301. terminalRegisterCommand(F("NTP.SETTIME"), [](Embedis* e) {
  302. if (e->argc != 2) return;
  303. _ntp_synced = true;
  304. _ntpSetTimestamp(String(e->argv[1]).toInt());
  305. terminalOK();
  306. });
  307. // TODO:
  308. // terminalRegisterCommand(F("NTP.SYNC"), [](Embedis* e) { ... }
  309. //
  310. #endif
  311. }
  312. #endif // NTP_SUPPORT && !NTP_LEGACY_SUPPORT