mirror of
https://github.com/Legolas-2025/Standalone-electricity-price-ticker.git
synced 2026-08-18 12:44:54 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b1fb6ac3d | ||
|
|
4fb5a6bc5f | ||
|
|
20bff8b00a | ||
|
|
0933a06973 | ||
|
|
48819161af | ||
|
|
bbef4f2bc7 | ||
|
|
a99caded8e | ||
|
|
bc41d9bb96 | ||
|
|
b38b4ce7a5 | ||
|
|
5ff76a57f9 | ||
|
|
4ac7449163 | ||
|
|
d571e69942 | ||
|
|
1a0fd42161 | ||
|
|
a7d479546d |
+491
-10
@@ -2,16 +2,497 @@
|
||||
|
||||
All notable changes to this project are documented here.
|
||||
|
||||
## v7.2 - Button Robustness & Screen-Control Fixes (2026-08-04)
|
||||
|
||||
**Summary**
|
||||
|
||||
Bug-fix release that resolves the three control glitches reported for the v7.1
|
||||
firmware on the Seeed XIAO ESP32‑C3:
|
||||
|
||||
- "It is 20:35 and I cannot scroll the values of the primary screen."
|
||||
- "Double click does not switch to the secondary screen."
|
||||
- "Strange behaviour at the end of the day, if there is no tomorrow's data
|
||||
available yet and the time is let's say 22:15."
|
||||
|
||||
No fee/VAT math, NVS layout, API scheduling or 48-hour scrolling behaviour
|
||||
was changed. The v7.1 negative-price provider fee logic is preserved
|
||||
verbatim.
|
||||
|
||||
### Fix 1 – Primary-screen scroll works at any hour of the day
|
||||
|
||||
**Symptom**
|
||||
|
||||
Clicking the button on the primary screen appeared to do nothing — the
|
||||
display stayed on the current hour and refused to advance.
|
||||
|
||||
**Root cause (two compounding bugs)**
|
||||
|
||||
1. `displayPriceRow()` only blanked past hours while `currentHour < 22`:
|
||||
|
||||
```cpp
|
||||
if (localHourIndex < currentHour && currentHour < 22) {
|
||||
lcd.print(" "); return;
|
||||
}
|
||||
```
|
||||
|
||||
After 22:00 the guard fell through, so the screen was allowed to repaint
|
||||
already-finished morning hours (00:00 – 21:59). The moment the user
|
||||
scrolled forward, the new "top" hour was visually overwritten by the
|
||||
previous morning's data, making the screen look frozen.
|
||||
|
||||
2. `displayPrimaryList()` contained an override:
|
||||
|
||||
```cpp
|
||||
if (currentHour >= 21 && timeOffsetHours > 0) {
|
||||
displayStartHourOffset = 21 + timeOffsetHours;
|
||||
}
|
||||
```
|
||||
|
||||
From 21:00 onward this pinned the top row at `21 + offset`. At 22:15
|
||||
every click computed start = 22+offset, was then clamped to 21+offset,
|
||||
and the user saw no movement.
|
||||
|
||||
**Fix**
|
||||
|
||||
- `displayPriceRow()`: simplified the past-hour guard to
|
||||
`if (localHourIndex < currentHour) blank();` so past hours of today are
|
||||
hidden at every hour of the day, not just before 22:00.
|
||||
- `displayPrimaryList()`: removed the `currentHour >= 21` override
|
||||
entirely. Past-hour blanking is now handled correctly by Fix 1a, so the
|
||||
override is no longer needed.
|
||||
|
||||
### Fix 2 – Double-click reliably toggles to the secondary screen
|
||||
|
||||
**Symptom**
|
||||
|
||||
A quick double-click did nothing — the display stayed on the primary
|
||||
price view. In some cases the screen showed a stuck "Long press detected!
|
||||
Release to refresh" message right after the device booted or was reset.
|
||||
|
||||
**Root cause**
|
||||
|
||||
The double-click path itself was correct; it was being starved by a false
|
||||
"Long press detected!" that fired immediately after every reset. On the
|
||||
ESP32-C3 the button pin (configured as `INPUT_PULLUP`) floats HIGH for a
|
||||
few seconds during boot while the internal pull-up is settling and
|
||||
power-rail noise is ringing. Because `buttonPressStartTime` is
|
||||
initialised to `0`, the long-press detector's predicate
|
||||
`millis() - buttonPressStartTime >= 3000` evaluated to
|
||||
`millis() >= 3000` a few seconds after boot — the firmware interpreted
|
||||
the floating-pin noise as a genuine 3-second hold, cleared the LCD to:
|
||||
|
||||
```
|
||||
Long press detected!
|
||||
Release to refresh
|
||||
```
|
||||
|
||||
…and from then on the user could not see any prices to click on
|
||||
(single- and double-click recognisers both still ran, but their visible
|
||||
effect was hidden behind the long-press splash).
|
||||
|
||||
**Fix**
|
||||
|
||||
Added a single new global flag and gated the long-press detector on it:
|
||||
|
||||
```cpp
|
||||
// New flag, set true the first time the pin is observed LOW after boot
|
||||
bool buttonEverReleased = false;
|
||||
|
||||
// In the "reading went LOW" branch:
|
||||
if (reading == LOW) {
|
||||
buttonPressStartTime = millis();
|
||||
longPressDetected = false;
|
||||
buttonEverReleased = true; // v7.2
|
||||
}
|
||||
|
||||
// In the long-press trip block:
|
||||
if (buttonState == LOW && !longPressDetected && buttonEverReleased) { // v7.2
|
||||
if (millis() - buttonPressStartTime >= longPressThreshold) {
|
||||
longPressDetected = true;
|
||||
// ... show "Long press detected! Release to refresh"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The detector now refuses to fire until the user (or the power-rail noise)
|
||||
has released the button at least once. The v7.1 button logic (50 ms
|
||||
debounce, 3 s long-press threshold, 500 ms double-click window, TTP223
|
||||
timing) is otherwise preserved verbatim.
|
||||
|
||||
### Fix 3 – End-of-day scroll is stable with no tomorrow data
|
||||
|
||||
**Symptom**
|
||||
|
||||
Around 22:00 – 23:59 with no tomorrow data in the buffer, scrolling
|
||||
through the primary screen produced a screen full of blank past-hour
|
||||
rows, or wrapped the start hour back to 00:00 in a confusing way.
|
||||
|
||||
**Root cause**
|
||||
|
||||
`advanceDisplayOffset()` contained a hack:
|
||||
|
||||
```cpp
|
||||
if (allowedAhead < 2 && currentHour >= 21 && !isTomorrowDataAvailable)
|
||||
allowedAhead = 2;
|
||||
```
|
||||
|
||||
At 22:00 (with no tomorrow data) this let the user click past hour 23
|
||||
into "24:00 / 25:00". `displayStartHourOffset` then exceeded
|
||||
`maxOffsetLimit = 23` and the wrap logic:
|
||||
|
||||
```cpp
|
||||
if (displayStartHourOffset > maxOffsetLimit)
|
||||
displayStartHourOffset %= (maxOffsetLimit + 1);
|
||||
```
|
||||
|
||||
…wrapped it back to 0. Combined with the buggy past-hour blanking in
|
||||
Fix 1a, the result was a screen full of stale blank rows from 00:00 to
|
||||
21:59.
|
||||
|
||||
**Fix**
|
||||
|
||||
Removed the `allowedAhead = 2` hack. The natural cap is now sufficient:
|
||||
|
||||
- 22:00 → can step 22 → 23, then wraps back to current
|
||||
- 23:00 → cannot step forward at all
|
||||
|
||||
No wrap to 00:00 of the previous day is reachable any more, and Fix 1a
|
||||
ensures any past hour that does briefly land on the screen is blanked
|
||||
correctly.
|
||||
|
||||
### Fix 4 (bonus) – Auto-return timer now resets on every click
|
||||
|
||||
**Symptom**
|
||||
|
||||
Scrolling through the 20-line secondary status page (4 lines at a time)
|
||||
did not push the 10 s auto-return-to-top timeout forward — the display
|
||||
could jump back to the primary price view mid-read.
|
||||
|
||||
**Root cause**
|
||||
|
||||
`lastButtonActivity` and `autoScrollExecuted` were only updated inside the
|
||||
primary-list branches of `advanceDisplayOffset()`. The secondary-list
|
||||
branch scrolled the offset but did not touch the timer.
|
||||
|
||||
**Fix**
|
||||
|
||||
Moved the two resets to the very top of `advanceDisplayOffset()`:
|
||||
|
||||
```cpp
|
||||
void advanceDisplayOffset() {
|
||||
// Any successful click (single, double, or long-press-release) ends up
|
||||
// here, so this is the single place that resets the auto-return timer.
|
||||
lastButtonActivity = millis();
|
||||
autoScrollExecuted = false;
|
||||
// ... rest of the function unchanged
|
||||
}
|
||||
```
|
||||
|
||||
### Other changes (cosmetic / non-behavioural)
|
||||
|
||||
- Filename and three user-visible version strings bumped to v7.2:
|
||||
- `connectToWiFi()` splash: `"Elec. Rate SI v7.1"` → `"v7.2"`
|
||||
- `displaySecondaryList()` credit line (line 18): `"price ticker v7.1"` → `"v7.2"`
|
||||
- `setup()` debug banner: `"Starting Dynamic Electricity Ticker v7.1 (Neg Price Fee) - DST-SAFE"`
|
||||
→ `"Starting Dynamic Electricity Ticker v7.2 (Button Robustness) - DST-SAFE"`
|
||||
- Inline comments added at each fix site explaining what v7.1 did wrong,
|
||||
so future maintainers do not re-introduce the overrides.
|
||||
- New global flag `bool buttonEverReleased` (see Fix 2).
|
||||
|
||||
### Files changed
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `ESP32_standalone_electricity_ticker_7_2.ino` | Filename, header comment, `buttonEverReleased` flag, `handleButton()` long-press gate, `displayPriceRow()` past-hour guard, `displayPrimaryList()` removed override, `advanceDisplayOffset()` timer reset, three version strings |
|
||||
|
||||
---
|
||||
|
||||
## v7.1 - Negative Price Provider Fee (2026-04-06)
|
||||
|
||||
**Summary**
|
||||
|
||||
Added a separate configurable provider fee for negative spot prices, correctly
|
||||
modelling contracts where the provider's fee structure differs between positive
|
||||
and negative market prices.
|
||||
|
||||
### What changed
|
||||
|
||||
**New constant (in `// Price computation` globals block):**
|
||||
```cpp
|
||||
const float NEG_PRICE_COMPANY_FEE_PERCENTAGE = 30.0;
|
||||
```
|
||||
|
||||
**Price calculation is now:**
|
||||
|
||||
| Market price | Formula |
|
||||
|---|---|
|
||||
| Positive (`raw >= 0`) | `raw × (1 + POWER_COMPANY_FEE_PERCENTAGE/100) × (1 + VAT_PERCENTAGE/100)` |
|
||||
| Negative (`raw < 0`) | `raw × (1 - NEG_PRICE_COMPANY_FEE_PERCENTAGE/100) × (1 + VAT_PERCENTAGE/100)` |
|
||||
|
||||
The switch happens on the **raw API price** before any multiplier is applied.
|
||||
VAT is applied to both cases, consistent with net billing contracts where VAT
|
||||
is calculated on the monthly net sum (mathematically equivalent due to VAT
|
||||
being a linear multiplier).
|
||||
|
||||
**Key values for `NEG_PRICE_COMPANY_FEE_PERCENTAGE`:**
|
||||
|
||||
| Value | Meaning |
|
||||
|---|---|
|
||||
| `30.0` | Provider keeps 30%, pays you 70% of the negative market price |
|
||||
| `0.0` | Provider passes the full negative price to you (no fee deducted) |
|
||||
|
||||
**All 5 fee calculation sites updated:**
|
||||
|
||||
| Function | Purpose |
|
||||
|---|---|
|
||||
| `updateLeds()` | LED brightness reflects correct negative price |
|
||||
| `format15MinPrice()` | 15-min row values on primary display |
|
||||
| `displayPriceRow()` | Hourly price rows on primary display |
|
||||
| `displaySecondaryList()` | Daily average on secondary info screen |
|
||||
| Version strings | `connectToWiFi()` LCD and `displaySecondaryList()` credit line |
|
||||
|
||||
---
|
||||
|
||||
## v7.0 - Rolling 48-Hour Logic & Midnight Bridge (2026-04-03)
|
||||
|
||||
**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 +503,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 +566,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 +638,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 |
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -11,21 +11,108 @@ This project is an Arduino‑IDE‑friendly firmware for the **Seeed XIAO ESP32
|
||||
- Uses a white LED and an optional presence sensor to give quick visual feedback.
|
||||
- Stores daily price data in **NVS** to survive reboots and reduce API calls.
|
||||
|
||||
The latest sketch implements **Version 6.2.4**.
|
||||
The latest sketch implements **Version 7.2** — a bug-fix release that
|
||||
restores primary-screen scrolling, fixes double-click → secondary-screen
|
||||
switching on the ESP32-C3, and stabilises end-of-day behaviour when no
|
||||
tomorrow data is available yet. The v7.1 negative-price provider fee logic
|
||||
is preserved unchanged.
|
||||
|
||||
---
|
||||
|
||||
## Version Highlights
|
||||
|
||||
### v6.2.4 - Exact-boundary display refresh bug (critical fix of v6.2.3 update)
|
||||
- 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.
|
||||
### v7.2 - Button Robustness & Screen-Control Fixes (2026-08-04)
|
||||
|
||||
Bug-fix release. Resolves the three control glitches reported for v7.1:
|
||||
"cannot scroll the primary screen at 20:35", "double-click does not switch
|
||||
to the secondary screen", and "strange end-of-day behaviour at 22:15 with
|
||||
no tomorrow data". No fee/VAT math, NVS layout, API scheduling or
|
||||
48-hour scrolling behaviour was changed.
|
||||
|
||||
- **Fix 1 – Primary-screen scroll** works at any hour of the day:
|
||||
past-hour blanking in `displayPriceRow()` simplified from
|
||||
`currentHour < 22` to a plain `localHourIndex < currentHour`, and the
|
||||
`currentHour >= 21` override in `displayPrimaryList()` removed.
|
||||
- **Fix 2 – Double-click** reliably toggles to the secondary screen.
|
||||
A new `bool buttonEverReleased` flag prevents a false
|
||||
"Long press detected!" from firing right after every reset on the
|
||||
ESP32-C3 (floating button pin + `buttonPressStartTime = 0` was making
|
||||
the long-press detector trip on phantom 3-second holds during boot).
|
||||
- **Fix 3 – End-of-day scroll** is stable with no tomorrow data. The
|
||||
`allowedAhead = 2` hack in `advanceDisplayOffset()` is gone, so the
|
||||
user can no longer scroll into "24:00 / 25:00" and wrap back to 00:00.
|
||||
- **Fix 4 (bonus) – Auto-return timer** now resets on every click, so
|
||||
scrolling the secondary status page no longer jumps back to the primary
|
||||
view mid-read.
|
||||
|
||||
See [`VERSION.md`](./VERSION.md) for the highlights and
|
||||
[`CHANGELOG.md`](./CHANGELOG.md) for full implementation details.
|
||||
|
||||
---
|
||||
|
||||
### v7.1 - Negative Price Provider Fee (2026-04-06)
|
||||
|
||||
Added a separate configurable provider fee for negative spot prices, correctly
|
||||
modelling contracts where the provider's fee structure differs between positive
|
||||
and negative market prices.
|
||||
|
||||
**New constant:**
|
||||
```cpp
|
||||
const float NEG_PRICE_COMPANY_FEE_PERCENTAGE = 30.0;
|
||||
```
|
||||
|
||||
**Price calculation is now:**
|
||||
|
||||
| Market price | Formula |
|
||||
|---|---|
|
||||
| Positive (`raw >= 0`) | `raw × (1 + POWER_COMPANY_FEE_PERCENTAGE/100) × (1 + VAT_PERCENTAGE/100)` |
|
||||
| Negative (`raw < 0`) | `raw × (1 - NEG_PRICE_COMPANY_FEE_PERCENTAGE/100) × (1 + VAT_PERCENTAGE/100)` |
|
||||
|
||||
The switch happens on the **raw API price** before any multiplier is applied.
|
||||
VAT is applied to both cases, consistent with net billing contracts where VAT
|
||||
is calculated on the monthly net sum (mathematically equivalent due to VAT
|
||||
being a linear multiplier).
|
||||
|
||||
**Key values for `NEG_PRICE_COMPANY_FEE_PERCENTAGE`:**
|
||||
|
||||
| Value | Meaning |
|
||||
|---|---|
|
||||
| `30.0` | Provider keeps 30%, pays you 70% of the negative market price |
|
||||
| `0.0` | Provider passes the full negative price to you (no fee deducted) |
|
||||
|
||||
All 5 fee calculation sites updated: `updateLeds()`, `format15MinPrice()`,
|
||||
`displayPriceRow()`, `displaySecondaryList()` (daily average), and version strings.
|
||||
|
||||
---
|
||||
|
||||
### v7.0 - Rolling 48-Hour Logic & Midnight Bridge (Major Upgrade)
|
||||
|
||||
This version is the **"Golden Build"** for this hardware platform. It combines all hardware stability fixes from v6.2.4 with a revolutionary new 48-hour price prediction system.
|
||||
|
||||
#### Key New Features
|
||||
|
||||
- **Midnight Bridge**: Instantly promotes pre-fetched tomorrow's data to become today's data at midnight, eliminating the "1 AM fetch gap"
|
||||
- **Dual-Buffer NVS**: Stores "Today" and "Tomorrow" data independently
|
||||
- **47-Hour Scrolling**: View up to 47 hours of price data when tomorrow's data is available
|
||||
- **Visual Tomorrow Indicators**: Future hours are marked with `HH:>>` format
|
||||
- **Smart Fetching**: Automatically fetches tomorrow's data after 14:00 (2 PM)
|
||||
|
||||
#### Technical Highlights
|
||||
|
||||
- **Instant Midnight Transition**: No more "No Data" screen at midnight
|
||||
- **Power-Failure Resilience**: New day's data is saved to NVS immediately after midnight swap
|
||||
- **Correct Min/Max for Tomorrow**: Price indicators correctly reference tomorrow's statistics
|
||||
- **LED Indicators Pinned to Current**: White LED always reflects actual current prices
|
||||
|
||||
### v6.2.4 - Exact-boundary display refresh bug (critical fix of v6.2.3 update)
|
||||
- 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() pre-calculated the next boundary.
|
||||
- Fix: Simplified findCurrentPriceIndex() to use a robust "last entry <= now" comparison. This ensures the display transitions to the new hour instantaneously at XX:00:00.
|
||||
|
||||
### v6.2.3 - State-based display refresh logic (critical fix of v6.2.2 update)
|
||||
### v6.2.3 - State-based display refresh logic (critical fix of v6.2.2 update)
|
||||
- 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.
|
||||
- Fix: 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.
|
||||
|
||||
### v6.2.2 - Display Blank Lines Issue Fix (critical fix of v6.2.1 update)
|
||||
### v6.2.2 - Display Blank Lines Issue Fix (critical fix of v6.2.1 update)
|
||||
- Problem: Sometimes rows 0 and 1 (current 15-min prices and current hour) were blank.
|
||||
- Cause: The "hour suppression" logic was hiding the current hour unexpectedly.
|
||||
- Fix:
|
||||
@@ -38,7 +125,7 @@ The latest sketch implements **Version 6.2.4**.
|
||||
|
||||
Major update sketch implements **Version 6.2.0**, focusing on:
|
||||
|
||||
- **Version 6.2.0 FIX**: **DST (Daylight Saving Time) handling fully fixed** – the ticker now works correctly on ALL days including DST switch days (spring forward and fall back). Uses timestamp-based price lookups instead of arithmetic calculations.
|
||||
- **Version 6.2.0 FIX**: **DST (Daylight Saving Time) handling fully fixed** – the ticker now works correctly on ALL days including DST switch days (spring forward and fall back). Uses timestamp-based lookups throughout.
|
||||
- Version 6.1.2 fix: restore correct **white LED indicator** behavior (ESP32 PWM fix; no dim glow when off).
|
||||
- Version 6.1.1 fix: Correct daily **low/high hourly markers** (now includes negative and **0.0** prices).
|
||||
- Daily (not hourly) API fetching.
|
||||
@@ -51,7 +138,7 @@ Major update sketch implements **Version 6.2.0**, focusing on:
|
||||
|
||||
## DST (Daylight Saving Time) – How It Works
|
||||
|
||||
### v6.2.0: Fully DST-Safe
|
||||
### v6.2.0+: Fully DST-Safe
|
||||
|
||||
**Important**: Starting with v6.2.0, the ticker is **fully DST-safe** and requires **no manual intervention** on DST switch days.
|
||||
|
||||
@@ -59,9 +146,9 @@ The firmware uses **timestamp-based price lookups** that work correctly regardle
|
||||
|
||||
| Day Type | Hours in Day | Price Entries | Status |
|
||||
|----------|-------------|---------------|--------|
|
||||
| Normal | 24 | 96 | ✅ Works |
|
||||
| Spring forward (March) | 23 | 92 | ✅ Works (fixed in v6.2.0) |
|
||||
| Fall back (October) | 25 | 100 | ✅ Works (fixed in v6.2.0) |
|
||||
| Normal | 24 | 96 | Works |
|
||||
| Spring forward (March) | 23 | 92 | Works (fixed in v6.2.0) |
|
||||
| Fall back (October) | 25 | 100 | Works (fixed in v6.2.0) |
|
||||
|
||||
### Timezone Configuration
|
||||
|
||||
@@ -91,6 +178,144 @@ The rest of the code works unchanged because it uses timestamp-based lookups.
|
||||
|
||||
---
|
||||
|
||||
## The Midnight Bridge (v7.0)
|
||||
|
||||
### The Problem with Traditional Tickers
|
||||
|
||||
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.
|
||||
|
||||
### The Solution: Midnight Bridge
|
||||
|
||||
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.
|
||||
|
||||
**How it works:**
|
||||
|
||||
1. **Pre-fetching**: After 14:00 (2 PM), the ticker automatically fetches tomorrow's prices using the `&start=YYYY-MM-DD` API parameter
|
||||
2. **Buffer Storage**: Tomorrow's data is stored in a separate NVS slot (`data_prc_t`)
|
||||
3. **Midnight Detection**: The main loop detects day rollover by comparing `tm_mday`
|
||||
4. **Instant Swap**: At 00:00:00, tomorrow's buffer instantly becomes today's data
|
||||
5. **NVS Persistence**: New day's data is saved immediately after swap (power-failure protection)
|
||||
|
||||
### Power-Failure Protection
|
||||
|
||||
Immediately after the midnight 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.
|
||||
|
||||
---
|
||||
|
||||
## Dual-Buffer System (v7.0)
|
||||
|
||||
The v7.0 firmware implements a dual-buffer system that stores today and tomorrow data independently:
|
||||
|
||||
### Buffer Comparison
|
||||
|
||||
| Buffer | Variable | NVS Keys | Contents |
|
||||
|--------|----------|----------|----------|
|
||||
| Today | `doc` | `data_prc`, `data_day`, `data_mon`, `data_year` | Current day's prices |
|
||||
| Tomorrow | `docTomorrow` | `data_prc_t`, `data_store_t` | Next day's prices |
|
||||
|
||||
### Statistics Per Buffer
|
||||
|
||||
Each buffer maintains its own statistics:
|
||||
- **Daily average**: `averagePrice` / `averagePriceTomorrow`
|
||||
- **Lowest price index**: `lowestPriceIndex` / `lowestPriceIndexTomorrow`
|
||||
- **Highest price index**: `highestPriceIndex` / `highestPriceIndexTomorrow`
|
||||
|
||||
### Display Selection
|
||||
|
||||
The display logic automatically selects the correct buffer based on the time offset:
|
||||
|
||||
```cpp
|
||||
bool showTomorrow = (totalHourOffset >= 24);
|
||||
StaticJsonDocument<Config::JSON_BUFFER_SIZE>& targetDoc = showTomorrow ? docTomorrow : doc;
|
||||
int lowIdx = showTomorrow ? lowestPriceIndexTomorrow : lowestPriceIndex;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 48-Hour Scrolling (v7.0)
|
||||
|
||||
### Extended Range
|
||||
|
||||
When tomorrow's data is available, users can scroll up to **47 hours ahead**:
|
||||
|
||||
```cpp
|
||||
int maxOffsetLimit = isTomorrowDataAvailable ? 47 : 23;
|
||||
```
|
||||
|
||||
### Visual Tomorrow Indication
|
||||
|
||||
Future hours (tomorrow) are displayed with `HH:>>` format to clearly distinguish them from today's hours:
|
||||
|
||||
```
|
||||
Today's hour: 14:00 | Tomorrow's hour: 14:>>
|
||||
```
|
||||
|
||||
### Correct Min/Max Indicators
|
||||
|
||||
The low/high price markers (arrows) correctly reference tomorrow's statistics when viewing tomorrow's hours:
|
||||
|
||||
```cpp
|
||||
if (dataIndex == lowIdx) {
|
||||
lcd.write(byte(3)); // Low price arrow
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Behavior & Display States (v7.2)
|
||||
|
||||
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 browsing future hours. |
|
||||
| **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.2" followed by "Connecting..." and progress dots. | Built-in LED is **OFF** until connection is established. |
|
||||
|
||||
### Key UX Principle: LEDs Stay Pinned to Current Time
|
||||
|
||||
Unlike the display which can scroll through future hours, the white LED **always** reflects the actual current price status. This means:
|
||||
- Even while browsing tomorrow's cheap hours, the LED tells you the **true current** price situation
|
||||
- This prevents confusion and helps you decide "should I turn on the dishwasher **now**?"
|
||||
|
||||
---
|
||||
|
||||
## API Call Intervals & Retry Strategy (v7.0)
|
||||
|
||||
### 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 = 5`)
|
||||
- **Backoff Factor:** 2 (`HTTP_GET_BACKOFF_FACTOR = 2`)
|
||||
- **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`:
|
||||
|
||||
- **Behavior:** Bypasses the standard daily schedule and retries the API **more aggressively**
|
||||
- **Initial Interval:** Attempts every minute until successful
|
||||
- **Goal:** 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 making API calls constantly, the device performs these checks continuously:
|
||||
|
||||
- **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
|
||||
|
||||
---
|
||||
|
||||
## Bidding Zones (BZN) / Region Selection
|
||||
|
||||
The firmware currently uses:
|
||||
@@ -107,9 +332,9 @@ const char* api_url = "https://api.energy-charts.info/price?bzn=SI";
|
||||
|
||||
to any supported BZN.
|
||||
|
||||
All available bidding zones (from the original README):
|
||||
All available bidding zones:
|
||||
|
||||
- `AT` ‑ Austria
|
||||
- `AT` ‑ Austria
|
||||
- `BE` ‑ Belgium
|
||||
- `BG` ‑ Bulgaria
|
||||
- `CH` ‑ Switzerland
|
||||
@@ -161,7 +386,7 @@ All available bidding zones (from the original README):
|
||||
|
||||
## Hardware Setup (Detailed)
|
||||
|
||||
This section merges the original v5.5 instructions with the current v6.1 hardware expectations.
|
||||
This section merges the original v5.5 instructions with the current v7.2 hardware expectations.
|
||||
Follow it carefully to reproduce the working setup.
|
||||
|
||||
### 1. Microcontroller
|
||||
@@ -181,7 +406,7 @@ Typical pins used in the sketch:
|
||||
### 2. 20x4 I²C LCD (2004) – PCF8574 Backpack
|
||||
|
||||
- LCD: **20x4 2004 character display** with I²C backpack (PCF8574 or compatible).
|
||||
- Default I²C address (in code): `0x27`
|
||||
- Default I²C address (in code): `0x27`
|
||||
(Change in the sketch if your module differs: `LiquidCrystal_I2C lcd(0x27, 20, 4);`)
|
||||
|
||||
**Connections:**
|
||||
@@ -298,7 +523,7 @@ The sketch uses a **white LED** (or LED strip control line) on `GPIO 5` (`whiteL
|
||||
- Ensure the **strip power supply shares ground** with the ESP32‑C3 board.
|
||||
- Do **not** drive large loads directly from the GPIO pin.
|
||||
|
||||
The LED is driven with various patterns to indicate price level; see “LED Price Signalling” below.
|
||||
The LED is driven with various patterns to indicate price level; see "LED Price Signalling" below.
|
||||
|
||||
---
|
||||
|
||||
@@ -311,29 +536,39 @@ The LED is driven with various patterns to indicate price level; see “LED Pric
|
||||
|
||||
---
|
||||
|
||||
## Firmware Features (v6.2.0)
|
||||
## Firmware Features (v7.2)
|
||||
|
||||
### Core Display & Pricing
|
||||
|
||||
- Data source: `https://api.energy-charts.info/price?bzn=SI`
|
||||
- Resolution: 15‑minute intervals with hourly averages
|
||||
- Display shows:
|
||||
- Display shows up to **47 hours** of price data (when tomorrow's data is available)
|
||||
- **Row 0**: Current hour, four 15‑minute values
|
||||
- **Rows 1–3**: Current hour + next two hours as hourly averages
|
||||
- Price calculation: Raw MWh → EUR/kWh with configurable surcharges (power company fee + VAT)
|
||||
- Two configurable surcharges:
|
||||
- `POWER_COMPANY_FEE_PERCENTAGE` (default `12.0` %).
|
||||
- `VAT_PERCENTAGE` (default `22.0` %).
|
||||
- **v7.0 Feature**: Tomorrow's hours are marked with `HH:>>` format
|
||||
- Price calculation: Raw MWh → EUR/kWh with configurable surcharges
|
||||
- Three configurable constants:
|
||||
- `POWER_COMPANY_FEE_PERCENTAGE` (default `12.0` %) — fee for **positive** spot prices
|
||||
- `NEG_PRICE_COMPANY_FEE_PERCENTAGE` (default `30.0` %) — fee kept by provider on **negative** spot prices
|
||||
- `VAT_PERCENTAGE` (default `22.0` %)
|
||||
- **v7.1**: Positive and negative spot prices use independent fee multipliers:
|
||||
|
||||
| Market price | Formula |
|
||||
|---|---|
|
||||
| Positive (`raw >= 0`) | `raw × (1 + POWER_COMPANY_FEE_PERCENTAGE/100) × (1 + VAT_PERCENTAGE/100)` |
|
||||
| Negative (`raw < 0`) | `raw × (1 - NEG_PRICE_COMPANY_FEE_PERCENTAGE/100) × (1 + VAT_PERCENTAGE/100)` |
|
||||
|
||||
- LCD:
|
||||
- `LiquidCrystal_I2C` with custom characters for:
|
||||
- Local language letters.
|
||||
- Low‑price and high‑price indicators.
|
||||
- **Daily min/max markers**:
|
||||
- The low/high hourly indicators consider **negative**, **0.0**, and positive prices (v6.1.1 fix).
|
||||
- The low/high hourly indicators consider **negative**, **0.0**, and positive prices
|
||||
- **v7.0 Feature**: Tomorrow's min/max indices are tracked separately and displayed correctly
|
||||
|
||||
### LED Price Signalling
|
||||
|
||||
The white LED (GPIO 5) reflects the **current 15‑minute interval** price:
|
||||
The white LED (GPIO 5) reflects the **current 15‑minute interval** price (regardless of what's displayed on screen):
|
||||
|
||||
| Price Level | LED Behavior |
|
||||
|-------------|--------------|
|
||||
@@ -370,40 +605,45 @@ LED is **disabled** when:
|
||||
One button (or touch) on GPIO 4 controls the UI:
|
||||
|
||||
- **Single short press**:
|
||||
- On primary screen: scrolls the time offset (future hours).
|
||||
- On primary screen: scrolls the time offset (future hours up to 47h in v7.0).
|
||||
- On secondary screen: scrolls through the 20‑line status text (4 lines at a time).
|
||||
- **Double press**:
|
||||
- Toggles between:
|
||||
- Primary price view.
|
||||
- Secondary status/info view.
|
||||
- **Long press (~3 seconds in v6.1)**:
|
||||
- **Long press (~3 seconds)**:
|
||||
- While held:
|
||||
- LCD shows: “Long press detected! Release to refresh”.
|
||||
- LCD shows: "Long press detected! Release to refresh".
|
||||
- On release:
|
||||
- Forces a **manual data refresh**:
|
||||
- Sets `nextScheduledFetchTime = now`.
|
||||
- Shows “Manual Refresh… Please wait…”.
|
||||
- Shows "Manual Refresh… Please wait…".
|
||||
- `handleDataFetching()` will perform an immediate API fetch outside the normal schedule.
|
||||
|
||||
An **auto‑scroll timeout** resets the view to “current hour / top of lists” after inactivity.
|
||||
An **auto‑scroll timeout** resets the view to "current hour / top of lists" after inactivity.
|
||||
|
||||
---
|
||||
|
||||
## NVS Storage (Daily Data Cache)
|
||||
## NVS Storage (v7.0: Enhanced with Dual Buffers)
|
||||
|
||||
This firmware uses ESP32‑C3 **Preferences API** (`Preferences`) under namespace `"my-ticker"`.
|
||||
|
||||
Stored keys:
|
||||
### Stored Keys (v7.0)
|
||||
|
||||
- **Wi‑Fi credentials:**
|
||||
- `ssid`
|
||||
- `pass`
|
||||
- **Daily price data:**
|
||||
- `data_day` – calendar day (1–31)
|
||||
- `data_mon` – month (0–11)
|
||||
- `data_year` – full year (e.g. 2026)
|
||||
- `data_prc` – full raw JSON payload from the API
|
||||
- `data_last_store` – Unix time (`time_t`) when data was last written
|
||||
**Wi‑Fi credentials:**
|
||||
- `ssid`
|
||||
- `pass`
|
||||
|
||||
**Today's price data:**
|
||||
- `data_day` – calendar day (1–31)
|
||||
- `data_mon` – month (0–11)
|
||||
- `data_year` – full year (e.g. 2026)
|
||||
- `data_prc` – full raw JSON payload from the API
|
||||
- `data_last_store` – Unix time (`time_t`) when data was last written
|
||||
|
||||
**Tomorrow's price data (v7.0 new):**
|
||||
- `data_prc_t` – full raw JSON payload for next day
|
||||
- `data_store_t` – Unix time when tomorrow's data was stored
|
||||
|
||||
### On Boot
|
||||
|
||||
@@ -412,22 +652,32 @@ After successful NTP time sync:
|
||||
1. Attempt to load `data_day`, `data_mon`, `data_year`, and `data_prc` from NVS.
|
||||
2. If **stored date matches current local date**:
|
||||
- Deserialize `data_prc` into `StaticJsonDocument doc`.
|
||||
- Run `processJsonData()` as if it were fresh from the API.
|
||||
- Run `processJsonData(false)` as if it were fresh from the API.
|
||||
- Set `isTodayDataAvailable = true`.
|
||||
- **Skip** the initial API call to save traffic.
|
||||
3. If the stored date does **not** match today or JSON parsing fails:
|
||||
3. Attempt to load tomorrow's data from `data_prc_t`.
|
||||
4. If the stored date does **not** match today or JSON parsing fails:
|
||||
- NVS data is **ignored** for display.
|
||||
- System starts from “No data for today”.
|
||||
- System starts from "No data for today".
|
||||
- Schedules an immediate API fetch.
|
||||
|
||||
### After Each Successful Fetch for Today
|
||||
### After Each Successful Fetch
|
||||
|
||||
- Raw JSON payload is stored into NVS as `data_prc`, along with date (`data_day`, `data_mon`, `data_year`) and `data_last_store`.
|
||||
- On reboot later the same day, the device will show prices immediately from NVS without hitting the API.
|
||||
- Today's data: Raw JSON payload is stored into NVS as `data_prc`, along with date and `data_last_store`.
|
||||
- Tomorrow's data (v7.0): After 14:00, tomorrow's payload is stored as `data_prc_t` with `data_store_t`.
|
||||
|
||||
### Midnight Bridge NVS Update (v7.0)
|
||||
|
||||
At midnight rollover:
|
||||
1. Tomorrow's buffer is swapped to become today's buffer
|
||||
2. New "today" data is immediately serialized and saved to NVS
|
||||
3. Tomorrow's NVS slot is cleared
|
||||
|
||||
This ensures power-failure resilience: if power is lost immediately after midnight, the device boots with valid data.
|
||||
|
||||
---
|
||||
|
||||
## Daily Fetch Strategy (v6.1.0+)
|
||||
## Daily Fetch Strategy (v7.0: Enhanced with Smart Tomorrow Fetching)
|
||||
|
||||
### Goals
|
||||
|
||||
@@ -435,17 +685,18 @@ After successful NTP time sync:
|
||||
- Fetch:
|
||||
- Once after boot (if no valid NVS data for today).
|
||||
- Once per **new day** (after midnight), with robust retries while the next‑day dataset is not yet published.
|
||||
- **NEW in v7.0**: Tomorrow's data automatically after 14:00 local time.
|
||||
|
||||
### Time Sync & First Fetch
|
||||
|
||||
- `configTzTime(TZ_CET_CEST, "pool.ntp.org")` is used to enable CET/CEST aware `localtime()` and `getLocalTime()`.
|
||||
- Until time sync completes, the UI only shows “Syncing Time… Please wait…”.
|
||||
- Until time sync completes, the UI only shows "Syncing Time… Please wait…".
|
||||
- On first successful sync:
|
||||
- `isTimeSynced = true`.
|
||||
- `trackedDay` is set to the current `tm_mday`.
|
||||
- Either NVS is used (if it has today’s data) or an initial fetch is scheduled.
|
||||
- Either NVS is used (if it has today's data) or an initial fetch is scheduled.
|
||||
|
||||
### Day‑Rollover Detection
|
||||
### Day‑Rollover Detection (v7.0: Enhanced with Midnight Bridge)
|
||||
|
||||
In the main `loop()`:
|
||||
|
||||
@@ -455,109 +706,45 @@ In the main `loop()`:
|
||||
- If `tm_mday != trackedDay`:
|
||||
- Day rollover detected (midnight).
|
||||
- `trackedDay` updated.
|
||||
- Immediately:
|
||||
- `isTodayDataAvailable = false`.
|
||||
- `displayState = NO_DATA_OFFSET`.
|
||||
- `timeOffsetHours = 0`.
|
||||
- White LED turned off.
|
||||
- **Midnight phase** is entered:
|
||||
- `midnightPhaseActive = true`.
|
||||
- `midnightRetryCount = 0`.
|
||||
- `nextScheduledFetchTime = now` (immediate attempt).
|
||||
- LCD updated to “No data for today. Press & hold to refresh manually”.
|
||||
- **v7.0 Midnight Bridge Logic**:
|
||||
- If tomorrow's data is available:
|
||||
- Instantly swap `docTomorrow` to `doc`
|
||||
- Update all statistics (`averagePrice`, `lowestPriceIndex`, etc.)
|
||||
- Save to NVS and clear tomorrow slot
|
||||
- Reset `timeOffsetHours` to 0
|
||||
- If tomorrow's data is NOT available:
|
||||
- Enter "No Data" mode
|
||||
- Start midnight retry phase
|
||||
|
||||
### “Today” Detection (Market Day Logic)
|
||||
### Smart Tomorrow Fetching (v7.0)
|
||||
|
||||
The Energy‑Charts API can keep serving **yesterday’s** market day for some time after local midnight.
|
||||
To avoid accidentally accepting yesterday’s data as today’s, v6.1 uses a more robust rule.
|
||||
After 14:00 local time, if tomorrow's data is not yet available:
|
||||
|
||||
```cpp
|
||||
if (ti->tm_hour >= 14 && !isTomorrowDataAvailable) {
|
||||
fetchAndProcessData(true); // Fetch tomorrow's data
|
||||
}
|
||||
```
|
||||
|
||||
The API URL is constructed with the `&start=YYYY-MM-DD` parameter for the next day.
|
||||
|
||||
### "Today" Detection (Market Day Logic)
|
||||
|
||||
The Energy‑Charts API can keep serving **yesterday's** market day for some time after local midnight.
|
||||
To avoid accidentally accepting yesterday's data as today's, v6.1+ uses a more robust rule.
|
||||
|
||||
In `processJsonData()`:
|
||||
|
||||
1. Read `unix_seconds[]`.
|
||||
2. Interpret the **LAST** timestamp as representing the end of the dataset’s market day.
|
||||
2. Interpret the **LAST** timestamp as representing the end of the dataset's market day.
|
||||
3. Convert it to local time (`localtime()`).
|
||||
4. Compare its date (day, month, year) to the current local date.
|
||||
4. Compare its date (day, month, year) to the current local date (or tomorrow's date if `isTomorrow` is true).
|
||||
- If they **match**:
|
||||
- Dataset is accepted as “today’s” data.
|
||||
- `isTodayDataAvailable = true`.
|
||||
- `lastProcessJsonAcceptedToday = true`.
|
||||
- Prices are processed (hourly averages, min/max, daily average).
|
||||
- Dataset is accepted as valid.
|
||||
- Statistics are updated for the appropriate buffer.
|
||||
- If they **do not match**:
|
||||
- Dataset is considered to belong to a **different** day (e.g. yesterday).
|
||||
- `isTodayDataAvailable = false`.
|
||||
- `lastProcessJsonAcceptedToday = false`.
|
||||
- Function returns without updating display data.
|
||||
|
||||
This prevents the device from accidentally treating “yesterday’s day‑ahead curve” as if it were already “today”.
|
||||
|
||||
### Midnight Retry Logic (v6.1.0 + your tuning)
|
||||
|
||||
When `midnightPhaseActive == true`, any scheduled fetch that:
|
||||
|
||||
- Fails at HTTP/JSON level, **or**
|
||||
- Succeeds at HTTP/JSON level but `processJsonData()` **rejects** the dataset as “not today”
|
||||
|
||||
is treated as a **failure** for scheduling.
|
||||
|
||||
The retry rules:
|
||||
|
||||
1. **First hour after midnight – fast retries:**
|
||||
|
||||
In `scheduleAfterMidnightFailure()` (with your current config):
|
||||
|
||||
```cpp
|
||||
if (midnightRetryCount < 2) {
|
||||
// Retry every 20 minutes for first 2 attempts (~1 hour window)
|
||||
midnightRetryCount++;
|
||||
nextScheduledFetchTime = now + 1200; // 20 minutes
|
||||
debugPrint(2, "Midnight retry " + String(midnightRetryCount) + "/2 in 20 minutes");
|
||||
} else {
|
||||
// After that, retry only at top of each hour
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Timeline:
|
||||
|
||||
- 00:00 – first attempt at rollover.
|
||||
- If data is still yesterday’s:
|
||||
- 00:20 – 1st retry.
|
||||
- 00:40 – 2nd retry.
|
||||
- All “fast retries” remain fully within the first post‑midnight hour.
|
||||
|
||||
2. **After the first hour – hourly retries:**
|
||||
|
||||
Once `midnightRetryCount >= 2`, next retries are scheduled at the **top of each hour**:
|
||||
|
||||
```cpp
|
||||
struct tm* ti = localtime(&now);
|
||||
if (ti != NULL) {
|
||||
time_t nextHour = now - (ti->tm_min * 60) - ti->tm_sec + 3600;
|
||||
nextScheduledFetchTime = nextHour;
|
||||
debugPrint(2, "Midnight retries exhausted; next fetch top-of-hour");
|
||||
} else {
|
||||
nextScheduledFetchTime = now + 3600;
|
||||
debugPrint(2, "Midnight retries exhausted; fallback 1h");
|
||||
}
|
||||
```
|
||||
|
||||
So after ~00:40, if still no valid dataset for today, the device tries again at ~01:00, 02:00, 03:00, … until success.
|
||||
|
||||
3. **Success condition & exit from midnight phase:**
|
||||
|
||||
A scheduled fetch is treated as a **real success** only if:
|
||||
|
||||
- HTTP + JSON succeed **and**
|
||||
- `lastProcessJsonAcceptedToday == true` (dataset’s last timestamp’s date matches today).
|
||||
|
||||
When this happens:
|
||||
|
||||
- `isTodayDataAvailable = true`.
|
||||
- `midnightPhaseActive = false`.
|
||||
- `midnightRetryCount = 0`.
|
||||
- LCD leaves `NO_DATA_OFFSET` back to `CURRENT_PRICES`.
|
||||
- White LED resumes price indication.
|
||||
- `nextScheduledFetchTime` is set ≈24 hours ahead (until the next midnight rollover resets it).
|
||||
- Dataset is rejected.
|
||||
- Appropriate availability flag is set to false.
|
||||
|
||||
---
|
||||
|
||||
@@ -565,15 +752,15 @@ The retry rules:
|
||||
|
||||
A **secondary screen** (toggled via **double‑click**) provides 20 lines of status information, displayed 4 lines at a time:
|
||||
|
||||
Typical content:
|
||||
Typical content (updated for v7.2):
|
||||
|
||||
1. Current date and time (`HH:MM DD.MM.YYYY`)
|
||||
2. Separator line (`--------------------`)
|
||||
3. “Zadnja posodobitev:” (Last update header)
|
||||
3. "Zadnja posodobitev:" (Last update header)
|
||||
4. Last successful fetch (for today) date & time
|
||||
5. Blank
|
||||
6. “Dnevno povprečje:” (Daily average)
|
||||
7. Daily average price in EUR/kWh (with surcharges) or “Cene niso na voljo.”
|
||||
6. "Dnevno povprečje:" (Daily average)
|
||||
7. Daily average price in EUR/kWh (with surcharges) or "Cene niso na voljo."
|
||||
8. Blank
|
||||
9. Wi‑Fi status and RSSI
|
||||
10. Local IP address
|
||||
@@ -583,12 +770,12 @@ Typical content:
|
||||
- `NVS status:`
|
||||
- `Data day: DD.MM.YYYY` or `Data day: none`
|
||||
- `Last save: DD.MM.YY` or `Last save: none`
|
||||
- `NVS: OK (today)` / `NVS: old data` / `NVS: empty`
|
||||
- `NVS: Today+Tomorrow` / `NVS: Today only` / `NVS: Empty/Old`
|
||||
17–20. Credits and version:
|
||||
- `energy-charts.info`
|
||||
- `dynamic electricity`
|
||||
- `price ticker v6.1`
|
||||
- `by Legolas-2025` (or your preferred credit line)
|
||||
- `price ticker v7.2`
|
||||
- `by Legolas-2025`
|
||||
|
||||
---
|
||||
|
||||
@@ -602,7 +789,7 @@ If NVS does not contain valid Wi‑Fi credentials, or if connecting fails repeat
|
||||
MyTicker_Setup
|
||||
```
|
||||
|
||||
2. LCD shows “No Wi‑Fi access! Setup Wi‑Fi: SSID: MyTicker_Setup” and the AP IP.
|
||||
2. LCD shows "No Wi‑Fi access! Setup Wi‑Fi: SSID: MyTicker_Setup" and the AP IP.
|
||||
3. A simple captive portal is served:
|
||||
- Open any URL while connected to `MyTicker_Setup`.
|
||||
- Enter SSID and password in the HTML form.
|
||||
@@ -620,7 +807,7 @@ If NVS does not contain valid Wi‑Fi credentials, or if connecting fails repeat
|
||||
- `DNSServer` (from ESP32 core)
|
||||
- `WebServer` (from ESP32 core)
|
||||
- `Preferences` (built‑in for ESP32)
|
||||
3. Open the v6.2.0 `.ino` file (e.g. `ESP32_standalone_electricity_ticker_6_1_2_nvs_daily_fetch.ino`).
|
||||
3. Open the v7.2 `.ino` file (`ESP32_standalone_electricity_ticker_7_2.ino`).
|
||||
4. In Tools:
|
||||
- Board: `Seeed XIAO ESP32C3`
|
||||
- Port: choose the correct serial port.
|
||||
@@ -630,30 +817,49 @@ If NVS does not contain valid Wi‑Fi credentials, or if connecting fails repeat
|
||||
- NTP sync messages.
|
||||
- NVS load/save status.
|
||||
- Midnight rollover and retry debug output.
|
||||
- Tomorrow fetch logs (`Fetching Tomorrow's Data...`)
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Versioning & Changelog
|
||||
|
||||
- **v7.2** – Button robustness & screen-control fixes (2026-08-04). See
|
||||
highlights above; full details in [`CHANGELOG.md`](./CHANGELOG.md).
|
||||
- **v7.1** – Negative price provider fee:
|
||||
- Separate `NEG_PRICE_COMPANY_FEE_PERCENTAGE` constant (default `30.0` %)
|
||||
- Positive prices: `raw × (1 + pos_fee) × (1 + VAT)`
|
||||
- Negative prices: `raw × (1 - neg_fee) × (1 + VAT)`
|
||||
- Switch on raw API price before any multiplier
|
||||
- All 5 fee calculation sites updated
|
||||
- **v7.0** – Rolling 48-Hour Logic & Midnight Bridge:
|
||||
- Dual-buffer NVS system for today and tomorrow data
|
||||
- Midnight Bridge for seamless day rollover
|
||||
- 47-hour scrolling with `HH:>>` visual indicators
|
||||
- Smart fetching of tomorrow's data after 14:00
|
||||
- Correct min/max indicators for tomorrow's hours
|
||||
- Power-failure resilient NVS updates
|
||||
- **v6.2.4** – Exact-boundary display refresh bug fix
|
||||
- **v6.2.3** – State-based display refresh logic fix
|
||||
- **v6.2.2** – Display blank lines issue fix
|
||||
- **v6.2.1** – Current interval fix
|
||||
- **v6.2.0** – DST handling fully fixed via timestamp-based lookups
|
||||
- **v6.1.2** – LED indicator restored (broken in previous version):
|
||||
- Avoid mixing PWM and `digitalWrite` on the same LED pin (ESP32 LEDC behavior).
|
||||
- Ensures LED is fully off when gated off; patterns operate correctly.
|
||||
- **v6.1.1** – Daily low/high marker fix:
|
||||
- Daily min/max and average now include negative and **0.0** prices.
|
||||
- **v6.1.0** – Midnight fetch & “today” detection fixes:
|
||||
- **v6.1.0** – Midnight fetch & "today" detection fixes:
|
||||
- Correctly detect **market day** using the last `unix_seconds` timestamp.
|
||||
- Distinguish between:
|
||||
- HTTP/JSON success, but data for **wrong day** (treated as failure).
|
||||
- Full success with accepted “today” dataset.
|
||||
- Full success with accepted "today" dataset.
|
||||
- Robust midnight retry scheme:
|
||||
- Two retries every 20 minutes in the first hour (~00:20, ~00:40).
|
||||
- Then hourly retries (top‑of‑hour) until today’s dataset is available.
|
||||
- Behavior on reboot and manual long‑press is unchanged, but now respects the improved “today” logic.
|
||||
- Then hourly retries (top‑of‑hour) until today's dataset is available.
|
||||
- Behavior on reboot and manual long‑press is unchanged, but now respects the improved "today" logic.
|
||||
- **v6.0.0** – NVS storage & daily fetch:
|
||||
- Store daily price data in NVS.
|
||||
- Reduce API calls to “boot + after‑midnight”.
|
||||
- Reduce API calls to "boot + after‑midnight".
|
||||
- Add NVS status section to secondary menu.
|
||||
- **v5.5** – 15‑minute detail mode, LED based on current 15‑minute slot, improved DST handling (see file `20251027a_electricity_ticker_10_5_5_latest_DST_and_midnight_fix.ino`).
|
||||
|
||||
@@ -661,6 +867,15 @@ See [`CHANGELOG.md`](./CHANGELOG.md) for more details.
|
||||
|
||||
---
|
||||
|
||||
## Data attribution
|
||||
|
||||
Electricity price data provided by
|
||||
**[Energy-Charts](https://energy-charts.info)** (Fraunhofer ISE)
|
||||
via the [Energy-Charts API](https://api.energy-charts.info),
|
||||
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License – see the [`LICENSE`](./LICENSE) file for details.
|
||||
|
||||
+170
-9
@@ -2,26 +2,187 @@
|
||||
|
||||
## Current firmware
|
||||
|
||||
- **Version:** 6.2.4
|
||||
- **Release date:** 2026-04-01
|
||||
- **Version:** 7.2
|
||||
- **Release date:** 2026-08-04
|
||||
- **Target MCU:** Seeed XIAO ESP32‑C3
|
||||
- **Display:** 20x4 I²C LCD (PCF8574, default address `0x27`)
|
||||
- **API endpoint:** `https://api.energy-charts.info/price?bzn=SI`
|
||||
- **Resolution:** 15‑minute intervals, hourly averages for overview
|
||||
|
||||
## Highlights of v6.2.4
|
||||
## Highlights of v7.2
|
||||
|
||||
- **BUG FIX:** Exact-boundary display refresh bug
|
||||
- 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.
|
||||
- Fix: Simplified findCurrentPriceIndex() to use a robust "last entry <= now" comparison. This ensures the display transitions to the new hour instantaneously at XX:00:00.
|
||||
### Button Robustness & Screen-Control Fixes
|
||||
|
||||
## Highlights of v6.2.3
|
||||
Bug-fix release that resolves the three control glitches reported for the v7.1
|
||||
firmware on the Seeed XIAO ESP32‑C3: the primary screen looked unscrollable,
|
||||
double-clicks failed to switch to the secondary status screen, and the end-of-day
|
||||
behaviour (no tomorrow data yet) was unstable around 22:00–23:59. No fee/VAT
|
||||
math, NVS layout, API scheduling or 48-hour scrolling behaviour was changed.
|
||||
|
||||
#### Fix 1 – Primary-screen scroll now works at any hour of the day
|
||||
|
||||
Two compounding bugs in `displayPrimaryList()` and `displayPriceRow()` were
|
||||
cancelling each other out and made single-click scrolling look dead:
|
||||
|
||||
- `displayPriceRow()` only blanked past hours while `currentHour < 22`. After
|
||||
22:00 the screen could repaint already-finished morning hours, so as soon
|
||||
as the user scrolled forward the new "top" hour was visually over-written
|
||||
by the previous morning's data. The guard is now
|
||||
`if (localHourIndex < currentHour) blank();`, so past hours of today are
|
||||
hidden at every hour of the day.
|
||||
- `displayPrimaryList()` contained an override
|
||||
`if (currentHour >= 21 && timeOffsetHours > 0) displayStartHourOffset = 21 + timeOffsetHours;`
|
||||
which pinned the top row at 21:00 + offset from 21:00 onward. At 22:15
|
||||
every click computed start = 22+offset, was then clamped to 21+offset, and
|
||||
the user saw no movement. The override is no longer needed and has been
|
||||
removed.
|
||||
|
||||
#### Fix 2 – Double-click on the secondary screen now fires reliably
|
||||
|
||||
The double-click path itself was correct; it was being starved by a false
|
||||
"Long press detected!" message that fired immediately after every reset.
|
||||
On the ESP32-C3 the button pin (`INPUT_PULLUP`) floats HIGH for a few
|
||||
seconds during boot, while `buttonPressStartTime` is initialised to `0`.
|
||||
As soon as `millis()` crossed the 3 s threshold, the long-press detector
|
||||
tripped on a phantom 3-second hold, cleared the LCD to
|
||||
"Long press detected! / Release to refresh", and from then on the user
|
||||
could not see any prices to click on (single- and double-click recognisers
|
||||
both still ran, but their visible effect was hidden behind the long-press
|
||||
splash).
|
||||
|
||||
Fix: a new `bool buttonEverReleased` is set to `true` the first time the
|
||||
pin is observed LOW after boot, and the long-press detector is gated on
|
||||
it: `if (buttonState == LOW && !longPressDetected && buttonEverReleased)`.
|
||||
The detector refuses to fire until the user (or the power-rail noise) has
|
||||
released the button at least once. The v7.1 button logic (debounce, 3 s
|
||||
long-press threshold, 500 ms double-click window, TTP223 timing) is
|
||||
otherwise preserved verbatim.
|
||||
|
||||
#### Fix 3 – End-of-day scroll is now stable when no tomorrow data is available
|
||||
|
||||
`advanceDisplayOffset()` contained a hack
|
||||
`if (allowedAhead < 2 && currentHour >= 21 && !isTomorrowDataAvailable) allowedAhead = 2;`
|
||||
which at 22:00 (with no tomorrow data) let the user click past hour 23 into
|
||||
"24:00 / 25:00", where `displayStartHourOffset` wrapped back to 0 and filled
|
||||
the LCD with the now-unblanked past-hour rows. The hack has been removed;
|
||||
the natural cap is now sufficient:
|
||||
|
||||
- **22:00** → can step 22 → 23, then wraps back to current
|
||||
- **23:00** → cannot step forward at all
|
||||
|
||||
No wrap to 00:00 of the previous day is reachable any more.
|
||||
|
||||
#### Fix 4 (bonus) – Auto-return timer now resets on every click
|
||||
|
||||
`lastButtonActivity` / `autoScrollExecuted` were only updated in the
|
||||
primary-list branches of `advanceDisplayOffset()`. Scrolling the secondary
|
||||
status page therefore did not push the 10 s auto-return-to-top timeout
|
||||
forward. The two resets are now at the top of `advanceDisplayOffset()` so
|
||||
every successful click (single, double, or long-press-release) refreshes
|
||||
the timer regardless of which list is showing.
|
||||
|
||||
#### Cosmetic / non-behavioural changes
|
||||
|
||||
- Filename and three user-visible version strings bumped to v7.2:
|
||||
- `connectToWiFi()` splash: `"Elec. Rate SI v7.1"` → `"v7.2"`
|
||||
- `displaySecondaryList()` credit line: `"price ticker v7.1"` → `"v7.2"`
|
||||
- `setup()` debug banner: `"v7.1 (Neg Price Fee)"` → `"v7.2 (Button Robustness)"`
|
||||
- Inline comments added at each fix site explaining what v7.1 did wrong,
|
||||
so future maintainers do not re-introduce the overrides.
|
||||
- New global flag `bool buttonEverReleased` (see Fix 2).
|
||||
|
||||
See `CHANGELOG.md` for full implementation details.
|
||||
|
||||
---
|
||||
|
||||
## Highlights of v7.1
|
||||
|
||||
### Negative Price Provider Fee
|
||||
|
||||
Added a separate provider fee for negative spot prices via a new constant
|
||||
`NEG_PRICE_COMPANY_FEE_PERCENTAGE`. Positive and negative market prices now
|
||||
use independent fee multipliers, correctly modelling contracts where the
|
||||
provider's fee structure differs between the two cases.
|
||||
|
||||
**Price calculation:**
|
||||
|
||||
| Market price | Formula |
|
||||
|---|---|
|
||||
| Positive (`raw >= 0`) | `raw × (1 + POWER_COMPANY_FEE_PERCENTAGE/100) × (1 + VAT_PERCENTAGE/100)` |
|
||||
| Negative (`raw < 0`) | `raw × (1 - NEG_PRICE_COMPANY_FEE_PERCENTAGE/100) × (1 + VAT_PERCENTAGE/100)` |
|
||||
|
||||
VAT is applied to both, consistent with net billing where VAT is calculated
|
||||
on the monthly net sum (linear equivalence applies).
|
||||
|
||||
All 5 fee calculation sites updated: `updateLeds()`, `format15MinPrice()`,
|
||||
`displayPriceRow()`, `displaySecondaryList()` (daily average), and version strings.
|
||||
|
||||
See `CHANGELOG.md` for full implementation details.
|
||||
|
||||
---
|
||||
|
||||
## Highlights of v7.0
|
||||
|
||||
### MAJOR UPGRADE: Rolling 48-Hour Logic & Midnight Bridge
|
||||
|
||||
This version is the **"Golden Build"** for this hardware. It represents the culmination of hardware stability fixes from v6.2.4 combined with revolutionary new 48-hour price prediction capabilities.
|
||||
|
||||
#### 1. The Midnight Bridge (Rollover Logic)
|
||||
|
||||
The most complex part of electricity tickers is handling the midnight transition. This code now correctly detects the moment the local clock moves from 23:59:59 to 00:00:00.
|
||||
|
||||
**The Swap:** Instead of waiting for a slow API call at midnight (which usually fails because the server hasn't updated yet), the code instantly promotes the "Tomorrow" buffer to become "Today" data.
|
||||
|
||||
**The NVS Update:** The code correctly serializes the new "Today" data and saves it to NVS immediately after the swap. This ensures that if power cuts at 00:05 AM, the device reboots with the correct data already loaded.
|
||||
|
||||
#### 2. Dual-Buffer NVS System
|
||||
|
||||
The ticker now stores "Today" and "Tomorrow" data independently in NVS:
|
||||
|
||||
- **Today buffer (`doc`)**: Contains the current day's price data
|
||||
- **Tomorrow buffer (`docTomorrow`)**: Contains the next day's price data
|
||||
- **NVS keys**: `data_prc`/`data_day`/`data_mon`/`data_year` for today, `data_prc_t`/`data_store_t` for tomorrow
|
||||
|
||||
#### 3. Smart Fetching & API URL
|
||||
|
||||
The logic for fetching tomorrow's data is implemented correctly:
|
||||
|
||||
- **URL Construction**: Adding `&start=YYYY-MM-DD` dynamically after 14:00 (2 PM) queries the Energy-Charts API for the next day
|
||||
- **Validation**: In `processJsonData()`, the code compares the timestamp in the JSON against the target date, preventing the "Tomorrow" buffer from being filled with "Today's" data if the API is lagging
|
||||
|
||||
#### 4. Seamless 48H Scrolling
|
||||
|
||||
If next-day data is available, the button allows scrolling up to **47 hours ahead**:
|
||||
|
||||
- **Visual Distinction**: Using `HH:>>` for tomorrow's hours prevents the user from confusing a cheap price "tomorrow" with a cheap price "today"
|
||||
- **Index Safety**: The code correctly uses `lowestPriceIndexTomorrow` and `highestPriceIndexTomorrow` when the display is in the "tomorrow" range, ensuring the Min/Max icons appear on the correct 15-minute segments
|
||||
|
||||
#### 5. Hardware Stability (Inherited from v6.2.4)
|
||||
|
||||
All v6.2.4 hardware stability fixes are preserved:
|
||||
|
||||
- **Refresh Logic**: "State-Based" refresh ensures the display updates exactly at 00, 15, 30, and 45 minutes past the hour, even if the CPU is busy with a background fetch
|
||||
- **LED Indicators**: White LED for low price and Built-in LED for connectivity remain pinned to the actual current price, even when the user is scrolling through future data on the screen
|
||||
|
||||
#### Final "Sanity Check" Verdict
|
||||
|
||||
**Status:** Verified. The code is safe to deploy. The transition from 15-minute intervals to the midnight rollover is now seamless. The "1 AM fetch gap" that plagues most electricity tickers has been successfully bypassed.
|
||||
|
||||
---
|
||||
|
||||
## Highlights of v6.2.4
|
||||
|
||||
- **BUG FIX:** Exact-boundary display refresh bug
|
||||
- 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.
|
||||
- Fix: Simplified findCurrentPriceIndex() to use a robust "last entry <= now" comparison. This ensures the display transitions to the new hour instantaneously at XX:00:00.
|
||||
|
||||
## Highlights of v6.2.3
|
||||
|
||||
- **BUG FIX**: State-based display refresh logic
|
||||
- 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.
|
||||
- Fix: 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.
|
||||
|
||||
## Highlights of v6.2.2
|
||||
|
||||
## Highlights of v6.2.2
|
||||
|
||||
- **BUG FIX**: Display blank lines issue
|
||||
- Problem: Sometimes rows 0 and 1 (current 15-min prices and current hour) were blank
|
||||
|
||||
Reference in New Issue
Block a user