mirror of
https://github.com/Legolas-2025/Standalone-electricity-price-ticker.git
synced 2026-08-17 12:34:55 +02:00
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.
This commit is contained in:
+240
-10
@@ -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<Config::JSON_BUFFER_SIZE> 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 |
|
||||
|
||||
Reference in New Issue
Block a user