From dcdac45d1f70e7e139bd12612834329fc402831b Mon Sep 17 00:00:00 2001 From: Gunnar Skjold Date: Thu, 25 Jun 2026 10:42:25 +0200 Subject: [PATCH] Track last SNTP sync time so Services tile can flag a stale clock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The NTP entry on the Services status tile only checked whether the system clock had ever been set (time() > BuildEpoch). A clock that was synced once and then silently stopped resyncing — which corrupts day-boundary energy accounting and shows up as a wrong GUI time — kept reporting green because the clock keeps ticking from millis(). Register a platform SNTP sync-notification callback that records millis64() at each successful sync: - ESP8266: settimeofday_cb(void(*)(bool from_sntp)) — the from_sntp flag lets us ignore the meter-timestamp settimeofday() so only real SNTP syncs count. - ESP32: sntp_set_time_sync_notification_cb(void(*)(struct timeval*)). The Services tile now reports NTP as degraded (yellow) when there has been no SNTP sync since boot or the last sync is older than 3h, and OK (green) only on a recent sync. Diagnostic only; no change to the accounting logic. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/AmsToMqttBridge.cpp | 3 +++ src/AmsWebServer.cpp | 17 ++++++++++++++++- src/NtpStatus.cpp | 40 ++++++++++++++++++++++++++++++++++++++++ src/NtpStatus.h | 20 ++++++++++++++++++++ 4 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 src/NtpStatus.cpp create mode 100644 src/NtpStatus.h 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