Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,4 @@ check_src_filters =
+<src/*>
-<src/GxDEPG0213BN/*>
-<src/GxGDE0213B72B/*>
check_flags = cppcheck: --suppress=*:*/.pio/* --suppress=*:*/GxDEPG0213BN/* --suppress=*:*/GxGDE0213B72B/*
check_flags = cppcheck: --inline-suppr --suppress=*:*/.pio/* --suppress=*:*/GxDEPG0213BN/* --suppress=*:*/GxGDE0213B72B/*
11 changes: 11 additions & 0 deletions src/PoolMonitor/Config.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
11 changes: 3 additions & 8 deletions src/PoolMonitor/DisplayManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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...");

Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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_;
}
Expand Down
6 changes: 0 additions & 6 deletions src/PoolMonitor/DisplayManager.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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&;

Expand Down
5 changes: 5 additions & 0 deletions src/PoolMonitor/NetworkManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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();
}
Expand All @@ -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) {
Expand All @@ -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();
Expand Down
72 changes: 65 additions & 7 deletions src/PoolMonitor/PoolMonitorContext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 |"));
Expand Down Expand Up @@ -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);
Expand All @@ -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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply elapsed skip count before deciding WiFi

When the previous sleep was a night interval and no_wifi_count was still below the threshold, doNetwork has already been computed before this scaled increment is written, so that wake still skips WiFi/MQTT even though 4 hours elapsed; for example, entering night right after a network cycle makes the 02:00 wake skip the retained MQTT sample and display update. Fresh evidence in this revision is that the scaled increment is only persisted here after the network decision was made, so the intended long-sleep catch-up applies one wake too late.

Useful? React with 👍 / 👎.

}
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();
Expand Down Expand Up @@ -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) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject bogus epochs before enabling night mode

When an NTP update fails after WiFi connects, getCurrentTime() returns "--:--" but setup still persists timeClient.getEpochTime(); on a fresh boot that can be a small nonzero uptime value. This new lastEpoch > 0 gate then treats the bogus 1970-era timestamp as valid local time (typically inside the 00:00-06:00 night window), so an NTP outage during the day can put the device into repeated 4-hour sleeps and delay MQTT/display/OTA work. Require a successful sync or reject implausible epochs before using last_epoch for night-mode scheduling.

Useful? React with 👍 / 👎.

// 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<int>(NIGHT_START_HOUR) ||
currentHour < static_cast<int>(NIGHT_END_HOUR)) {
sleepSeconds = NIGHT_SLEEP_INTERVAL_SECONDS;
Comment thread
stritti marked this conversation as resolved.
Comment thread
stritti marked this conversation as resolved.
Comment thread
stritti marked this conversation as resolved.

// Clamp night sleep so the device does not overshoot NIGHT_END_HOUR
int secondsUntilEnd;
if (currentHour >= static_cast<int>(NIGHT_START_HOUR)) {
// Night started today (22:xx-23:xx), end is tomorrow 06:xx
secondsUntilEnd = (static_cast<int>(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<int>(NIGHT_END_HOUR) - currentHour) * 3600
- currentMinute * 60 - currentSecond;
}

if (secondsUntilEnd > 60 && sleepSeconds > static_cast<uint32_t>(secondsUntilEnd)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clamp the final minute before morning

When the device evaluates night mode in the last minute before NIGHT_END_HOUR—for example after a restart or network cycle at 05:59:30—secondsUntilEnd is <= 60, so this guard skips the clamp and leaves sleepSeconds at the 4-hour night interval, waking around 09:59 instead of just after 06:00. Fresh evidence beyond the earlier clamp discussion is the secondsUntilEnd > 60 condition, which still allows this boundary oversleep; fall back to the normal interval or clamp the remaining time here.

Useful? React with 👍 / 👎.

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();

Expand All @@ -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);

Expand Down
2 changes: 2 additions & 0 deletions src/PoolMonitor/TimeClientHelper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down