diff --git a/src/AmsToMqttBridge.cpp b/src/AmsToMqttBridge.cpp index f008ff2c..037d829a 100644 --- a/src/AmsToMqttBridge.cpp +++ b/src/AmsToMqttBridge.cpp @@ -101,6 +101,7 @@ ADC_MODE(ADC_VCC); #include "PulseMeterCommunicator.h" #include "Uptime.h" +#include "NtpStatus.h" #if defined(AMS_REMOTE_DEBUG) #include "RemoteDebug.h" @@ -699,6 +700,8 @@ void setup() { ea.setPriceService(ps); ws.setup(&config, &gpioConfig, &meterState, &ds, &ea, &rtp, &updater); + ntpRegisterSyncCallback(); + UiConfig ui; if(config.getUiConfig(ui)) { if(strlen(ui.language) == 0) { diff --git a/src/AmsWebServer.cpp b/src/AmsWebServer.cpp index fb06fe9e..ad2bc643 100644 --- a/src/AmsWebServer.cpp +++ b/src/AmsWebServer.cpp @@ -8,6 +8,7 @@ #include "CustomDefaults.h" #include "AmsWebHeaders.h" #include "FirmwareVersion.h" +#include "NtpStatus.h" #include "base64.h" #include "hexutils.h" #include "AmsJsonGenerator.h" @@ -312,6 +313,10 @@ uint8_t AmsWebServer::mqttHandlerState(AmsMqttHandler* h) { return h->lastError() == 0 ? 2 : 3; } +// SNTP resyncs roughly hourly; flag the NTP service as degraded if no sync has +// landed in this long, allowing a couple of missed cycles before warning. +#define NTP_STALE_AFTER_SECONDS 10800 + String AmsWebServer::buildServicesJson() { String out = ""; char entry[320]; @@ -387,7 +392,17 @@ String AmsWebServer::buildServicesJson() { NtpConfig ntp; if(config->getNtpConfig(ntp) && ntp.enable) { const char* server = strlen(ntp.server) > 0 ? ntp.server : "pool.ntp.org"; - uint8_t s = time(nullptr) > FirmwareVersion::BuildEpoch ? 1 : 2; + // A set-but-stale clock (NTP stopped resyncing) silently corrupts + // day-boundary accounting, so flag staleness rather than only + // reporting whether the clock was ever set. + uint64_t lastSync = ntpLastSyncMillis(); + uint8_t s; + if(lastSync == 0) { + s = 2; // No SNTP sync since boot yet + } else { + uint32_t ageSec = (uint32_t) ((millis64() - lastSync) / 1000); + s = ageSec > NTP_STALE_AFTER_SECONDS ? 2 : 1; + } snprintf_P(entry, sizeof(entry), PSTR("{\"k\":\"ntp\",\"s\":%d,\"e\":0,\"d\":\"%s\"}"), s, server); if(!out.isEmpty()) out += ","; diff --git a/src/NtpStatus.cpp b/src/NtpStatus.cpp new file mode 100644 index 00000000..dd06eebd --- /dev/null +++ b/src/NtpStatus.cpp @@ -0,0 +1,40 @@ +/** + * @copyright Utilitech AS 2023-2026 + * License: Fair Source + * + */ + +#include "NtpStatus.h" +#include "Uptime.h" + +#if defined(ESP8266) +#include +#elif defined(ESP32) +#include +#endif + +static uint64_t lastSyncMillis = 0; + +uint64_t ntpLastSyncMillis() { + return lastSyncMillis; +} + +#if defined(ESP8266) +// from_sntp is false when the clock is set manually (e.g. from the meter +// timestamp), so we only record actual SNTP syncs here. +static void onTimeSync(bool from_sntp) { + if(from_sntp) lastSyncMillis = millis64(); +} +#elif defined(ESP32) +static void onTimeSync(struct timeval* tv) { + lastSyncMillis = millis64(); +} +#endif + +void ntpRegisterSyncCallback() { +#if defined(ESP8266) + settimeofday_cb(onTimeSync); +#elif defined(ESP32) + sntp_set_time_sync_notification_cb(onTimeSync); +#endif +} diff --git a/src/NtpStatus.h b/src/NtpStatus.h new file mode 100644 index 00000000..2fb9d20f --- /dev/null +++ b/src/NtpStatus.h @@ -0,0 +1,20 @@ +/** + * @copyright Utilitech AS 2023-2026 + * License: Fair Source + * + */ + +#ifndef _NTPSTATUS_H +#define _NTPSTATUS_H + +#include + +// Registers the platform SNTP sync-notification callback. Call once during setup. +void ntpRegisterSyncCallback(); + +// millis64() value captured at the last successful SNTP sync, or 0 if no SNTP +// sync has happened since boot. Used to detect a clock that is set but stale +// (NTP stopped resyncing), which silently corrupts day-boundary accounting. +uint64_t ntpLastSyncMillis(); + +#endif