Browse Source

NTP: use sntp app from lwip on latest Cores, replace ntpclientlib (#2132)

* ntp: try using sntp app from lwip, drop ntpclientlib

* fix display

* thermostat: fix day and month getters

* test build sizes with scheduler

* use system timers for once-a-minute scheduling, no polling

* tick

* avoid timestamps, use tm

* drop utc rpn operator, add utc_hour and utc_dow

* try to build with old implementation too

* dep

* notify ws

* progmem

* cleanup types

* offset tm values by 1 to match existing schedules

* avoid using ntpclientlib with rpn

* test. show debug strings in sch

* fix secureclient

* consts, fix unsyncing when changing tz (and not triggering sntp after reinit for some reason)

* startup time in seconds

* same delay as lwip

* header

* assume build timestamp is unixtime

* cache server value

* fmt

* typo

* handle dhcp request

* rename

* web

* TZ.h

* add notice about what alias means

* fix disabling NTP_SUPPORT

* scheduled ticker

* same behaviour as old module

* rollback rpn dependency check, utc_ prefixes

* ...

* comments, refactor naming
mcspr-patch-1
Max Prokhorov 4 years ago
committed by GitHub
parent
commit
ba3ec47ed0
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 20710 additions and 19976 deletions
  1. +0
    -1
      code/espurna/broker.h
  2. +11
    -1
      code/espurna/config/dependencies.h
  3. +26
    -13
      code/espurna/config/general.h
  4. BIN
      code/espurna/data/index.all.html.gz
  5. BIN
      code/espurna/data/index.light.html.gz
  6. BIN
      code/espurna/data/index.lightfox.html.gz
  7. BIN
      code/espurna/data/index.rfbridge.html.gz
  8. BIN
      code/espurna/data/index.rfm69.html.gz
  9. BIN
      code/espurna/data/index.sensor.html.gz
  10. BIN
      code/espurna/data/index.small.html.gz
  11. BIN
      code/espurna/data/index.thermostat.html.gz
  12. +2
    -0
      code/espurna/espurna.ino
  13. +4
    -0
      code/espurna/libs/NtpClientWrap.h
  14. +5
    -1
      code/espurna/libs/SecureClientHelpers.h
  15. +1
    -1
      code/espurna/mqtt.ino
  16. +52
    -0
      code/espurna/ntp.h
  17. +290
    -172
      code/espurna/ntp.ino
  18. +279
    -0
      code/espurna/ntp_legacy.ino
  19. +108
    -0
      code/espurna/ntp_timelib.h
  20. +20
    -0
      code/espurna/rpnrules.h
  21. +118
    -44
      code/espurna/rpnrules.ino
  22. +80
    -55
      code/espurna/scheduler.ino
  23. +3045
    -3043
      code/espurna/static/index.all.html.gz.h
  24. +2851
    -2849
      code/espurna/static/index.light.html.gz.h
  25. +2422
    -2420
      code/espurna/static/index.lightfox.html.gz.h
  26. +2448
    -2446
      code/espurna/static/index.rfbridge.html.gz.h
  27. +2943
    -2941
      code/espurna/static/index.rfm69.html.gz.h
  28. +1832
    -1830
      code/espurna/static/index.sensor.html.gz.h
  29. +2385
    -2383
      code/espurna/static/index.small.html.gz.h
  30. +1763
    -1761
      code/espurna/static/index.thermostat.html.gz.h
  31. +8
    -7
      code/espurna/thermostat.ino
  32. +9
    -5
      code/espurna/utils.ino
  33. +8
    -3
      code/html/index.html

+ 0
- 1
code/espurna/broker.h View File

@ -54,7 +54,6 @@ using StatusBroker = TBroker<TBrokerType::STATUS, const String&, unsigned char,
using SensorReadBroker = TBroker<TBrokerType::SENSOR_READ, const String&, unsigned char, double, const char*>; using SensorReadBroker = TBroker<TBrokerType::SENSOR_READ, const String&, unsigned char, double, const char*>;
using SensorReportBroker = TBroker<TBrokerType::SENSOR_REPORT, const String&, unsigned char, double, const char*>; using SensorReportBroker = TBroker<TBrokerType::SENSOR_REPORT, const String&, unsigned char, double, const char*>;
using TimeBroker = TBroker<TBrokerType::DATETIME, const String&, time_t, const String&>;
using ConfigBroker = TBroker<TBrokerType::CONFIG, const String&, const String&>; using ConfigBroker = TBroker<TBrokerType::CONFIG, const String&, const String&>;
#endif // BROKER_SUPPORT #endif // BROKER_SUPPORT

+ 11
- 1
code/espurna/config/dependencies.h View File

@ -76,7 +76,9 @@
#if SCHEDULER_SUPPORT #if SCHEDULER_SUPPORT
#undef NTP_SUPPORT #undef NTP_SUPPORT
#define NTP_SUPPORT 1 // Scheduler needs NTP
#define NTP_SUPPORT 1 // Scheduler needs NTP to work
#undef BROKER_SUPPORT
#define BROKER_SUPPORT 1 // Scheduler needs Broker to trigger every minute
#endif #endif
#if LWIP_VERSION_MAJOR != 1 #if LWIP_VERSION_MAJOR != 1
@ -146,3 +148,11 @@
#define SSDP_SUPPORT 0 #define SSDP_SUPPORT 0
#endif #endif
//------------------------------------------------------------------------------
// Change ntp module depending on Core version
#if NTP_SUPPORT && defined(ARDUINO_ESP8266_RELEASE_2_3_0)
#define NTP_LEGACY_SUPPORT 1
#else
#define NTP_LEGACY_SUPPORT 0
#endif

+ 26
- 13
code/espurna/config/general.h View File

@ -1476,13 +1476,38 @@
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
#ifndef NTP_SUPPORT #ifndef NTP_SUPPORT
#define NTP_SUPPORT 1 // Build with NTP support by default (6.78Kb)
#define NTP_SUPPORT 1 // Build with NTP support by default (depends on Core version)
#endif #endif
#ifndef NTP_SERVER #ifndef NTP_SERVER
#define NTP_SERVER "pool.ntp.org" // Default NTP server #define NTP_SERVER "pool.ntp.org" // Default NTP server
#endif #endif
#ifndef NTP_TIMEZONE
#define NTP_TIMEZONE TZ_Etc_UTC // POSIX TZ variable. Default to UTC from TZ.h (which is PSTR("UTC0"))
// For the format documentation, see:
// - https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html
// ESP8266 Core provides human-readable aliases for POSIX format, see:
// - Latest: https://github.com/esp8266/Arduino/blob/master/cores/esp8266/TZ.h
// - PlatformIO: ~/.platformio/packages/framework-arduinoespressif8266/cores/esp8266/TZ.h
// (or, possibly, c:\.platformio\... on Windows)
// - Arduino IDE: depends on platform, see `/dist/arduino_ide/README.md`
#endif
#ifndef NTP_UPDATE_INTERVAL
#define NTP_UPDATE_INTERVAL 1800 // NTP check every 30 minutes
#endif
#ifndef NTP_START_DELAY
#define NTP_START_DELAY 3 // Delay NTP start for 3 seconds
#endif
#ifndef NTP_WAIT_FOR_SYNC
#define NTP_WAIT_FOR_SYNC 1 // Do not report any datetime until NTP sync'ed
#endif
// WARNING: legacy NTP settings. can be ignored with Core 2.6.2+
#ifndef NTP_TIMEOUT #ifndef NTP_TIMEOUT
#define NTP_TIMEOUT 1000 // Set NTP request timeout to 2 seconds (issue #452) #define NTP_TIMEOUT 1000 // Set NTP request timeout to 2 seconds (issue #452)
#endif #endif
@ -1499,22 +1524,10 @@
#define NTP_SYNC_INTERVAL 60 // NTP initial check every minute #define NTP_SYNC_INTERVAL 60 // NTP initial check every minute
#endif #endif
#ifndef NTP_UPDATE_INTERVAL
#define NTP_UPDATE_INTERVAL 1800 // NTP check every 30 minutes
#endif
#ifndef NTP_START_DELAY
#define NTP_START_DELAY 1000 // Delay NTP start 1 second
#endif
#ifndef NTP_DST_REGION #ifndef NTP_DST_REGION
#define NTP_DST_REGION 0 // 0 for Europe, 1 for USA (defined in NtpClientLib) #define NTP_DST_REGION 0 // 0 for Europe, 1 for USA (defined in NtpClientLib)
#endif #endif
#ifndef NTP_WAIT_FOR_SYNC
#define NTP_WAIT_FOR_SYNC 1 // Do not report any datetime until NTP sync'ed
#endif
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// ALEXA // ALEXA
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------


BIN
code/espurna/data/index.all.html.gz View File


BIN
code/espurna/data/index.light.html.gz View File


BIN
code/espurna/data/index.lightfox.html.gz View File


BIN
code/espurna/data/index.rfbridge.html.gz View File


BIN
code/espurna/data/index.rfm69.html.gz View File


BIN
code/espurna/data/index.sensor.html.gz View File


BIN
code/espurna/data/index.small.html.gz View File


BIN
code/espurna/data/index.thermostat.html.gz View File


+ 2
- 0
code/espurna/espurna.ino View File

@ -26,7 +26,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "button.h" #include "button.h"
#include "debug.h" #include "debug.h"
#include "led.h" #include "led.h"
#include "ntp.h"
#include "relay.h" #include "relay.h"
#include "rpnrules.h"
#include "settings.h" #include "settings.h"
#include "system.h" #include "system.h"
#include "tuya.h" #include "tuya.h"


+ 4
- 0
code/espurna/libs/NtpClientWrap.h View File

@ -4,6 +4,8 @@
#pragma once #pragma once
#if NTP_LEGACY_SUPPORT
#include <WiFiUdp.h> #include <WiFiUdp.h>
#include <NtpClientLib.h> #include <NtpClientLib.h>
@ -27,3 +29,5 @@ public:
// NOTE: original NTP should be discarded by the linker // NOTE: original NTP should be discarded by the linker
// TODO: allow NTP client object to be destroyed // TODO: allow NTP client object to be destroyed
NTPClientWrap NTPw; NTPClientWrap NTPw;
#endif

+ 5
- 1
code/espurna/libs/SecureClientHelpers.h View File

@ -185,7 +185,11 @@ struct SecureClientChecks {
client.setFingerprint(_buffer); client.setFingerprint(_buffer);
} }
} else if (check == SECURE_CLIENT_CHECK_CA) { } else if (check == SECURE_CLIENT_CHECK_CA) {
client.setX509Time(ntpLocal2UTC(now()));
#if NTP_LEGACY_SUPPORT
client.setX509Time(ntpLocal2UTC(now()));
#else
client.setX509Time(now());
#endif
if (!certs.getCount()) { if (!certs.getCount()) {
if (config.on_certificate) certs.append(config.on_certificate()); if (config.on_certificate) certs.append(config.on_certificate());
} }


+ 1
- 1
code/espurna/mqtt.ino View File

@ -16,10 +16,10 @@ Updated secure client support by Niek van der Maas < mail at niekvandermaas dot
#include <vector> #include <vector>
#include <utility> #include <utility>
#include <Ticker.h> #include <Ticker.h>
#include <TimeLib.h>
#include "system.h" #include "system.h"
#include "libs/SecureClientHelpers.h" #include "libs/SecureClientHelpers.h"
#include "ntp.h"
#include "ws.h" #include "ws.h"
#if MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT #if MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT


+ 52
- 0
code/espurna/ntp.h View File

@ -0,0 +1,52 @@
/*
NTP MODULE
*/
#pragma once
#include "broker.h"
// TODO: need this prototype for .ino
struct NtpCalendarWeekday;
#if NTP_SUPPORT
#if NTP_LEGACY_SUPPORT // Use legacy TimeLib and NtpClientLib
#include <TimeLib.h>
#include "libs/NtpClientWrap.h"
#else // POSIX time functions + configTime(...)
#include <lwip/apps/sntp.h>
#include <TZ.h>
#include "ntp_timelib.h"
#endif
// --- rest of the module is ESPurna functions
enum class NtpTick {
EveryMinute,
EveryHour
};
struct NtpCalendarWeekday {
int local_wday;
int local_hour;
int local_minute;
int utc_wday;
int utc_hour;
int utc_minute;
};
using NtpBroker = TBroker<TBrokerType::DATETIME, const NtpTick, time_t, const String&>;
String ntpDateTime(time_t ts);
String ntpDateTime();
void ntpSetup();
#endif // NTP_SUPPORT

+ 290
- 172
code/espurna/ntp.ino View File

@ -2,264 +2,371 @@
NTP MODULE NTP MODULE
Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
Based on esp8266 / esp32 configTime and C date and time functions:
- https://github.com/esp8266/Arduino/blob/master/libraries/esp8266/examples/NTP-TZ-DST/NTP-TZ-DST.ino
- https://www.nongnu.org/lwip/2_1_x/group__sntp.html
- man 3 ctime
Copyright (C) 2019 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
*/ */
#if NTP_SUPPORT
#if NTP_SUPPORT && !NTP_LEGACY_SUPPORT
#include <TimeLib.h>
#include <WiFiClient.h>
#include <Arduino.h>
#include <coredecls.h>
#include <Ticker.h> #include <Ticker.h>
#include "libs/NtpClientWrap.h"
static_assert(
(SNTP_SERVER_DNS == 1),
"lwip must be configured with SNTP_SERVER_DNS"
);
#include "config/buildtime.h"
#include "debug.h"
#include "broker.h" #include "broker.h"
#include "ws.h" #include "ws.h"
#include "ntp.h"
Ticker _ntp_defer;
// Arduino/esp8266 lwip2 custom functions that can be redefined
// Must return time in milliseconds, legacy settings are in seconds.
bool _ntp_report = false;
bool _ntp_configure = false;
bool _ntp_want_sync = false;
// -----------------------------------------------------------------------------
// NTP
// -----------------------------------------------------------------------------
String _ntp_server;
#if WEB_SUPPORT
uint32_t _ntp_startup_delay = (NTP_START_DELAY * 1000);
uint32_t _ntp_update_delay = (NTP_UPDATE_INTERVAL * 1000);
bool _ntpWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
return (strncmp(key, "ntp", 3) == 0);
uint32_t sntp_startup_delay_MS_rfc_not_less_than_60000() {
return _ntp_startup_delay;
} }
void _ntpWebSocketOnVisible(JsonObject& root) {
root["ntpVisible"] = 1;
uint32_t sntp_update_delay_MS_rfc_not_less_than_15000() {
return _ntp_update_delay;
} }
void _ntpWebSocketOnData(JsonObject& root) {
root["ntpStatus"] = (timeStatus() == timeSet);
}
// We also must shim TimeLib functions until everything else is ported.
// We can't sometimes avoid TimeLib as dependancy though, which would be really bad
void _ntpWebSocketOnConnected(JsonObject& root) {
root["ntpServer"] = getSetting("ntpServer", NTP_SERVER);
root["ntpOffset"] = getSetting("ntpOffset", NTP_TIME_OFFSET);
root["ntpDST"] = getSetting("ntpDST", 1 == NTP_DAY_LIGHT);
root["ntpRegion"] = getSetting("ntpRegion", NTP_DST_REGION);
}
static Ticker _ntp_broker_timer;
static bool _ntp_synced = false;
#endif
static time_t _ntp_last = 0;
static time_t _ntp_ts = 0;
time_t _ntpSyncProvider() {
_ntp_want_sync = true;
return 0;
}
static tm _ntp_tm_local;
static tm _ntp_tm_utc;
void _ntpWantSync() {
_ntp_want_sync = true;
static void _ntpTmCache(time_t ts) {
if (_ntp_ts != ts) {
_ntp_ts = ts;
localtime_r(&_ntp_ts, &_ntp_tm_local);
gmtime_r(&_ntp_ts, &_ntp_tm_utc);
}
} }
// Randomized in time to avoid clogging the server with simultaious requests from multiple devices
// (for example, when multiple devices start up at the same time)
int inline _ntpSyncInterval() {
return secureRandom(NTP_SYNC_INTERVAL, NTP_SYNC_INTERVAL * 2);
int hour(time_t ts) {
_ntpTmCache(ts);
return _ntp_tm_local.tm_hour;
} }
int inline _ntpUpdateInterval() {
return secureRandom(NTP_UPDATE_INTERVAL, NTP_UPDATE_INTERVAL * 2);
int minute(time_t ts) {
_ntpTmCache(ts);
return _ntp_tm_local.tm_min;
} }
void _ntpStart() {
_ntpConfigure();
int second(time_t ts) {
_ntpTmCache(ts);
return _ntp_tm_local.tm_sec;
}
// short (initial) and long (after sync) intervals
NTPw.setInterval(_ntpSyncInterval(), _ntpUpdateInterval());
DEBUG_MSG_P(PSTR("[NTP] Update intervals: %us / %us\n"),
NTPw.getShortInterval(), NTPw.getLongInterval());
int day(time_t ts) {
_ntpTmCache(ts);
return _ntp_tm_local.tm_mday;
}
// setSyncProvider will immediatly call given function by setting next sync time to the current time.
// Avoid triggering sync immediatly by canceling sync provider flag and resetting sync interval again
setSyncProvider(_ntpSyncProvider);
_ntp_want_sync = false;
// `tm.tm_wday` range is 0..6, TimeLib is 1..7
int weekday(time_t ts) {
_ntpTmCache(ts);
return _ntp_tm_local.tm_wday + 1;
}
setSyncInterval(NTPw.getShortInterval());
// `tm.tm_mon` range is 0..11, TimeLib range is 1..12
int month(time_t ts) {
_ntpTmCache(ts);
return _ntp_tm_local.tm_mon + 1;
}
int year(time_t ts) {
_ntpTmCache(ts);
return _ntp_tm_local.tm_year + 1900;
} }
void _ntpConfigure() {
int utc_hour(time_t ts) {
_ntpTmCache(ts);
return _ntp_tm_utc.tm_hour;
}
_ntp_configure = false;
int utc_minute(time_t ts) {
_ntpTmCache(ts);
return _ntp_tm_utc.tm_min;
}
int offset = getSetting("ntpOffset", NTP_TIME_OFFSET);
int sign = offset > 0 ? 1 : -1;
offset = abs(offset);
int tz_hours = sign * (offset / 60);
int tz_minutes = sign * (offset % 60);
if (NTPw.getTimeZone() != tz_hours || NTPw.getTimeZoneMinutes() != tz_minutes) {
NTPw.setTimeZone(tz_hours, tz_minutes);
_ntp_report = true;
}
int utc_second(time_t ts) {
_ntpTmCache(ts);
return _ntp_tm_utc.tm_sec;
}
const bool daylight = getSetting("ntpDST", 1 == NTP_DAY_LIGHT);
if (NTPw.getDayLight() != daylight) {
NTPw.setDayLight(daylight);
_ntp_report = true;
}
int utc_day(time_t ts) {
_ntpTmCache(ts);
return _ntp_tm_utc.tm_mday;
}
String server = getSetting("ntpServer", NTP_SERVER);
if (!NTPw.getNtpServerName().equals(server)) {
NTPw.setNtpServerName(server);
}
int utc_weekday(time_t ts) {
_ntpTmCache(ts);
return _ntp_tm_utc.tm_wday + 1;
}
uint8_t dst_region = getSetting("ntpRegion", NTP_DST_REGION);
NTPw.setDSTZone(dst_region);
int utc_month(time_t ts) {
_ntpTmCache(ts);
return _ntp_tm_utc.tm_mon + 1;
}
// Some remote servers can be slow to respond, increase accordingly
// TODO does this need upper constrain?
NTPw.setNTPTimeout(getSetting("ntpTimeout", NTP_TIMEOUT));
int utc_year(time_t ts) {
_ntpTmCache(ts);
return _ntp_tm_utc.tm_year + 1900;
}
time_t now() {
return time(nullptr);
} }
void _ntpReport() {
// -----------------------------------------------------------------------------
_ntp_report = false;
#if WEB_SUPPORT
#if DEBUG_SUPPORT
if (ntpSynced()) {
time_t t = now();
DEBUG_MSG_P(PSTR("[NTP] UTC Time : %s\n"), ntpDateTime(ntpLocal2UTC(t)).c_str());
DEBUG_MSG_P(PSTR("[NTP] Local Time: %s\n"), ntpDateTime(t).c_str());
}
#endif
bool _ntpWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
return (strncmp(key, "ntp", 3) == 0);
}
void _ntpWebSocketOnVisible(JsonObject& root) {
root["ntpVisible"] = 1;
root["ntplwipVisible"] = 1;
} }
#if BROKER_SUPPORT
void _ntpWebSocketOnData(JsonObject& root) {
root["ntpStatus"] = ntpSynced();
}
void inline _ntpBroker() {
static unsigned char last_minute = 60;
if (ntpSynced() && (minute() != last_minute)) {
last_minute = minute();
TimeBroker::Publish(MQTT_TOPIC_DATETIME, now(), ntpDateTime());
}
void _ntpWebSocketOnConnected(JsonObject& root) {
root["ntpServer"] = getSetting("ntpServer", F(NTP_SERVER));
root["ntpTZ"] = getSetting("ntpTZ", NTP_TIMEZONE);
} }
#endif #endif
void _ntpLoop() {
// TODO: mention possibility of multiple servers
String _ntpGetServer() {
String server;
// Disable ntp sync when softAP is active. This will not crash, but instead spam debug-log with pointless sync failures.
if (!wifiConnected()) return;
server = sntp_getservername(0);
if (!server.length()) {
server = IPAddress(sntp_getserver(0)).toString();
}
if (_ntp_configure) _ntpConfigure();
return server;
}
// NTPClientLib will trigger callback with sync status
// see: NTPw.onNTPSyncEvent([](NTPSyncEvent_t error){ ... }) below
if (_ntp_want_sync) {
_ntp_want_sync = false;
NTPw.getTime();
void _ntpReport() {
if (!ntpSynced()) {
DEBUG_MSG_P(PSTR("[NTP] Not synced\n"));
return;
} }
// Print current time whenever configuration changes or after successful sync
if (_ntp_report) _ntpReport();
tm utc_tm;
tm sync_tm;
#if BROKER_SUPPORT
_ntpBroker();
#endif
auto ts = now();
gmtime_r(&ts, &utc_tm);
gmtime_r(&_ntp_last, &sync_tm);
DEBUG_MSG_P(PSTR("[NTP] Server : %s\n"), _ntp_server.c_str());
DEBUG_MSG_P(PSTR("[NTP] Sync Time : %s (UTC)\n"), ntpDateTime(&sync_tm).c_str());
DEBUG_MSG_P(PSTR("[NTP] UTC Time : %s\n"), ntpDateTime(&utc_tm).c_str());
const char* cfg_tz = getenv("TZ");
if ((cfg_tz != nullptr) && (strcmp(cfg_tz, "UTC0") != 0)) {
tm local_tm;
localtime_r(&ts, &local_tm);
DEBUG_MSG_P(PSTR("[NTP] Local Time : %s (%s)\n"),
ntpDateTime(&local_tm).c_str(), cfg_tz
);
}
} }
// TODO: remove me!
void _ntpBackwards() {
moveSetting("ntpServer1", "ntpServer");
delSetting("ntpServer2");
delSetting("ntpServer3");
int offset = getSetting("ntpOffset", NTP_TIME_OFFSET);
if (-30 < offset && offset < 30) {
offset *= 60;
setSetting("ntpOffset", offset);
void _ntpConfigure() {
// Note: TZ_... provided by the Core are already wrapped with PSTR(...)
const auto cfg_tz = getSetting("ntpTZ", NTP_TIMEZONE);
const char* active_tz = getenv("TZ");
if (cfg_tz != active_tz) {
setenv("TZ", cfg_tz.c_str(), 1);
tzset();
}
const auto cfg_server = getSetting("ntpServer", F(NTP_SERVER));
const auto active_server = _ntpGetServer();
if (cfg_tz != active_tz) {
_ntp_server = cfg_server;
configTime(cfg_tz.c_str(), _ntp_server.c_str());
DEBUG_MSG_P(PSTR("[NTP] Server: %s, TZ: %s\n"), cfg_server.c_str(), cfg_tz.length() ? cfg_tz.c_str() : "UTC0");
} }
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
bool ntpSynced() { bool ntpSynced() {
#if NTP_WAIT_FOR_SYNC
// Has synced at least once
return (NTPw.getFirstSync() > 0);
#else
// TODO: runtime setting?
return true;
#endif
return _ntp_synced;
} }
String ntpDateTime(time_t t) {
String ntpDateTime(tm* timestruct) {
char buffer[20]; char buffer[20];
snprintf_P(buffer, sizeof(buffer), snprintf_P(buffer, sizeof(buffer),
PSTR("%04d-%02d-%02d %02d:%02d:%02d"), PSTR("%04d-%02d-%02d %02d:%02d:%02d"),
year(t), month(t), day(t), hour(t), minute(t), second(t)
timestruct->tm_year + 1900,
timestruct->tm_mon + 1,
timestruct->tm_mday,
timestruct->tm_hour,
timestruct->tm_min,
timestruct->tm_sec
); );
return String(buffer); return String(buffer);
} }
String ntpDateTime(time_t ts) {
tm timestruct;
localtime_r(&ts, &timestruct);
return ntpDateTime(&timestruct);
}
String ntpDateTime() { String ntpDateTime() {
if (ntpSynced()) return ntpDateTime(now());
if (ntpSynced()) {
return ntpDateTime(now());
}
return String(); return String();
} }
// XXX: returns garbage during DST switch
time_t ntpLocal2UTC(time_t local) {
int offset = getSetting("ntpOffset", NTP_TIME_OFFSET);
if (NTPw.isSummerTime()) offset += 60;
return local - offset * 60;
// -----------------------------------------------------------------------------
#if BROKER_SUPPORT
// XXX: Nonos docs for some reason mention 100 micros as minimum time. Schedule next second in case this is 0
void _ntpBrokerSchedule(int offset) {
_ntp_broker_timer.once_scheduled(offset ?: 1, _ntpBrokerCallback);
}
void _ntpBrokerCallback() {
if (!ntpSynced()) {
_ntpBrokerSchedule(60);
return;
}
const auto ts = now();
// current time and formatter string is in local TZ
tm local_tm;
localtime_r(&ts, &local_tm);
int now_hour = local_tm.tm_hour;
int now_minute = local_tm.tm_min;
static int last_hour = -1;
static int last_minute = -1;
String datetime;
if ((last_minute != now_minute) || (last_hour != now_hour)) {
datetime = ntpDateTime(&local_tm);
}
// notify subscribers about each tick interval (note that both can happen simultaneously)
if (last_hour != now_hour) {
last_hour = now_hour;
NtpBroker::Publish(NtpTick::EveryHour, ts, datetime.c_str());
}
if (last_minute != now_minute) {
last_minute = now_minute;
NtpBroker::Publish(NtpTick::EveryMinute, ts, datetime.c_str());
}
// try to autocorrect each invocation
_ntpBrokerSchedule(60 - local_tm.tm_sec);
}
#endif
void _ntpSetTimeOfDayCallback() {
_ntp_synced = true;
_ntp_last = time(nullptr);
#if BROKER_SUPPORT
static bool once = true;
if (once) {
schedule_function(_ntpBrokerCallback);
once = false;
}
#endif
#if WEB_SUPPORT
wsPost(_ntpWebSocketOnData);
#endif
schedule_function(_ntpReport);
}
void _ntpSetTimestamp(time_t ts) {
timeval tv { ts, 0 };
timezone tz { 0, 0 };
settimeofday(&tv, &tz);
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
void ntpSetup() { void ntpSetup() {
_ntpBackwards();
// Randomized in time to avoid clogging the server with simultaneous requests from multiple devices
// (for example, when multiple devices start up at the same time)
const uint32_t startup_delay = getSetting("ntpStartDelay", NTP_START_DELAY);
const uint32_t update_delay = getSetting("ntpUpdateIntvl", NTP_UPDATE_INTERVAL);
#if TERMINAL_SUPPORT
terminalRegisterCommand(F("NTP"), [](Embedis* e) {
if (ntpSynced()) {
_ntpReport();
terminalOK();
} else {
DEBUG_MSG_P(PSTR("[NTP] Not synced\n"));
}
});
_ntp_startup_delay = secureRandom(startup_delay, startup_delay * 2);
_ntp_update_delay = secureRandom(update_delay, update_delay * 2);
DEBUG_MSG_P(PSTR("[NTP] Startup delay: %us, Update delay: %us\n"),
_ntp_startup_delay, _ntp_update_delay
);
terminalRegisterCommand(F("NTP.SYNC"), [](Embedis* e) {
_ntpWantSync();
terminalOK();
});
#endif
_ntp_startup_delay = _ntp_startup_delay * 1000;
_ntp_update_delay = _ntp_update_delay * 1000;
NTPw.onNTPSyncEvent([](NTPSyncEvent_t error) {
if (error) {
if (error == noResponse) {
DEBUG_MSG_P(PSTR("[NTP] Error: NTP server not reachable\n"));
} else if (error == invalidAddress) {
DEBUG_MSG_P(PSTR("[NTP] Error: Invalid NTP server address\n"));
}
#if WEB_SUPPORT
wsPost(_ntpWebSocketOnData);
#endif
} else {
_ntp_report = true;
setTime(NTPw.getLastNTPSync());
}
});
// start up with some reasonable timestamp already available
_ntpSetTimestamp(__UNIX_TIMESTAMP__);
// will be called every time after ntp syncs AND loop() finishes
settimeofday_cb(_ntpSetTimeOfDayCallback);
wifiRegister([](justwifi_messages_t code, char * parameter) {
if (code == MESSAGE_CONNECTED) {
if (!ntpSynced()) {
_ntp_defer.once_ms(secureRandom(NTP_START_DELAY, NTP_START_DELAY * 15), _ntpWantSync);
}
// generic configuration, always handled
espurnaRegisterReload(_ntpConfigure);
_ntpConfigure();
// make sure our logic does know about the actual server
// in case dhcp sends out ntp settings
static WiFiEventHandler on_sta = WiFi.onStationModeGotIP([](WiFiEventStationModeGotIP) {
const auto server = _ntpGetServer();
if (sntp_enabled() && (!_ntp_server.length() || (server != _ntp_server))) {
DEBUG_MSG_P(PSTR("[NTP] Updating `ntpServer` setting from DHCP: %s\n"), server.c_str());
_ntp_server = server;
setSetting("ntpServer", server);
} }
}); });
// optional functionality
#if WEB_SUPPORT #if WEB_SUPPORT
wsRegister() wsRegister()
.onVisible(_ntpWebSocketOnVisible) .onVisible(_ntpWebSocketOnVisible)
@ -268,13 +375,24 @@ void ntpSetup() {
.onKeyCheck(_ntpWebSocketOnKeyCheck); .onKeyCheck(_ntpWebSocketOnKeyCheck);
#endif #endif
// Main callbacks
espurnaRegisterLoop(_ntpLoop);
espurnaRegisterReload([]() { _ntp_configure = true; });
#if TERMINAL_SUPPORT
terminalRegisterCommand(F("NTP"), [](Embedis* e) {
_ntpReport();
terminalOK();
});
terminalRegisterCommand(F("NTP.SETTIME"), [](Embedis* e) {
if (e->argc != 2) return;
_ntp_synced = true;
_ntpSetTimestamp(String(e->argv[1]).toInt());
terminalOK();
});
// Sets up NTP instance, installs ours sync provider
_ntpStart();
// TODO:
// terminalRegisterCommand(F("NTP.SYNC"), [](Embedis* e) { ... }
//
#endif
} }
#endif // NTP_SUPPORT
#endif // NTP_SUPPORT && !NTP_LEGACY_SUPPORT

+ 279
- 0
code/espurna/ntp_legacy.ino View File

@ -0,0 +1,279 @@
/*
NTP MODULE (based on NtpClientLib)
Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
*/
#if NTP_LEGACY_SUPPORT && NTP_SUPPORT
#include <Ticker.h>
#include "broker.h"
#include "ws.h"
#include "ntp.h"
Ticker _ntp_defer;
bool _ntp_report = false;
bool _ntp_configure = false;
bool _ntp_want_sync = false;
// -----------------------------------------------------------------------------
// NTP
// -----------------------------------------------------------------------------
#if WEB_SUPPORT
bool _ntpWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
return (strncmp(key, "ntp", 3) == 0);
}
void _ntpWebSocketOnVisible(JsonObject& root) {
root["ntpVisible"] = 1;
root["ntplegacyVisible"] = 1;
}
void _ntpWebSocketOnData(JsonObject& root) {
root["ntpStatus"] = (timeStatus() == timeSet);
}
void _ntpWebSocketOnConnected(JsonObject& root) {
root["ntpServer"] = getSetting("ntpServer", NTP_SERVER);
root["ntpOffset"] = getSetting("ntpOffset", NTP_TIME_OFFSET);
root["ntpDST"] = getSetting("ntpDST", 1 == NTP_DAY_LIGHT);
root["ntpRegion"] = getSetting("ntpRegion", NTP_DST_REGION);
}
#endif
time_t _ntpSyncProvider() {
_ntp_want_sync = true;
return 0;
}
void _ntpWantSync() {
_ntp_want_sync = true;
}
// Randomized in time to avoid clogging the server with simultaious requests from multiple devices
// (for example, when multiple devices start up at the same time)
int _ntpSyncInterval() {
return secureRandom(NTP_SYNC_INTERVAL, NTP_SYNC_INTERVAL * 2);
}
int _ntpUpdateInterval() {
return secureRandom(NTP_UPDATE_INTERVAL, NTP_UPDATE_INTERVAL * 2);
}
void _ntpStart() {
_ntpConfigure();
// short (initial) and long (after sync) intervals
NTPw.setInterval(_ntpSyncInterval(), _ntpUpdateInterval());
DEBUG_MSG_P(PSTR("[NTP] Update intervals: %us / %us\n"),
NTPw.getShortInterval(), NTPw.getLongInterval());
// setSyncProvider will immediatly call given function by setting next sync time to the current time.
// Avoid triggering sync immediatly by canceling sync provider flag and resetting sync interval again
setSyncProvider(_ntpSyncProvider);
_ntp_want_sync = false;
setSyncInterval(NTPw.getShortInterval());
}
void _ntpConfigure() {
_ntp_configure = false;
int offset = getSetting("ntpOffset", NTP_TIME_OFFSET);
int sign = offset > 0 ? 1 : -1;
offset = abs(offset);
int tz_hours = sign * (offset / 60);
int tz_minutes = sign * (offset % 60);
if (NTPw.getTimeZone() != tz_hours || NTPw.getTimeZoneMinutes() != tz_minutes) {
NTPw.setTimeZone(tz_hours, tz_minutes);
_ntp_report = true;
}
const bool daylight = getSetting("ntpDST", 1 == NTP_DAY_LIGHT);
if (NTPw.getDayLight() != daylight) {
NTPw.setDayLight(daylight);
_ntp_report = true;
}
const auto server = getSetting("ntpServer", NTP_SERVER);
if (!NTPw.getNtpServerName().equals(server)) {
NTPw.setNtpServerName(server);
}
uint8_t dst_region = getSetting("ntpRegion", NTP_DST_REGION);
NTPw.setDSTZone(dst_region);
// Some remote servers can be slow to respond, increase accordingly
// TODO does this need upper constrain?
NTPw.setNTPTimeout(getSetting("ntpTimeout", NTP_TIMEOUT));
}
void _ntpReport() {
_ntp_report = false;
#if DEBUG_SUPPORT
if (ntpSynced()) {
time_t t = now();
DEBUG_MSG_P(PSTR("[NTP] UTC Time : %s\n"), ntpDateTime(ntpLocal2UTC(t)).c_str());
DEBUG_MSG_P(PSTR("[NTP] Local Time: %s\n"), ntpDateTime(t).c_str());
}
#endif
}
#if BROKER_SUPPORT
void inline _ntpBroker() {
static unsigned char last_minute = 60;
if (ntpSynced() && (minute() != last_minute)) {
last_minute = minute();
NtpBroker::Publish(NtpTick::EveryMinute, now(), ntpDateTime());
}
}
#endif
void _ntpLoop() {
// Disable ntp sync when softAP is active. This will not crash, but instead spam debug-log with pointless sync failures.
if (!wifiConnected()) return;
if (_ntp_configure) _ntpConfigure();
// NTPClientLib will trigger callback with sync status
// see: NTPw.onNTPSyncEvent([](NTPSyncEvent_t error){ ... }) below
if (_ntp_want_sync) {
_ntp_want_sync = false;
NTPw.getTime();
}
// Print current time whenever configuration changes or after successful sync
if (_ntp_report) _ntpReport();
#if BROKER_SUPPORT
_ntpBroker();
#endif
}
// TODO: remove me!
void _ntpBackwards() {
moveSetting("ntpServer1", "ntpServer");
delSetting("ntpServer2");
delSetting("ntpServer3");
int offset = getSetting("ntpOffset", NTP_TIME_OFFSET);
if (-30 < offset && offset < 30) {
offset *= 60;
setSetting("ntpOffset", offset);
}
}
// -----------------------------------------------------------------------------
bool ntpSynced() {
#if NTP_WAIT_FOR_SYNC
// Has synced at least once
return (NTPw.getFirstSync() > 0);
#else
// TODO: runtime setting?
return true;
#endif
}
String ntpDateTime(time_t t) {
char buffer[20];
snprintf_P(buffer, sizeof(buffer),
PSTR("%04d-%02d-%02d %02d:%02d:%02d"),
year(t), month(t), day(t), hour(t), minute(t), second(t)
);
return String(buffer);
}
String ntpDateTime() {
if (ntpSynced()) return ntpDateTime(now());
return String();
}
// XXX: returns garbage during DST switch
time_t ntpLocal2UTC(time_t local) {
int offset = getSetting("ntpOffset", NTP_TIME_OFFSET);
if (NTPw.isSummerTime()) offset += 60;
return local - offset * 60;
}
// -----------------------------------------------------------------------------
void ntpSetup() {
_ntpBackwards();
#if TERMINAL_SUPPORT
terminalRegisterCommand(F("NTP"), [](Embedis* e) {
if (ntpSynced()) {
_ntpReport();
terminalOK();
} else {
DEBUG_MSG_P(PSTR("[NTP] Not synced\n"));
}
});
terminalRegisterCommand(F("NTP.SYNC"), [](Embedis* e) {
_ntpWantSync();
terminalOK();
});
#endif
NTPw.onNTPSyncEvent([](NTPSyncEvent_t error) {
if (error) {
if (error == noResponse) {
DEBUG_MSG_P(PSTR("[NTP] Error: NTP server not reachable\n"));
} else if (error == invalidAddress) {
DEBUG_MSG_P(PSTR("[NTP] Error: Invalid NTP server address\n"));
}
#if WEB_SUPPORT
wsPost(_ntpWebSocketOnData);
#endif
} else {
_ntp_report = true;
setTime(NTPw.getLastNTPSync());
}
});
wifiRegister([](justwifi_messages_t code, char * parameter) {
if (code == MESSAGE_CONNECTED) {
if (!ntpSynced()) {
_ntp_defer.once(secureRandom(NTP_START_DELAY, NTP_START_DELAY * 2), _ntpWantSync);
}
}
});
#if WEB_SUPPORT
wsRegister()
.onVisible(_ntpWebSocketOnVisible)
.onConnected(_ntpWebSocketOnConnected)
.onData(_ntpWebSocketOnData)
.onKeyCheck(_ntpWebSocketOnKeyCheck);
#endif
// Main callbacks
espurnaRegisterLoop(_ntpLoop);
espurnaRegisterReload([]() { _ntp_configure = true; });
// Sets up NTP instance, installs ours sync provider
_ntpStart();
}
#endif // NTP_SUPPORT && NTP_LEGACY_SUPPORT

+ 108
- 0
code/espurna/ntp_timelib.h View File

@ -0,0 +1,108 @@
/*
Part of NTP MODULE
*/
// Based on https://github.com/PaulStoffregen/time
#pragma once
#include <time.h>
#include <sys/time.h>
constexpr time_t daysPerWeek = 7;
constexpr time_t secondsPerMinute = 60;
constexpr time_t secondsPerHour = 3600;
constexpr time_t secondsPerDay = secondsPerHour * 24;
constexpr time_t secondsPerWeek = daysPerWeek * secondsPerDay;
constexpr time_t secondsPerYear = secondsPerWeek * 52;
constexpr time_t secondsY2K = 946684800; // the time at the start of y2k
// wall clock values
template <typename T>
constexpr const T numberOfSeconds(T ts) {
return (ts % (T)secondsPerMinute);
}
template <typename T>
constexpr const T numberOfMinutes(T ts) {
return ((ts / (T)secondsPerMinute) % (T)secondsPerMinute);
}
template <typename T>
constexpr const time_t numberOfHours(T ts) {
return ((ts % (T)secondsPerDay) / (T)secondsPerHour);
}
// week starts with sunday as number 1, monday as 2 etc.
constexpr const int dayOfWeek(time_t ts) {
return ((ts / secondsPerDay + 4) % daysPerWeek) + 1;
}
// the number of days since 0 (Jan 1 1970 in case of time_t values)
constexpr const int elapsedDays(uint32_t ts) {
return (ts / secondsPerDay);
}
// the number of seconds since last midnight
constexpr const uint32_t elapsedSecsToday(uint32_t ts) {
return (ts % (uint32_t)secondsPerDay);
}
// note that week starts on day 1
constexpr const uint32_t elapsedSecsThisWeek(uint32_t ts) {
return elapsedSecsToday(ts) + ((dayOfWeek(ts) - 1) * (uint32_t)secondsPerDay);
}
// The following methods 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
// time at the start of the given day
constexpr const time_t previousMidnight(time_t ts) {
return ((ts / secondsPerDay) * secondsPerDay);
}
// time at the end of the given day
constexpr const time_t nextMidnight(time_t ts) {
return previousMidnight(ts) + secondsPerDay;
}
// time at the start of the week for the given time
constexpr const time_t previousSunday(time_t ts) {
return ts - elapsedSecsThisWeek(ts);
}
// time at the end of the week for the given time
constexpr const time_t nextSunday(time_t ts) {
return previousSunday(ts) + secondsPerWeek;
}
int utc_hour(time_t ts);
int utc_minute(time_t ts);
int utc_second(time_t ts);
int utc_day(time_t ts);
int utc_weekday(time_t ts);
int utc_month(time_t ts);
int utc_year(time_t ts);
int hour(time_t ts);
int minute(time_t ts);
int second(time_t ts);
int day(time_t ts);
int weekday(time_t ts);
int month(time_t ts);
int year(time_t ts);
int hour();
int minute();
int second();
int day();
int weekday();
int month();
int year();
time_t now();

+ 20
- 0
code/espurna/rpnrules.h View File

@ -0,0 +1,20 @@
/*
RPN RULES MODULE
Use RPNLib library (https://github.com/xoseperez/rpnlib)
Copyright (C) 2019 by Xose Pérez <xose dot perez at gmail dot com>
*/
#pragma once
// TODO: need this prototype for .ino
struct rpn_context;
#if RPN_RULES_SUPPORT
#include <rpnlib.h>
void rpnSetup();
#endif

+ 118
- 44
code/espurna/rpnrules.ino View File

@ -8,9 +8,9 @@ Copyright (C) 2019 by Xose Pérez <xose dot perez at gmail dot com>
#if RPN_RULES_SUPPORT #if RPN_RULES_SUPPORT
#include "ntp.h"
#include "relay.h" #include "relay.h"
#include <rpnlib.h>
#include "rpnrules.h"
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Custom commands // Custom commands
@ -115,57 +115,114 @@ void _rpnBrokerStatus(const String& topic, unsigned char id, unsigned int value)
_rpnBrokerCallback(topic, id, double(value), nullptr); _rpnBrokerCallback(topic, id, double(value), nullptr);
} }
#if NTP_SUPPORT
bool _rpnNtpNow(rpn_context & ctxt) {
if (!ntpSynced()) return false;
rpn_stack_push(ctxt, now());
return true;
}
bool _rpnNtpFunc(rpn_context & ctxt, int (*func)(time_t)) {
float timestamp;
rpn_stack_pop(ctxt, timestamp);
rpn_stack_push(ctxt, func(time_t(timestamp)));
return true;
}
#endif
void _rpnInit() { void _rpnInit() {
// Init context // Init context
rpn_init(_rpn_ctxt); rpn_init(_rpn_ctxt);
// Time functions
rpn_operator_set(_rpn_ctxt, "now", 0, [](rpn_context & ctxt) {
if (!ntpSynced()) return false;
rpn_stack_push(ctxt, now());
return true;
});
rpn_operator_set(_rpn_ctxt, "utc", 0, [](rpn_context & ctxt) {
if (!ntpSynced()) return false;
rpn_stack_push(ctxt, ntpLocal2UTC(now()));
return true;
});
rpn_operator_set(_rpn_ctxt, "dow", 1, [](rpn_context & ctxt) {
float a;
rpn_stack_pop(ctxt, a);
unsigned char dow = (weekday(int(a)) + 5) % 7;
rpn_stack_push(ctxt, dow);
return true;
});
rpn_operator_set(_rpn_ctxt, "hour", 1, [](rpn_context & ctxt) {
float a;
rpn_stack_pop(ctxt, a);
rpn_stack_push(ctxt, hour(int(a)));
return true;
});
rpn_operator_set(_rpn_ctxt, "minute", 1, [](rpn_context & ctxt) {
float a;
rpn_stack_pop(ctxt, a);
rpn_stack_push(ctxt, minute(int(a)));
return true;
});
// Time functions need NTP support
// TODO: since 1.14.2, timelib+ntpclientlib are no longer used with latest Cores
// `now` is always in UTC, `utc_...` functions to be used instead to convert time
#if NTP_SUPPORT && !NTP_LEGACY_SUPPORT
rpn_operator_set(_rpn_ctxt, "utc", 0, _rpnNtpNow);
rpn_operator_set(_rpn_ctxt, "now", 0, _rpnNtpNow);
rpn_operator_set(_rpn_ctxt, "utc_month", 1, [](rpn_context & ctxt) {
return _rpnNtpFunc(ctxt, utc_month);
});
rpn_operator_set(_rpn_ctxt, "month", 1, [](rpn_context & ctxt) {
return _rpnNtpFunc(ctxt, month);
});
rpn_operator_set(_rpn_ctxt, "utc_day", 1, [](rpn_context & ctxt) {
return _rpnNtpFunc(ctxt, utc_day);
});
rpn_operator_set(_rpn_ctxt, "day", 1, [](rpn_context & ctxt) {
return _rpnNtpFunc(ctxt, day);
});
rpn_operator_set(_rpn_ctxt, "utc_dow", 1, [](rpn_context & ctxt) {
return _rpnNtpFunc(ctxt, utc_weekday);
});
rpn_operator_set(_rpn_ctxt, "dow", 1, [](rpn_context & ctxt) {
return _rpnNtpFunc(ctxt, weekday);
});
// Debug
rpn_operator_set(_rpn_ctxt, "utc_hour", 1, [](rpn_context & ctxt) {
return _rpnNtpFunc(ctxt, utc_hour);
});
rpn_operator_set(_rpn_ctxt, "hour", 1, [](rpn_context & ctxt) {
return _rpnNtpFunc(ctxt, hour);
});
rpn_operator_set(_rpn_ctxt, "utc_minute", 1, [](rpn_context & ctxt) {
return _rpnNtpFunc(ctxt, utc_minute);
});
rpn_operator_set(_rpn_ctxt, "minute", 1, [](rpn_context & ctxt) {
return _rpnNtpFunc(ctxt, minute);
});
#endif
// TODO: 1.14.0 weekday(...) conversion seemed to have 0..6 range with Monday as 0
// using classic Sunday as first, but instead of 0 it is 1
// Implementation above also uses 1 for Sunday, staying compatible with TimeLib
#if NTP_SUPPORT && NTP_LEGACY_SUPPORT
rpn_operator_set(_rpn_ctxt, "utc", 0, [](rpn_context & ctxt) {
if (!ntpSynced()) return false;
rpn_stack_push(ctxt, ntpLocal2UTC(now()));
return true;
});
rpn_operator_set(_rpn_ctxt, "now", 0, _rpnNtpNow);
rpn_operator_set(_rpn_ctxt, "month", 1, [](rpn_context & ctxt) {
return _rpnNtpFunc(ctxt, month);
});
rpn_operator_set(_rpn_ctxt, "day", 1, [](rpn_context & ctxt) {
return _rpnNtpFunc(ctxt, day);
});
rpn_operator_set(_rpn_ctxt, "dow", 1, [](rpn_context & ctxt) {
return _rpnNtpFunc(ctxt, weekday);
});
rpn_operator_set(_rpn_ctxt, "hour", 1, [](rpn_context & ctxt) {
return _rpnNtpFunc(ctxt, hour);
});
rpn_operator_set(_rpn_ctxt, "minute", 1, [](rpn_context & ctxt) {
return _rpnNtpFunc(ctxt, minute);
});
#endif
// Dumps RPN stack contents
rpn_operator_set(_rpn_ctxt, "debug", 0, [](rpn_context & ctxt) { rpn_operator_set(_rpn_ctxt, "debug", 0, [](rpn_context & ctxt) {
_rpnDump(); _rpnDump();
return true; return true;
}); });
// Relay operators
// Accept relay number and numeric API status value (0, 1 and 2)
rpn_operator_set(_rpn_ctxt, "relay", 2, [](rpn_context & ctxt) { rpn_operator_set(_rpn_ctxt, "relay", 2, [](rpn_context & ctxt) {
float a, b;
rpn_stack_pop(ctxt, b); // relay number
rpn_stack_pop(ctxt, a); // new status
if (int(a) == 2) {
relayToggle(int(b));
float status, id;
rpn_stack_pop(ctxt, id);
rpn_stack_pop(ctxt, status);
if (int(status) == 2) {
relayToggle(int(id));
} else { } else {
relayStatus(int(b), int(a) == 1);
relayStatus(int(id), int(status) == 1);
} }
return true; return true;
}); });
@ -184,10 +241,10 @@ void _rpnInit() {
}); });
rpn_operator_set(_rpn_ctxt, "channel", 2, [](rpn_context & ctxt) { rpn_operator_set(_rpn_ctxt, "channel", 2, [](rpn_context & ctxt) {
float a, b;
rpn_stack_pop(ctxt, b); // channel number
rpn_stack_pop(ctxt, a); // new value
lightChannel(int(b), int(a));
float value, id;
rpn_stack_pop(ctxt, id);
rpn_stack_pop(ctxt, value);
lightChannel(int(id), int(value));
return true; return true;
}); });
@ -306,6 +363,23 @@ void rpnSetup() {
mqttRegister(_rpnMQTTCallback); mqttRegister(_rpnMQTTCallback);
#endif #endif
#if NTP_SUPPORT
NtpBroker::Register([](const NtpTick tick, time_t timestamp, const String& datetime) {
static const String tick_every_hour(F("tick1h"));
static const String tick_every_minute(F("tick1m"));
const char* ptr =
(tick == NtpTick::EveryMinute) ? tick_every_minute.c_str() :
(tick == NtpTick::EveryHour) ? tick_every_hour.c_str() : nullptr;
if (ptr != nullptr) {
rpn_variable_set(_rpn_ctxt, ptr, timestamp);
_rpn_last = millis();
_rpn_run = true;
}
});
#endif
StatusBroker::Register(_rpnBrokerStatus); StatusBroker::Register(_rpnBrokerStatus);
SensorReadBroker::Register(_rpnBrokerCallback); SensorReadBroker::Register(_rpnBrokerCallback);


+ 80
- 55
code/espurna/scheduler.ino View File

@ -9,9 +9,11 @@ Adapted by Xose Pérez <xose dot perez at gmail dot com>
#if SCHEDULER_SUPPORT #if SCHEDULER_SUPPORT
#include "broker.h"
#include "relay.h" #include "relay.h"
#include "ntp.h"
#include <TimeLib.h>
constexpr const int SchedulerDummySwitchId = 0xff;
int _sch_restore = 0; int _sch_restore = 0;
@ -76,8 +78,8 @@ void _schConfigure() {
for (unsigned char i = 0; i < SCHEDULER_MAX_SCHEDULES; i++) { for (unsigned char i = 0; i < SCHEDULER_MAX_SCHEDULES; i++) {
int sch_switch = getSetting({"schSwitch", i}, 0xFF);
if (sch_switch == 0xFF) delete_flag = true;
int sch_switch = getSetting({"schSwitch", i}, SchedulerDummySwitchId);
if (sch_switch == SchedulerDummySwitchId) delete_flag = true;
if (delete_flag) { if (delete_flag) {
@ -118,10 +120,10 @@ void _schConfigure() {
} }
bool _schIsThisWeekday(time_t t, String weekdays){
bool _schIsThisWeekday(int day, const String& weekdays){
// Convert from Sunday to Monday as day 1 // Convert from Sunday to Monday as day 1
int w = weekday(t) - 1;
int w = day - 1;
if (0 == w) w = 7; if (0 == w) w = 7;
char pch; char pch;
@ -134,10 +136,8 @@ bool _schIsThisWeekday(time_t t, String weekdays){
} }
int _schMinutesLeft(time_t t, unsigned char schedule_hour, unsigned char schedule_minute){
unsigned char now_hour = hour(t);
unsigned char now_minute = minute(t);
return (schedule_hour - now_hour) * 60 + schedule_minute - now_minute;
int _schMinutesLeft(int current_hour, int current_minute, int schedule_hour, int schedule_minute) {
return (schedule_hour - current_hour) * 60 + schedule_minute - current_minute;
} }
void _schAction(unsigned char sch_id, int sch_action, int sch_switch) { void _schAction(unsigned char sch_id, int sch_action, int sch_switch) {
@ -161,45 +161,82 @@ void _schAction(unsigned char sch_id, int sch_action, int sch_switch) {
#endif #endif
} }
#if NTP_LEGACY_SUPPORT
NtpCalendarWeekday _schGetWeekday(time_t timestamp, int daybefore) {
if (daybefore > 0) {
timestamp = timestamp - ((hour(timestamp) * SECS_PER_HOUR) + ((minute(timestamp) + 1) * SECS_PER_MIN) + second(timestamp) + (daybefore * SECS_PER_DAY));
}
// XXX: no
time_t utc_timestamp = ntpLocal2UTC(timestamp);
return NtpCalendarWeekday {
weekday(timestamp), hour(timestamp), minute(timestamp),
weekday(utc_timestamp), hour(utc_timestamp), minute(utc_timestamp)
};
}
#else
NtpCalendarWeekday _schGetWeekday(time_t timestamp, int daybefore) {
tm utc_time;
tm local_time;
gmtime_r(&timestamp, &utc_time);
if (daybefore > 0) {
timestamp = timestamp - ((utc_time.tm_hour * secondsPerHour) + ((utc_time.tm_min + 1) * secondsPerMinute) + utc_time.tm_sec + (daybefore * secondsPerDay));
gmtime_r(&timestamp, &utc_time);
localtime_r(&timestamp, &local_time);
} else {
localtime_r(&timestamp, &local_time);
}
// TimeLib sunday is 1 instead of 0
return NtpCalendarWeekday {
local_time.tm_wday + 1, local_time.tm_hour, local_time.tm_min,
utc_time.tm_wday + 1, utc_time.tm_hour, utc_time.tm_min
};
}
#endif
// If daybefore and relay is -1, check with current timestamp // If daybefore and relay is -1, check with current timestamp
// Otherwise, modify it by moving 'daybefore' days back and only use the 'relay' id // Otherwise, modify it by moving 'daybefore' days back and only use the 'relay' id
void _schCheck(int relay, int daybefore) { void _schCheck(int relay, int daybefore) {
time_t local_time = now();
time_t utc_time = ntpLocal2UTC(local_time);
int minimum_restore_time = -1440;
time_t timestamp = now();
auto calendar_weekday = _schGetWeekday(timestamp, daybefore);
int minimum_restore_time = -(60 * 24);
int saved_action = -1; int saved_action = -1;
int saved_sch = -1; int saved_sch = -1;
// Check schedules // Check schedules
for (unsigned char i = 0; i < SCHEDULER_MAX_SCHEDULES; i++) { for (unsigned char i = 0; i < SCHEDULER_MAX_SCHEDULES; i++) {
int sch_switch = getSetting({"schSwitch", i}, 0xFF);
if (sch_switch == 0xFF) break;
int sch_switch = getSetting({"schSwitch", i}, SchedulerDummySwitchId);
if (sch_switch == SchedulerDummySwitchId) break;
// Skip disabled schedules // Skip disabled schedules
if (!getSetting({"schEnabled", i}, false)) continue; if (!getSetting({"schEnabled", i}, false)) continue;
// Get the datetime used for the calculation // Get the datetime used for the calculation
const bool sch_utc = getSetting({"schUTC", i}, false); const bool sch_utc = getSetting({"schUTC", i}, false);
time_t t = sch_utc ? utc_time : local_time;
if (daybefore > 0) {
unsigned char now_hour = hour(t);
unsigned char now_minute = minute(t);
unsigned char now_sec = second(t);
t = t - ((now_hour * 3600) + ((now_minute + 1) * 60) + now_sec + (daybefore * 86400));
}
String sch_weekdays = getSetting({"schWDs", i}, SCHEDULER_WEEKDAYS); String sch_weekdays = getSetting({"schWDs", i}, SCHEDULER_WEEKDAYS);
if (_schIsThisWeekday(t, sch_weekdays)) {
if (_schIsThisWeekday(sch_utc ? calendar_weekday.utc_wday : calendar_weekday.local_wday, sch_weekdays)) {
int sch_hour = getSetting({"schHour", i}, 0); int sch_hour = getSetting({"schHour", i}, 0);
int sch_minute = getSetting({"schMinute", i}, 0); int sch_minute = getSetting({"schMinute", i}, 0);
int minutes_to_trigger = _schMinutesLeft(t, sch_hour, sch_minute);
int sch_action = getSetting({"schAction", i}, 0); int sch_action = getSetting({"schAction", i}, 0);
int sch_type = getSetting({"schType", i}, SCHEDULER_TYPE_SWITCH); int sch_type = getSetting({"schType", i}, SCHEDULER_TYPE_SWITCH);
int minutes_to_trigger = _schMinutesLeft(
sch_utc ? calendar_weekday.utc_hour : calendar_weekday.local_hour,
sch_utc ? calendar_weekday.utc_minute : calendar_weekday.local_minute,
sch_hour, sch_minute
);
if (sch_type == SCHEDULER_TYPE_SWITCH && sch_switch == relay && sch_action != 2 && minutes_to_trigger < 0 && minutes_to_trigger > minimum_restore_time) { if (sch_type == SCHEDULER_TYPE_SWITCH && sch_switch == relay && sch_action != 2 && minutes_to_trigger < 0 && minutes_to_trigger > minimum_restore_time) {
minimum_restore_time = minutes_to_trigger; minimum_restore_time = minutes_to_trigger;
saved_action = sch_action; saved_action = sch_action;
@ -241,48 +278,23 @@ void _schCheck(int relay, int daybefore) {
} }
if (daybefore >= 0 && daybefore < 7 && minimum_restore_time == -1440 && saved_action == -1) {
_schCheck(relay, ++daybefore);
return;
if (daybefore >= 0 && daybefore < 7 && minimum_restore_time == -(60 * 24) && saved_action == -1) {
_schCheck(relay, ++daybefore);
return;
} }
if (minimum_restore_time != -1440 && saved_action != -1 && saved_sch != -1) {
if (minimum_restore_time != -(60 * 24) && saved_action != -1 && saved_sch != -1) {
_schAction(saved_sch, saved_action, relay); _schAction(saved_sch, saved_action, relay);
} }
} }
void _schLoop() {
// Check time has been sync'ed
if (!ntpSynced()) return;
if (_sch_restore == 0) {
for (unsigned char i = 0; i < relayCount(); i++){
if (getSetting({"relayLastSch", i}, 1 == SCHEDULER_RESTORE_LAST_SCHEDULE)) {
_schCheck(i, 0);
}
}
_sch_restore = 1;
}
// Check schedules every minute at hh:mm:00
static unsigned long last_minute = 60;
unsigned char current_minute = minute();
if (current_minute != last_minute) {
last_minute = current_minute;
_schCheck(-1, -1);
}
}
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
void schSetup() { void schSetup() {
_schConfigure(); _schConfigure();
// Update websocket clients
#if WEB_SUPPORT #if WEB_SUPPORT
wsRegister() wsRegister()
.onVisible(_schWebSocketOnVisible) .onVisible(_schWebSocketOnVisible)
@ -290,8 +302,21 @@ void schSetup() {
.onKeyCheck(_schWebSocketOnKeyCheck); .onKeyCheck(_schWebSocketOnKeyCheck);
#endif #endif
// Main callbacks
espurnaRegisterLoop(_schLoop);
NtpBroker::Register([](const NtpTick tick, time_t, const String&) {
if (NtpTick::EveryMinute != tick) return;
static bool restore_once = true;
if (restore_once) {
for (unsigned char i = 0; i < relayCount(); i++) {
if (getSetting({"relayLastSch", i}, 1 == SCHEDULER_RESTORE_LAST_SCHEDULE)) {
_schCheck(i, 0);
}
}
restore_once = false;
}
_schCheck(-1, -1);
});
espurnaRegisterReload(_schConfigure); espurnaRegisterReload(_schConfigure);
} }


+ 3045
- 3043
code/espurna/static/index.all.html.gz.h
File diff suppressed because it is too large
View File


+ 2851
- 2849
code/espurna/static/index.light.html.gz.h
File diff suppressed because it is too large
View File


+ 2422
- 2420
code/espurna/static/index.lightfox.html.gz.h
File diff suppressed because it is too large
View File


+ 2448
- 2446
code/espurna/static/index.rfbridge.html.gz.h
File diff suppressed because it is too large
View File


+ 2943
- 2941
code/espurna/static/index.rfm69.html.gz.h
File diff suppressed because it is too large
View File


+ 1832
- 1830
code/espurna/static/index.sensor.html.gz.h
File diff suppressed because it is too large
View File


+ 2385
- 2383
code/espurna/static/index.small.html.gz.h
File diff suppressed because it is too large
View File


+ 1763
- 1761
code/espurna/static/index.thermostat.html.gz.h
File diff suppressed because it is too large
View File


+ 8
- 7
code/espurna/thermostat.ino View File

@ -12,6 +12,7 @@ Copyright (C) 2017 by Dmitry Blinov <dblinov76 at gmail dot com>
#include <float.h> #include <float.h>
#include "relay.h" #include "relay.h"
#include "ntp.h"
#include "ws.h" #include "ws.h"
@ -462,21 +463,21 @@ void updateCounters() {
} }
if (ntpSynced()) { if (ntpSynced()) {
String value = NTP.getDateStr();
unsigned int day = value.substring(0, 2).toInt();
unsigned int month = value.substring(3, 5).toInt();
if (day != _thermostat_burn_day) {
const auto ts = now();
unsigned int now_day = day(ts);
unsigned int now_month = month(ts);
if (now_day != _thermostat_burn_day) {
_thermostat_burn_yesterday = _thermostat_burn_today; _thermostat_burn_yesterday = _thermostat_burn_today;
_thermostat_burn_today = 0; _thermostat_burn_today = 0;
_thermostat_burn_day = day;
_thermostat_burn_day = now_day;
setSetting(NAME_BURN_YESTERDAY, _thermostat_burn_yesterday); setSetting(NAME_BURN_YESTERDAY, _thermostat_burn_yesterday);
setSetting(NAME_BURN_TODAY, _thermostat_burn_today); setSetting(NAME_BURN_TODAY, _thermostat_burn_today);
setSetting(NAME_BURN_DAY, _thermostat_burn_day); setSetting(NAME_BURN_DAY, _thermostat_burn_day);
} }
if (month != _thermostat_burn_month) {
if (now_month != _thermostat_burn_month) {
_thermostat_burn_prev_month = _thermostat_burn_this_month; _thermostat_burn_prev_month = _thermostat_burn_this_month;
_thermostat_burn_this_month = 0; _thermostat_burn_this_month = 0;
_thermostat_burn_month = month;
_thermostat_burn_month = now_month;
setSetting(NAME_BURN_PREV_MONTH, _thermostat_burn_prev_month); setSetting(NAME_BURN_PREV_MONTH, _thermostat_burn_prev_month);
setSetting(NAME_BURN_THIS_MONTH, _thermostat_burn_this_month); setSetting(NAME_BURN_THIS_MONTH, _thermostat_burn_this_month);
setSetting(NAME_BURN_MONTH, _thermostat_burn_month); setSetting(NAME_BURN_MONTH, _thermostat_burn_month);


+ 9
- 5
code/espurna/utils.ino View File

@ -8,11 +8,10 @@ Copyright (C) 2017-2019 by Xose Pérez <xose dot perez at gmail dot com>
#include <limits> #include <limits>
#include <Ticker.h>
#include <TimeLib.h>
#include "utils.h"
#include "config/buildtime.h"
#include "libs/HeapStats.h" #include "libs/HeapStats.h"
#include "ntp.h"
#include "utils.h"
void setDefaultHostname() { void setDefaultHostname() {
if (strlen(HOSTNAME) > 0) { if (strlen(HOSTNAME) > 0) {
@ -79,8 +78,13 @@ unsigned long getHeartbeatInterval() {
} }
String buildTime() { String buildTime() {
#if NTP_SUPPORT
#if NTP_LEGACY_SUPPORT && NTP_SUPPORT
return ntpDateTime(__UNIX_TIMESTAMP__); return ntpDateTime(__UNIX_TIMESTAMP__);
#elif NTP_SUPPORT
constexpr const time_t ts = __UNIX_TIMESTAMP__;
tm timestruct;
gmtime_r(&ts, &timestruct);
return ntpDateTime(&timestruct);
#else #else
char buffer[20]; char buffer[20];
snprintf_P( snprintf_P(


+ 8
- 3
code/html/index.html View File

@ -1046,17 +1046,22 @@
<input class="pure-u-1 pure-u-lg-3-4" name="ntpServer" type="text" tabindex="41" /> <input class="pure-u-1 pure-u-lg-3-4" name="ntpServer" type="text" tabindex="41" />
</div> </div>
<div class="pure-g">
<div class="pure-g module module-ntplwip">
<label class="pure-u-1 pure-u-lg-1-4">Time Zone</label>
<input class="pure-u-1 pure-u-lg-3-4" name="ntpTZ" type="text" tabindex="42" />
</div>
<div class="pure-g module module-ntplegacy">
<label class="pure-u-1 pure-u-lg-1-4">Time Zone</label> <label class="pure-u-1 pure-u-lg-1-4">Time Zone</label>
<select class="pure-u-1 pure-u-lg-1-4" name="ntpOffset" tabindex="42"></select> <select class="pure-u-1 pure-u-lg-1-4" name="ntpOffset" tabindex="42"></select>
</div> </div>
<div class="pure-g">
<div class="pure-g module module-ntplegacy">
<label class="pure-u-1 pure-u-lg-1-4">Enable DST</label> <label class="pure-u-1 pure-u-lg-1-4">Enable DST</label>
<div class="pure-u-1 pure-u-lg-1-4"><input type="checkbox" name="ntpDST" /></div> <div class="pure-u-1 pure-u-lg-1-4"><input type="checkbox" name="ntpDST" /></div>
</div> </div>
<div class="pure-g">
<div class="pure-g module module-ntplegacy">
<label class="pure-u-1 pure-u-lg-1-4">DST Region</label> <label class="pure-u-1 pure-u-lg-1-4">DST Region</label>
<select class="pure-u-1 pure-u-lg-1-4" name="ntpRegion"> <select class="pure-u-1 pure-u-lg-1-4" name="ntpRegion">
<option value="0">Europe</option> <option value="0">Europe</option>


Loading…
Cancel
Save