From eeb8a6fb5eb67fa5011f5614f7391a2a20c269f3 Mon Sep 17 00:00:00 2001 From: Stephan Strittmatter Date: Fri, 26 Jun 2026 14:44:19 +0200 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20night=20mode=20=E2=80=94=20extended?= =?UTF-8?q?=20sleep=20interval=20during=2022:00-06:00?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Between NIGHT_START_HOUR (22) and NIGHT_END_HOUR (6) local time, the device wakes only every 4 hours instead of every 3 minutes. Reduces night-time wake-ups from ~160 to ~2, cutting daily power consumption from ~40 mAh to ~27 mAh. Night mode only activates after the first NTP sync (valid epoch time). Time is reconstructed from stored epoch + uptime, so it works across deep-sleep cycles without additional network requests. --- src/PoolMonitor/Config.hpp | 11 +++++++++++ src/PoolMonitor/PoolMonitorContext.cpp | 26 ++++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/PoolMonitor/Config.hpp b/src/PoolMonitor/Config.hpp index 1a7e37f..9880f13 100644 --- a/src/PoolMonitor/Config.hpp +++ b/src/PoolMonitor/Config.hpp @@ -45,6 +45,17 @@ constexpr std::uint32_t SKIP_WIFI_WAKE_CYCLES{5}; */ constexpr std::uint32_t NTP_SYNC_INTERVAL_SECONDS{3600}; +/** + * @brief Night mode — reduces wake frequency when nobody watches the display. + * + * Between NIGHT_START_HOUR and NIGHT_END_HOUR (local time) the device + * wakes only every NIGHT_SLEEP_INTERVAL_SECONDS instead of the normal + * TIME_TO_SLEEP_SECONDS. Saves significant power during the night. + */ +constexpr std::uint32_t NIGHT_START_HOUR{22}; +constexpr std::uint32_t NIGHT_END_HOUR{6}; +constexpr std::uint32_t NIGHT_SLEEP_INTERVAL_SECONDS{14400}; // 4 hours + /** * @brief MQTT payload buffer size for callback handling. */ diff --git a/src/PoolMonitor/PoolMonitorContext.cpp b/src/PoolMonitor/PoolMonitorContext.cpp index 74574bb..9702124 100644 --- a/src/PoolMonitor/PoolMonitorContext.cpp +++ b/src/PoolMonitor/PoolMonitorContext.cpp @@ -199,7 +199,29 @@ auto PoolMonitorContext::loop() -> void { } auto PoolMonitorContext::prepareForSleep() -> void { - Serial.printf("😴\tGoing to sleep now for %d sec.\n", TIME_TO_SLEEP_SECONDS); + // ── Determine sleep interval ── + uint32_t sleepSeconds = TIME_TO_SLEEP_SECONDS; + + unsigned long totalUptime = preferences_->getULong("total_uptime", 0); + unsigned long lastEpoch = preferences_->getULong("last_epoch", 0); + + if (lastEpoch > 0) { + // Reconstruct current local time to check if we're in the night window + unsigned long lastNtpSync = preferences_->getULong("last_ntp_sync", 0); + unsigned long elapsed = 0; + if (totalUptime > lastNtpSync) { + elapsed = totalUptime - lastNtpSync; + } + time_t t = PoolMonitor::currentTZ.toLocal(lastEpoch + elapsed); + int currentHour = ::hour(t); + + if (currentHour >= static_cast(NIGHT_START_HOUR) || + currentHour < static_cast(NIGHT_END_HOUR)) { + sleepSeconds = NIGHT_SLEEP_INTERVAL_SECONDS; + } + } + + Serial.printf("😴\tGoing to sleep now for %d sec.\n", sleepSeconds); // Save current state saveState(); @@ -214,7 +236,7 @@ auto PoolMonitorContext::prepareForSleep() -> void { preferences_->end(); // Enter deep sleep - esp_sleep_enable_timer_wakeup(TIME_TO_SLEEP_SECONDS * 1000000); + esp_sleep_enable_timer_wakeup(sleepSeconds * 1000000ULL); pinMode(PIN_MODEM_POWER_ON, OUTPUT); digitalWrite(PIN_MODEM_POWER_ON, LOW); From a7d7dbf63f775614d9974b2c3e8ee76ed1f57542 Mon Sep 17 00:00:00 2001 From: Stephan Strittmatter Date: Fri, 26 Jun 2026 14:55:39 +0200 Subject: [PATCH 2/5] fix: persist actual sleep duration for correct uptime tracking across deep-sleep cycles Codex P1: When night mode selects a 4-hour sleep interval, the next boot must increment total_uptime by the actual duration (not just TIME_TO_SLEEP_SECONDS). Without this fix, the time reconstruction drifts by ~3h57m every night cycle, causing the device to stay in night mode well into the day. - prepareForSleep(): save 'last_sleep_sec' to NVS before deep sleep - setup(): read 'last_sleep_sec' when incrementing total_uptime, then remove it so normal cycles without the key default to TIME_TO_SLEEP_SECONDS --- src/PoolMonitor/PoolMonitorContext.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/PoolMonitor/PoolMonitorContext.cpp b/src/PoolMonitor/PoolMonitorContext.cpp index 9702124..eb0127f 100644 --- a/src/PoolMonitor/PoolMonitorContext.cpp +++ b/src/PoolMonitor/PoolMonitorContext.cpp @@ -98,9 +98,13 @@ auto PoolMonitorContext::setup() -> void { // Track cumulative uptime across sleep cycles unsigned long total_uptime = preferences_->getULong("total_uptime", 0); - total_uptime += TIME_TO_SLEEP_SECONDS; + // Use actual sleep duration from last cycle (supports night-mode 4h sleeps) + uint32_t lastSleepDuration = preferences_->getUInt("last_sleep_sec", TIME_TO_SLEEP_SECONDS); + total_uptime += lastSleepDuration; + preferences_->remove("last_sleep_sec"); preferences_->putULong("total_uptime", total_uptime); - Serial.printf("Total uptime: %lu seconds (%.1f hours)\n", total_uptime, total_uptime / 3600.0); + Serial.printf("Total uptime: %lu seconds (%.1f hours, last sleep: %u s)\n", + total_uptime, total_uptime / 3600.0, lastSleepDuration); // ── Power-save: WiFi/MQTT only every (SKIP_WIFI_WAKE_CYCLES + 1) wake-ups ── uint32_t cyclesWithoutWiFi = preferences_->getUInt("no_wifi_count", 0); @@ -226,6 +230,9 @@ auto PoolMonitorContext::prepareForSleep() -> void { // Save current state saveState(); + // Persist actual sleep duration for correct uptime tracking next boot + preferences_->putUInt("last_sleep_sec", sleepSeconds); + // Disconnect MQTT NetworkManager::disconnectMqtt(); From 6a4f196b333091115d50cdbd2408053e74773972 Mon Sep 17 00:00:00 2001 From: Stephan Strittmatter Date: Fri, 26 Jun 2026 17:17:20 +0200 Subject: [PATCH 3/5] fix: clamp night sleep to not overshoot NIGHT_END_HOUR (Codex P2) When the device enters night mode at an hour not aligned to the intended 22:00/02:00 cadence (e.g. 00:30 after a restart), the unconditional 4-hour sleep could carry it past 06:00 into morning. The fix calculates the remaining seconds until NIGHT_END_HOUR and clamps the sleep interval, so the device always wakes at or before 06:00 to resume normal operation. --- src/PoolMonitor/PoolMonitorContext.cpp | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/PoolMonitor/PoolMonitorContext.cpp b/src/PoolMonitor/PoolMonitorContext.cpp index eb0127f..7fc2cb1 100644 --- a/src/PoolMonitor/PoolMonitorContext.cpp +++ b/src/PoolMonitor/PoolMonitorContext.cpp @@ -218,10 +218,30 @@ auto PoolMonitorContext::prepareForSleep() -> void { } time_t t = PoolMonitor::currentTZ.toLocal(lastEpoch + elapsed); int currentHour = ::hour(t); + int currentMinute = ::minute(t); + int currentSecond = ::second(t); if (currentHour >= static_cast(NIGHT_START_HOUR) || currentHour < static_cast(NIGHT_END_HOUR)) { sleepSeconds = NIGHT_SLEEP_INTERVAL_SECONDS; + + // Clamp night sleep so the device does not overshoot NIGHT_END_HOUR + int secondsUntilEnd; + if (currentHour >= static_cast(NIGHT_START_HOUR)) { + // Night started today (22:xx-23:xx), end is tomorrow 06:xx + secondsUntilEnd = (static_cast(NIGHT_END_HOUR) + 24 - currentHour) * 3600 + - currentMinute * 60 - currentSecond; + } else { + // Night continues today (00:xx-05:xx), end is today 06:xx + secondsUntilEnd = (static_cast(NIGHT_END_HOUR) - currentHour) * 3600 + - currentMinute * 60 - currentSecond; + } + + if (secondsUntilEnd > 60 && sleepSeconds > static_cast(secondsUntilEnd)) { + sleepSeconds = secondsUntilEnd; + Serial.printf("🌙\tClamping night sleep to %d sec (wake at %02d:00)\n", + sleepSeconds, NIGHT_END_HOUR); + } } } From 0bdf95635c33b890bd54dc85b7c9771b0db92fbd Mon Sep 17 00:00:00 2001 From: Stephan Strittmatter Date: Sat, 27 Jun 2026 22:02:39 +0200 Subject: [PATCH 4/5] fix: scale no_wifi_count increment by actual sleep duration During night mode (4h sleep), no_wifi_count was only incremented by 1, undercounting skipped wake cycles by a factor of ~80. This caused the post-night wake (06:00) to skip WiFi/MQTT/display because the counter hadn't reached the SKIP_WIFI_WAKE_CYCLES threshold. Now the increment is scaled by lastSleepDuration / TIME_TO_SLEEP_SECONDS, so a 4-hour night sleep correctly advances the counter by ~80 cycles. Addresses P2 feedback from Codex review on PR #25. --- src/PoolMonitor/PoolMonitorContext.cpp | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/PoolMonitor/PoolMonitorContext.cpp b/src/PoolMonitor/PoolMonitorContext.cpp index 7fc2cb1..68464f7 100644 --- a/src/PoolMonitor/PoolMonitorContext.cpp +++ b/src/PoolMonitor/PoolMonitorContext.cpp @@ -113,15 +113,23 @@ auto PoolMonitorContext::setup() -> void { bool hasConfig = (preferences_->getString("mqtt_server", "").length() > 0); bool doNetwork = !hasConfig || (cyclesWithoutWiFi >= SKIP_WIFI_WAKE_CYCLES); + // Scale no_wifi_count increment by actual sleep duration so a 4-hour + // night sleep advances the counter by ~80 cycles (14400/180) instead + // of only 1, preventing network-skip on the post-night wake. + uint32_t skipIncrement = lastSleepDuration / TIME_TO_SLEEP_SECONDS; + if (skipIncrement < 1) skipIncrement = 1; + if (doNetwork) { preferences_->putUInt("no_wifi_count", 0); } else { - preferences_->putUInt("no_wifi_count", cyclesWithoutWiFi + 1); + preferences_->putUInt("no_wifi_count", cyclesWithoutWiFi + skipIncrement); } - Serial.printf("📡\tNetwork cycle: %s (%u/%u without WiFi)\n", + Serial.printf("📡\tNetwork cycle: %s (%u/%u without WiFi, last sleep: %u s, inc: %u)\n", doNetwork ? "YES" : "NO", doNetwork ? 0 : cyclesWithoutWiFi + 1, - SKIP_WIFI_WAKE_CYCLES); + SKIP_WIFI_WAKE_CYCLES, + lastSleepDuration, + skipIncrement); // Initialize NTP time client PoolMonitor::beginTimeClient(); From ec569c062c0fd22f7aea3562c6ec1b2f5c2ebe17 Mon Sep 17 00:00:00 2001 From: Stephan Strittmatter Date: Tue, 30 Jun 2026 20:21:15 +0200 Subject: [PATCH 5/5] style: suppress cross-TU unusedFunction false positives in cppcheck cppcheck's --unusedFunction cannot resolve calls across translation units for PlatformIO builds, producing false positives for every function called from PoolMonitorContext.cpp to DisplayManager, NetworkManager, and TimeClientHelper, plus the Arduino framework entry point PoolMonitorContext::setup(). Changes: - Added --inline-suppr to platformio.ini check_flags - Added // cppcheck-suppress unusedFunction with ; comment syntax to 11 cross-TU functions that are genuinely called from other .cpp files - Removed DisplayManager::getWidth() and getHeight() which were genuinely unused (callers use getDisplay().width()/.height() directly) --- platformio.ini | 2 +- src/PoolMonitor/DisplayManager.cpp | 11 +++-------- src/PoolMonitor/DisplayManager.hpp | 6 ------ src/PoolMonitor/NetworkManager.cpp | 5 +++++ src/PoolMonitor/PoolMonitorContext.cpp | 1 + src/PoolMonitor/TimeClientHelper.cpp | 2 ++ 6 files changed, 12 insertions(+), 15 deletions(-) diff --git a/platformio.ini b/platformio.ini index 01f8633..e10763e 100644 --- a/platformio.ini +++ b/platformio.ini @@ -49,4 +49,4 @@ check_src_filters = + - - -check_flags = cppcheck: --suppress=*:*/.pio/* --suppress=*:*/GxDEPG0213BN/* --suppress=*:*/GxGDE0213B72B/* +check_flags = cppcheck: --inline-suppr --suppress=*:*/.pio/* --suppress=*:*/GxDEPG0213BN/* --suppress=*:*/GxGDE0213B72B/* diff --git a/src/PoolMonitor/DisplayManager.cpp b/src/PoolMonitor/DisplayManager.cpp index fb36f6a..95377a3 100644 --- a/src/PoolMonitor/DisplayManager.cpp +++ b/src/PoolMonitor/DisplayManager.cpp @@ -49,6 +49,7 @@ void DisplayManager::displayText(const char* text, int16_t y, uint8_t align, int display_.print(text); } +// cppcheck-suppress unusedFunction ; called from PoolMonitorContext.cpp (cross-TU) void DisplayManager::initDisplay() { Serial.println("🖥️\tInitializing display with static content..."); @@ -91,6 +92,7 @@ void DisplayManager::initDisplay() { fullUpdate(); } +// cppcheck-suppress unusedFunction ; called from PoolMonitorContext.cpp (cross-TU) void DisplayManager::updateDisplay(float poolTemp, float solarTemp, bool poolPumpOn, bool solarPumpOn, const char* mode, const char* lastUpdate) { Serial.println("🖥️\tUpdating display"); @@ -158,14 +160,7 @@ void DisplayManager::fullUpdate() { display_.update(); } -uint16_t DisplayManager::getWidth() { - return display_.width(); -} - -uint16_t DisplayManager::getHeight() { - return display_.height(); -} - +// cppcheck-suppress unusedFunction ; called from PoolMonitorContext.cpp (cross-TU) auto DisplayManager::getDisplay() -> GxEPD_Class& { return display_; } diff --git a/src/PoolMonitor/DisplayManager.hpp b/src/PoolMonitor/DisplayManager.hpp index 3bb6f6f..6884f83 100644 --- a/src/PoolMonitor/DisplayManager.hpp +++ b/src/PoolMonitor/DisplayManager.hpp @@ -86,12 +86,6 @@ class DisplayManager { /** @brief Update full display (not just partial). */ static void fullUpdate(); - /** @brief Get display width. */ - static uint16_t getWidth(); - - /** @brief Get display height. */ - static uint16_t getHeight(); - /** @brief Get reference to the display instance. */ static auto getDisplay() -> GxEPD_Class&; diff --git a/src/PoolMonitor/NetworkManager.cpp b/src/PoolMonitor/NetworkManager.cpp index c14833b..772eb3b 100644 --- a/src/PoolMonitor/NetworkManager.cpp +++ b/src/PoolMonitor/NetworkManager.cpp @@ -54,6 +54,7 @@ bool NetworkManager::begin(const char* hostname, uint32_t timeoutSeconds) { return false; } +// cppcheck-suppress unusedFunction ; called from PoolMonitorContext.cpp (cross-TU) bool NetworkManager::beginMqtt(const char* server, uint16_t port, const char* clientId) { mqttClient_.setServer(server, port); @@ -74,10 +75,12 @@ void NetworkManager::loop() { } } +// cppcheck-suppress unusedFunction ; called from PoolMonitorContext.cpp (cross-TU) bool NetworkManager::isWiFiConnected() { return WiFi.status() == WL_CONNECTED; } +// cppcheck-suppress unusedFunction ; called from PoolMonitorContext.cpp (cross-TU) bool NetworkManager::isMqttConnected() { return mqttClient_.connected(); } @@ -96,6 +99,7 @@ bool NetworkManager::subscribe(const char* topic) { return mqttClient_.subscribe(topic); } +// cppcheck-suppress unusedFunction ; called from PoolMonitorContext.cpp (cross-TU) void NetworkManager::setMqttCallback(MqttMessageCallback callback) { mqttCallback_ = callback; mqttClient_.setCallback([](char* topic, byte* payload, unsigned int length) { @@ -105,6 +109,7 @@ void NetworkManager::setMqttCallback(MqttMessageCallback callback) { }); } +// cppcheck-suppress unusedFunction ; called from PoolMonitorContext.cpp (cross-TU) void NetworkManager::disconnectMqtt() { if (mqttClient_.connected()) { mqttClient_.disconnect(); diff --git a/src/PoolMonitor/PoolMonitorContext.cpp b/src/PoolMonitor/PoolMonitorContext.cpp index 68464f7..9af1045 100644 --- a/src/PoolMonitor/PoolMonitorContext.cpp +++ b/src/PoolMonitor/PoolMonitorContext.cpp @@ -67,6 +67,7 @@ PoolMonitorContext::~PoolMonitorContext() { Self = nullptr; } +// cppcheck-suppress unusedFunction ; called from main.cpp (cross-TU) auto PoolMonitorContext::setup() -> void { Serial.println(F(" ------------------------------------- ")); Serial.println(F("| Pool Monitor |")); diff --git a/src/PoolMonitor/TimeClientHelper.cpp b/src/PoolMonitor/TimeClientHelper.cpp index 53324f3..482aafc 100644 --- a/src/PoolMonitor/TimeClientHelper.cpp +++ b/src/PoolMonitor/TimeClientHelper.cpp @@ -23,10 +23,12 @@ WiFiUDP ntpUDP; NTPClient timeClient(ntpUDP, "europe.pool.ntp.org"); Timezone currentTZ = CE; +// cppcheck-suppress unusedFunction ; called from PoolMonitorContext.cpp (cross-TU) void beginTimeClient() { timeClient.begin(); } +// cppcheck-suppress unusedFunction ; called from PoolMonitorContext.cpp (cross-TU) String getCurrentTime() { // update time with timeout to prevent infinite loop int retries = 0;