From d571e6994271db83e3c9141197776b1854f79cf0 Mon Sep 17 00:00:00 2001 From: Legolas-2025 Date: Fri, 3 Apr 2026 04:02:03 +0200 Subject: [PATCH] Update CHANGELOG for v7.0 release Document major changes and new features in v7.0, including the Dual-Buffer NVS System, Midnight Bridge logic, and enhancements for fetching and displaying tomorrow's data. --- CHANGELOG.md | 250 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 240 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93fcdf5..62d37e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,16 +2,246 @@ All notable changes to this project are documented here. +## v7.0 - Rolling 48-Hour Logic & Midnight Bridge (2026-04-02) + +**Summary** + +This is the **"Golden Build"** for this hardware platform. It combines all the hardware stability fixes from v6.2.4 with a revolutionary new 48-hour price prediction system that eliminates the "1 AM fetch gap" problem that plagues most electricity tickers. + +### New Features + +#### 1. Dual-Buffer NVS System + +The ticker now stores "Today" and "Tomorrow" data independently in NVS, allowing seamless display of up to 47 hours of price data. + +**New NVS Keys:** +- `data_prc_t` – Raw JSON payload for tomorrow's prices +- `data_store_t` – Unix timestamp when tomorrow's data was stored + +**New Global Variables:** +- `StaticJsonDocument docTomorrow` – Tomorrow's price data buffer +- `bool isTomorrowDataAvailable` – Flag indicating tomorrow's data availability +- `float averagePriceTomorrow` – Tomorrow's daily average price +- `int lowestPriceIndexTomorrow` – Index of tomorrow's lowest price hour +- `int highestPriceIndexTomorrow` – Index of tomorrow's highest price hour + +#### 2. The Midnight Bridge (Rollover Logic) + +**Problem:** +Most electricity tickers fail at midnight because they rely on slow API calls to fetch new data. The Energy-Charts API typically doesn't publish next-day data until 1-2 AM, leaving users with a "No Data" screen for hours. + +**Solution:** +The Midnight Bridge detects the moment the local clock moves from 23:59:59 to 00:00:00 and instantly promotes the pre-fetched "Tomorrow" data to become "Today" data. + +**Implementation (in `loop()`):** +```cpp +if (daycheck->tm_mday != trackedDay) { + // Midnight rollover detected + if (isTomorrowDataAvailable) { + // Swap tomorrow to today instantly + doc = docTomorrow; + docTomorrow.clear(); + + // Update all statistics + averagePrice = averagePriceTomorrow; + lowestPriceIndex = lowestPriceIndexTomorrow; + highestPriceIndex = highestPriceIndexTomorrow; + + // Save to NVS and clear tomorrow slot + serializeJson(doc, payload); + saveDataToNVS(payload, false); + clearTomorrowNVS(); + + timeOffsetHours = 0; + displayPrices(); + } +} +``` + +**NVS Power-Failure Protection:** +Immediately after the swap, the new "Today" data is serialized and saved to NVS. If power is cut at 00:05 AM, the device reboots with correct data already loaded. + +#### 3. Smart Fetching & Tomorrow's Data + +**Automatic Tomorrow Fetch:** +After 14:00 (2 PM) local time, the ticker automatically fetches tomorrow's data using the `&start=YYYY-MM-DD` parameter: + +```cpp +void fetchAndProcessData(bool fetchTomorrow) { + String url = api_url; + if (fetchTomorrow) { + time_t now = time(nullptr); + now += 24 * 3600; // Add 24 hours + struct tm* tmr = localtime(&now); + char dateStr[20]; + snprintf(dateStr, sizeof(dateStr), "%04d-%02d-%02d", + tmr->tm_year + 1900, tmr->tm_mon + 1, tmr->tm_mday); + url += "&start="; + url += dateStr; + } + // ... HTTP request follows +} +``` + +**Smart Scheduling (`handleDataFetching()`):** +```cpp +struct tm* ti = localtime(&now); +// Priority 1: If it's after 14:00 and we don't have tomorrow's data yet +if (ti->tm_hour >= 14 && !isTomorrowDataAvailable) { + fetchAndProcessData(true); // Fetch tomorrow +} +``` + +#### 4. Seamless 48H Scrolling + +**Extended Display Range:** +When tomorrow's data is available, users can scroll up to **47 hours ahead**: + +```cpp +int maxOffsetLimit = isTomorrowDataAvailable ? 47 : 23; +if (displayStartHourOffset > maxOffsetLimit) + displayStartHourOffset %= (maxOffsetLimit + 1); +``` + +**Visual Distinction for Tomorrow:** +Future hours are marked with `HH:>>` to clearly distinguish tomorrow's prices from today's: + +```cpp +if (showTomorrow) { + snprintf(buffer, sizeof(buffer), "%02d:>>", localHourIndex); +} else { + snprintf(buffer, sizeof(buffer), "%02d:00", localHourIndex); +} +``` + +**Correct Min/Max Indicators:** +The code correctly uses tomorrow's statistics when displaying future hours: + +```cpp +int lowIdx = showTomorrow ? lowestPriceIndexTomorrow : lowestPriceIndex; +int highIdx = showTomorrow ? highestPriceIndexTomorrow : highestPriceIndex; +``` + +#### 5. Hardware Stability (Preserved from v6.2.4) + +All v6.2.4 stability fixes remain intact: + +- **State-Based Refresh**: Display updates exactly at 00, 15, 30, and 45 minutes past the hour, even if the CPU is busy +- **LED Indicators Pinned to Current**: White LED and built-in LED reflect actual current prices, regardless of what the user is viewing on screen + +### Technical Implementation Details + +#### Dual-Buffer Display Helpers + +**`display15MinuteDetails(int row, int totalHourOffset)`:** +- Now accepts `totalHourOffset` (0-47) instead of just hour index +- Automatically selects correct buffer (`doc` or `docTomorrow`) based on offset +- Shows past segments ("> ") for current hour + +**`displayPriceRow(int row, int totalHourOffset, bool isCurrentHourRow)`:** +- Extended to handle tomorrow's data with visual indicators +- Correctly applies hour suppression logic only to today's hours + +#### NVS Persistence Updates + +**`saveDataToNVS(const String& rawJson, bool isTomorrow)`:** +```cpp +if (isTomorrow) { + preferences.putString("data_prc_t", rawJson); + preferences.putULong("data_store_t", (unsigned long)now); +} else { + // Original "today" save logic + preferences.putInt("data_day", timeinfo.tm_mday); + // ... +} +``` + +**`loadDataFromNVS()`:** +- Now loads both today and tomorrow data from NVS +- Validates and processes both buffers independently + +#### API Date Validation + +The `processJsonData()` function now validates data against the correct target date: + +```cpp +time_t targetTime = time(nullptr); +if (isTomorrow) targetTime += 24 * 3600; +struct tm* targetDayTm = localtime(&targetTime); + +bool sameDate = (lastDataTm->tm_mday == targetDayTm->tm_mday && + lastDataTm->tm_mon == targetDayTm->tm_mon && + lastDataTm->tm_year == targetDayTm->tm_year); +``` + +### Why This Is the "Golden Build" + +| Feature | v6.2.4 | v7.0 | +|---------|--------|------| +| Display hours ahead | 23 hours (today only) | 47 hours (today + tomorrow) | +| Midnight transition | "No Data" until API updates | Seamless swap from buffer | +| Power failure resilience | Relies on API availability | NVS contains valid data | +| Visual tomorrow indication | None | `HH:>>` format | +| Tomorrow min/max markers | N/A | Correct indices | +| Fetch strategy | Once per day | Smart: today + tomorrow after 14:00 | + +### User Experience: Behavior & Display States + +The display changes based on which data buffer is being used and the status of the fetch: + +| **State** | **Display Output** | **LED Behavior** | +|----------|-------------------|-----------------| +| **Normal (Today)** | Shows current prices and 15-min details. Hours are marked as HH:00. | White LED reflects current price status (Breathe, Solid, or Blink). | +| **Scrolling (Tomorrow)** | Future prices are displayed. Hours are marked with HH:>> to indicate "Tomorrow". | **Pinned to Today:** The LEDs continue showing the _actual current_ price status even while you scroll through tomorrow. | +| **No Data** | Displays: "No data for today, Press & hold to, refresh manually." | White LED is turned **OFF** to avoid misleading price signals. | +| **Connecting** | "Elec. Rate SI v7.0" followed by "Connecting..." and progress dots. | Built-in LED is **OFF** until connection is established. | + +### API Call Intervals & Retry Strategy (v7.0) + +The exact API call intervals in version 7.0 vary depending on the device's state, data availability, and time of day: + +#### Primary Scheduling (Daily Fetch) + +The device aims to maintain a rolling 48-hour data window by fetching today's and tomorrow's data at specific times: + +- **Initial Boot:** An API call is attempted immediately upon startup and time synchronization. +- **Tomorrow's Data (Smart Fetching):** Starting at **14:00 (2 PM) local time**, the device begins checking for the next day's prices. It will attempt to fetch this data periodically until successful. +- **Midnight Rollover:** At exactly **00:00:00**, the device "promotes" tomorrow's data to the today buffer. If tomorrow's data was already successfully fetched and stored, **no API call is needed at midnight**. + +#### Retry Logic (Exponential Backoff) + +If a scheduled API call fails (e.g., due to a temporary server error or WiFi glitch), the device uses a safety-oriented retry interval: + +- **Max Retries:** 5 attempts (`HTTP_GET_RETRY_MAX`) +- **Interval Formula:** Uses a backoff factor of **2** (`HTTP_GET_BACKOFF_FACTOR`) +- **Typical Progression:** After a failure, it waits a short period, then doubles that wait time for each subsequent failure until the maximum retry count is reached + +#### "Midnight Phase" Recovery + +If the device reaches midnight but **does not** have tomorrow's data ready (meaning the afternoon fetches failed), it enters a high-priority state called `midnightPhaseActive`: + +- **Interval:** It bypasses the standard daily schedule and retries the API **more aggressively** (initially every minute). +- **Goal:** To clear the "No Data" screen and restore the price display as quickly as possible once the energy provider's server updates. + +#### Background Monitoring + +While not a full API call, the device performs these checks constantly: + +- **Loop Pacing:** The main system loop runs every **100ms** to check if it's time for a scheduled fetch. +- **Display Refresh:** The screen logic checks the time every loop but only refreshes the UI every **15 minutes** (at :00, :15, :30, :45) to match the price data intervals. + +--- + ## v6.2.4 - Exact-boundary display refresh bug (2026-04-01): **Summary** Top of the hour auto display refresh glitch fix where display automatically refreshed but showed the PREVIOUS hour's data. -### Problem: +### Problem: - At the exact top of the hour (e.g., 20:00:00), the display automatically refreshed but showed the PREVIOUS hour's data (19:00). This happened because the "next-boundary" rounding logic in findCurrentPriceIndex() incorrectly excluded the current interval if the time was exactly on the boundary. -### Solution: +### Solution: - Simplified findCurrentPriceIndex() to use a robust "last entry <= now" comparison. This ensures the display transitions to the new hour instantaneously at XX:00:00. --- @@ -22,12 +252,12 @@ Top of the hour auto display refresh glitch fix where display automatically refr The refresh logic should be "State-Based" rather than "Event-Based." Instead of checking if the minute is zero, it should check if the current hour is different from the last recorded hour. -### Problem: Screen would occasionally fail to update if the ESP32 was busy +### Problem: Screen would occasionally fail to update if the ESP32 was busy - fetching data or reconnecting WiFi) during the exact 00/15/30/45 minute mark. -### Solution: Switched from "Event-Based" (refresh only AT minute X) to "State-Based" - - (refresh IF current time != last refresh time). +### Solution: Switched from "Event-Based" (refresh only AT minute X) to "State-Based" + + (refresh IF current time != last refresh time). - This ensures the screen updates immediately even if the device was busy during the transition. --- @@ -85,9 +315,9 @@ for (size_t i = unixSeconds.size(); i > 0; i--) { ``` **Example:** -- At 17:57: nextQuarter = 18:00, finds last entry < 18:00 = 17:45 ✅ -- At 18:00: nextQuarter = 18:15, finds last entry < 18:15 = 18:00 ✅ -- At 18:46: nextQuarter = 19:00, finds last entry < 19:00 = 18:45 ✅ +- At 17:57: nextQuarter = 18:00, finds last entry < 18:00 = 17:45 +- At 18:00: nextQuarter = 18:15, finds last entry < 18:15 = 18:00 +- At 18:46: nextQuarter = 19:00, finds last entry < 19:00 = 18:45 --- @@ -157,7 +387,7 @@ int findPriceIndexForHour(const JsonArray& unixSeconds, int targetHour) { ### Why This Is Future-Proof | Scenario | Code Behavior | -|----------|--------------| +|----------|---------------| | Normal days (96 entries) | Works as before | | DST spring forward (92 entries) | Timestamp lookup finds correct indices | | DST fall back (100 entries) | Timestamp lookup finds correct indices |