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.

309 lines
8.0 KiB

  1. /*
  2. NTP MODULE (based on NtpClientLib)
  3. Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
  4. */
  5. #include "ntp.h"
  6. #if NTP_LEGACY_SUPPORT && NTP_SUPPORT
  7. #include <Ticker.h>
  8. #include "debug.h"
  9. #include "broker.h"
  10. #include "ws.h"
  11. BrokerBind(NtpBroker);
  12. Ticker _ntp_defer;
  13. bool _ntp_report = false;
  14. bool _ntp_configure = false;
  15. bool _ntp_want_sync = false;
  16. // -----------------------------------------------------------------------------
  17. // NtpClient overrides to avoid triggering network sync
  18. // -----------------------------------------------------------------------------
  19. class NTPClientWrap : public NTPClient {
  20. public:
  21. NTPClientWrap() : NTPClient() {
  22. udp = new WiFiUDP();
  23. _lastSyncd = 0;
  24. }
  25. bool setInterval(int shortInterval, int longInterval) {
  26. _shortInterval = shortInterval;
  27. _longInterval = longInterval;
  28. return true;
  29. }
  30. };
  31. // NOTE: original NTP should be discarded by the linker
  32. // TODO: allow NTP client object to be destroyed
  33. static NTPClientWrap NTPw;
  34. // -----------------------------------------------------------------------------
  35. // NTP
  36. // -----------------------------------------------------------------------------
  37. #if WEB_SUPPORT
  38. bool _ntpWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
  39. return (strncmp(key, "ntp", 3) == 0);
  40. }
  41. void _ntpWebSocketOnVisible(JsonObject& root) {
  42. root["ntpVisible"] = 1;
  43. root["ntplegacyVisible"] = 1;
  44. }
  45. void _ntpWebSocketOnData(JsonObject& root) {
  46. root["ntpStatus"] = (timeStatus() == timeSet);
  47. }
  48. void _ntpWebSocketOnConnected(JsonObject& root) {
  49. root["ntpServer"] = getSetting("ntpServer", NTP_SERVER);
  50. root["ntpOffset"] = getSetting("ntpOffset", NTP_TIME_OFFSET);
  51. root["ntpDST"] = getSetting("ntpDST", 1 == NTP_DAY_LIGHT);
  52. root["ntpRegion"] = getSetting("ntpRegion", NTP_DST_REGION);
  53. }
  54. #endif
  55. time_t _ntpSyncProvider() {
  56. _ntp_want_sync = true;
  57. return 0;
  58. }
  59. void _ntpWantSync() {
  60. _ntp_want_sync = true;
  61. }
  62. // Randomized in time to avoid clogging the server with simultaious requests from multiple devices
  63. // (for example, when multiple devices start up at the same time)
  64. int _ntpSyncInterval() {
  65. return secureRandom(NTP_SYNC_INTERVAL, NTP_SYNC_INTERVAL * 2);
  66. }
  67. int _ntpUpdateInterval() {
  68. return secureRandom(NTP_UPDATE_INTERVAL, NTP_UPDATE_INTERVAL * 2);
  69. }
  70. void _ntpConfigure() {
  71. _ntp_configure = false;
  72. int offset = getSetting("ntpOffset", NTP_TIME_OFFSET);
  73. int sign = offset > 0 ? 1 : -1;
  74. offset = abs(offset);
  75. int tz_hours = sign * (offset / 60);
  76. int tz_minutes = sign * (offset % 60);
  77. if (NTPw.getTimeZone() != tz_hours || NTPw.getTimeZoneMinutes() != tz_minutes) {
  78. NTPw.setTimeZone(tz_hours, tz_minutes);
  79. _ntp_report = true;
  80. }
  81. const bool daylight = getSetting("ntpDST", 1 == NTP_DAY_LIGHT);
  82. if (NTPw.getDayLight() != daylight) {
  83. NTPw.setDayLight(daylight);
  84. _ntp_report = true;
  85. }
  86. const auto server = getSetting("ntpServer", NTP_SERVER);
  87. if (!NTPw.getNtpServerName().equals(server)) {
  88. NTPw.setNtpServerName(server);
  89. }
  90. uint8_t dst_region = getSetting("ntpRegion", NTP_DST_REGION);
  91. NTPw.setDSTZone(dst_region);
  92. // Some remote servers can be slow to respond, increase accordingly
  93. // TODO does this need upper constrain?
  94. NTPw.setNTPTimeout(getSetting("ntpTimeout", NTP_TIMEOUT));
  95. }
  96. void _ntpStart() {
  97. _ntpConfigure();
  98. // short (initial) and long (after sync) intervals
  99. NTPw.setInterval(_ntpSyncInterval(), _ntpUpdateInterval());
  100. DEBUG_MSG_P(PSTR("[NTP] Update intervals: %us / %us\n"),
  101. NTPw.getShortInterval(), NTPw.getLongInterval());
  102. // setSyncProvider will immediatly call given function by setting next sync time to the current time.
  103. // Avoid triggering sync immediatly by canceling sync provider flag and resetting sync interval again
  104. setSyncProvider(_ntpSyncProvider);
  105. _ntp_want_sync = false;
  106. setSyncInterval(NTPw.getShortInterval());
  107. }
  108. void _ntpReport() {
  109. _ntp_report = false;
  110. #if DEBUG_SUPPORT
  111. if (ntpSynced()) {
  112. time_t t = now();
  113. DEBUG_MSG_P(PSTR("[NTP] UTC Time : %s\n"), ntpDateTime(ntpLocal2UTC(t)).c_str());
  114. DEBUG_MSG_P(PSTR("[NTP] Local Time: %s\n"), ntpDateTime(t).c_str());
  115. }
  116. #endif
  117. }
  118. #if BROKER_SUPPORT
  119. void inline _ntpBroker() {
  120. static unsigned char last_minute = 60;
  121. if (ntpSynced() && (minute() != last_minute)) {
  122. last_minute = minute();
  123. NtpBroker::Publish(NtpTick::EveryMinute, now(), ntpDateTime());
  124. }
  125. }
  126. #endif
  127. void _ntpLoop() {
  128. // Disable ntp sync when softAP is active. This will not crash, but instead spam debug-log with pointless sync failures.
  129. if (!wifiConnected()) return;
  130. if (_ntp_configure) _ntpConfigure();
  131. // NTPClientLib will trigger callback with sync status
  132. // see: NTPw.onNTPSyncEvent([](NTPSyncEvent_t error){ ... }) below
  133. if (_ntp_want_sync) {
  134. _ntp_want_sync = false;
  135. NTPw.getTime();
  136. }
  137. // Print current time whenever configuration changes or after successful sync
  138. if (_ntp_report) _ntpReport();
  139. #if BROKER_SUPPORT
  140. _ntpBroker();
  141. #endif
  142. }
  143. // TODO: remove me!
  144. void _ntpBackwards() {
  145. moveSetting("ntpServer1", "ntpServer");
  146. delSetting("ntpServer2");
  147. delSetting("ntpServer3");
  148. int offset = getSetting("ntpOffset", NTP_TIME_OFFSET);
  149. if (-30 < offset && offset < 30) {
  150. offset *= 60;
  151. setSetting("ntpOffset", offset);
  152. }
  153. }
  154. // -----------------------------------------------------------------------------
  155. bool ntpSynced() {
  156. #if NTP_WAIT_FOR_SYNC
  157. // Has synced at least once
  158. return (NTPw.getFirstSync() > 0);
  159. #else
  160. // TODO: runtime setting?
  161. return true;
  162. #endif
  163. }
  164. String ntpDateTime(time_t t) {
  165. char buffer[20];
  166. snprintf_P(buffer, sizeof(buffer),
  167. PSTR("%04d-%02d-%02d %02d:%02d:%02d"),
  168. year(t), month(t), day(t), hour(t), minute(t), second(t)
  169. );
  170. return String(buffer);
  171. }
  172. String ntpDateTime() {
  173. if (ntpSynced()) return ntpDateTime(now());
  174. return String();
  175. }
  176. // XXX: returns garbage during DST switch
  177. time_t ntpLocal2UTC(time_t local) {
  178. int offset = getSetting("ntpOffset", NTP_TIME_OFFSET);
  179. if (NTPw.isSummerTime()) offset += 60;
  180. return local - offset * 60;
  181. }
  182. // -----------------------------------------------------------------------------
  183. void ntpSetup() {
  184. _ntpBackwards();
  185. #if TERMINAL_SUPPORT
  186. terminalRegisterCommand(F("NTP"), [](Embedis* e) {
  187. if (ntpSynced()) {
  188. _ntpReport();
  189. terminalOK();
  190. } else {
  191. DEBUG_MSG_P(PSTR("[NTP] Not synced\n"));
  192. }
  193. });
  194. terminalRegisterCommand(F("NTP.SYNC"), [](Embedis* e) {
  195. _ntpWantSync();
  196. terminalOK();
  197. });
  198. #endif
  199. NTPw.onNTPSyncEvent([](NTPSyncEvent_t error) {
  200. if (error) {
  201. if (error == noResponse) {
  202. DEBUG_MSG_P(PSTR("[NTP] Error: NTP server not reachable\n"));
  203. } else if (error == invalidAddress) {
  204. DEBUG_MSG_P(PSTR("[NTP] Error: Invalid NTP server address\n"));
  205. }
  206. #if WEB_SUPPORT
  207. wsPost(_ntpWebSocketOnData);
  208. #endif
  209. } else {
  210. _ntp_report = true;
  211. setTime(NTPw.getLastNTPSync());
  212. }
  213. });
  214. wifiRegister([](justwifi_messages_t code, char * parameter) {
  215. if (code == MESSAGE_CONNECTED) {
  216. if (!ntpSynced()) {
  217. _ntp_defer.once(secureRandom(NTP_START_DELAY, NTP_START_DELAY * 2), _ntpWantSync);
  218. }
  219. }
  220. });
  221. #if WEB_SUPPORT
  222. wsRegister()
  223. .onVisible(_ntpWebSocketOnVisible)
  224. .onConnected(_ntpWebSocketOnConnected)
  225. .onData(_ntpWebSocketOnData)
  226. .onKeyCheck(_ntpWebSocketOnKeyCheck);
  227. #endif
  228. // Main callbacks
  229. espurnaRegisterLoop(_ntpLoop);
  230. espurnaRegisterReload([]() { _ntp_configure = true; });
  231. // Sets up NTP instance, installs ours sync provider
  232. _ntpStart();
  233. }
  234. #endif // NTP_SUPPORT && NTP_LEGACY_SUPPORT