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/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/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 74574bb..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 |")); @@ -98,9 +99,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); @@ -109,15 +114,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(); @@ -199,11 +212,56 @@ 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); + 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); + } + } + } + + Serial.printf("😴\tGoing to sleep now for %d sec.\n", sleepSeconds); // Save current state saveState(); + // Persist actual sleep duration for correct uptime tracking next boot + preferences_->putUInt("last_sleep_sec", sleepSeconds); + // Disconnect MQTT NetworkManager::disconnectMqtt(); @@ -214,7 +272,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); 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;