mirror of
https://github.com/Legolas-2025/Standalone-electricity-price-ticker.git
synced 2026-08-17 12:34:55 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b1fb6ac3d | ||
|
|
4fb5a6bc5f | ||
|
|
20bff8b00a | ||
|
|
0933a06973 | ||
|
|
48819161af | ||
|
|
bbef4f2bc7 | ||
|
|
a99caded8e | ||
|
|
bc41d9bb96 | ||
|
|
b38b4ce7a5 | ||
|
|
5ff76a57f9 | ||
|
|
4ac7449163 | ||
|
|
d571e69942 | ||
|
|
1a0fd42161 | ||
|
|
a7d479546d | ||
|
|
0f6fadd1f8 | ||
|
|
28f7be344f | ||
|
|
5c870f240c | ||
|
|
707ebcd99c | ||
|
|
4e9b9495cd | ||
|
|
1b17a7b80c | ||
|
|
91dea24da8 | ||
|
|
52c3727d39 | ||
|
|
824170a6d1 | ||
|
|
dfb11004ba | ||
|
|
739b21609e | ||
|
|
5ef53916ea | ||
|
|
b55a7b4945 | ||
|
|
d7e27b4f56 | ||
|
|
614be25a1f | ||
|
|
1e5fa661f0 | ||
|
|
13da0e0c87 | ||
|
|
8543493ac0 | ||
|
|
eb5ef5954b | ||
|
|
2fff8296c5 | ||
|
|
3f09e475fc | ||
|
|
eb34d07ccf |
+663
-137
@@ -2,6 +2,650 @@
|
||||
|
||||
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:
|
||||
- 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:
|
||||
- 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 fix (2026-04-01):
|
||||
|
||||
**Summary**
|
||||
|
||||
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
|
||||
- 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).
|
||||
- This ensures the screen updates immediately even if the device was busy during the transition.
|
||||
|
||||
---
|
||||
|
||||
## v6.2.2 - Display blank lines issue fix (2026-03-31):
|
||||
|
||||
**Summary**
|
||||
|
||||
Fixed a bug where the display was showing blank lines
|
||||
|
||||
### 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
|
||||
|
||||
### Solution:
|
||||
|
||||
- Row 1 (current hour) now ALWAYS shows - suppression logic only applies to rows 2-3
|
||||
- Row 0 (15-min details) also always shows for the current hour
|
||||
|
||||
---
|
||||
|
||||
## v6.2.1 – Current Interval Fix (2026‑03‑29)
|
||||
|
||||
**Summary**
|
||||
|
||||
Fixed a bug where the display was showing prices one hour ahead of the current time.
|
||||
|
||||
### Problem: Display Showing Next Hour Instead of Current
|
||||
|
||||
**Root Cause:**
|
||||
|
||||
The `findCurrentPriceIndex()` function was finding the **next** 15-minute interval (first entry with timestamp >= now), but it should find the **current** interval (the one we're currently IN).
|
||||
|
||||
For example, at 17:57:
|
||||
- The current 15-minute interval is **17:45-18:00** (price indexed at 17:45)
|
||||
- The **next** interval is 18:00-18:15 (price indexed at 18:00)
|
||||
- The buggy function returned the index for **18:00** instead of **17:45**
|
||||
- Result: Display showed hour **18** instead of hour **17**
|
||||
|
||||
### Solution
|
||||
|
||||
The fix calculates the **next 15-minute boundary** and finds the last entry **strictly before** that boundary:
|
||||
|
||||
```cpp
|
||||
// Calculate the next 15-minute boundary
|
||||
const int QUARTER_SECONDS = 15 * 60; // 900 seconds
|
||||
time_t nextQuarter = ((now + QUARTER_SECONDS - 1) / QUARTER_SECONDS) * QUARTER_SECONDS;
|
||||
|
||||
// Find the last entry strictly before nextQuarter
|
||||
for (size_t i = unixSeconds.size(); i > 0; i--) {
|
||||
if ((time_t)unixTime < nextQuarter) {
|
||||
return (int)(i - 1);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**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
|
||||
|
||||
---
|
||||
|
||||
## v6.2.0 – DST (Daylight Saving Time) Handling Fixed (2026‑03‑29)
|
||||
|
||||
**Summary**
|
||||
|
||||
This release fixes a critical bug that caused incorrect price display on DST switch days. On March 29, 2026 (the spring forward day), the ticker at 12:41 showed prices for hours 13:00, 14:00, and 15:00 instead of the correct 12:00, 13:00, and 14:00.
|
||||
|
||||
### Problem: Arithmetic-Based Index Calculation
|
||||
|
||||
**Root Cause (v6.0.0 – v6.1.2):**
|
||||
|
||||
The code assumed every day has exactly 96 price entries:
|
||||
|
||||
```cpp
|
||||
int startIndex = hourIndex * 4; // e.g., hour 12 → index 48
|
||||
```
|
||||
|
||||
This assumption breaks on DST switch days:
|
||||
|
||||
| Day Type | Hours | Price Entries | Example |
|
||||
|----------|-------|---------------|---------|
|
||||
| Normal day | 24 | 96 | Array indices 0-95 |
|
||||
| Spring forward (March) | 23 | 92 | Index 48 points to wrong time |
|
||||
| Fall back (October) | 25 | 100 | Index 48 points to wrong time |
|
||||
|
||||
At 12:41 on March 29, 2026:
|
||||
- ESP32 correctly reported `timeinfo.tm_hour = 12`
|
||||
- Old code calculated `12 × 4 = 48`
|
||||
- But array only had 92 entries (no index 48 that maps to local hour 12)
|
||||
- Result: Displayed prices for 13:00, 14:00, 15:00 instead of 12:00, 13:00, 14:00
|
||||
|
||||
### Solution: Timestamp-Based Lookups
|
||||
|
||||
**New approach (v6.2.0):**
|
||||
|
||||
All price lookups now search the `unix_seconds` array using actual timestamps:
|
||||
|
||||
```cpp
|
||||
// Find index by searching for matching hour in timestamps
|
||||
int findPriceIndexForHour(const JsonArray& unixSeconds, int targetHour) {
|
||||
for (size_t i = 0; i < unixSeconds.size(); i++) {
|
||||
unsigned long unixTime = unixSeconds[i].as<unsigned long>();
|
||||
time_t t = (time_t)unixTime;
|
||||
struct tm* ptm = localtime(&t);
|
||||
if (ptm != NULL && ptm->tm_hour == targetHour) {
|
||||
return (int)i; // Found correct index for this hour
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
```
|
||||
|
||||
**New functions added:**
|
||||
- `findPriceIndexForHour()` – Finds first price index for a given hour
|
||||
- `findCurrentPriceIndex()` – Finds current 15-minute slot using Unix timestamp
|
||||
- `getHourFromPriceIndex()` – Gets hour from price array index
|
||||
|
||||
**Updated functions:**
|
||||
- `getHourlyAverage()` – Now uses timestamp lookup instead of `hourIndex * 4`
|
||||
- `display15MinuteDetails()` – Timestamp-based with hour verification in loop
|
||||
- `displayPriceRow()` – Timestamp-based data index lookup
|
||||
- `displayPrimaryList()` – Timestamp-based current hour detection
|
||||
- `updateLeds()` – Timestamp-based current interval lookup
|
||||
|
||||
### 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 |
|
||||
| EU cancels DST | Only update `TZ_CET_CEST` string; code works unchanged |
|
||||
|
||||
If EU parliament ever cancels DST switching, you only need to update the `TZ_CET_CEST` line (one line of code). The price lookup logic requires no changes.
|
||||
|
||||
---
|
||||
|
||||
## v6.1.2 – LED Indicator Restored (ESP32 PWM Fix) (2026‑03‑11)
|
||||
@@ -15,11 +659,11 @@ This release fixes a regression introduced in **v6.1.1** where the **white LED p
|
||||
**Problem (v6.1.1):**
|
||||
|
||||
- The sketch uses `analogWrite()` to drive the LED with PWM for breathe/blink patterns.
|
||||
- However, in some “LED OFF” branches the code used `digitalWrite(LOW)` on the same `whiteLedPin`.
|
||||
- However, in some "LED OFF" branches the code used `digitalWrite(LOW)` on the same `whiteLedPin`.
|
||||
- On ESP32 (LEDC), once PWM is attached to a pin, `digitalWrite(LOW)` may **not fully disable** PWM output.
|
||||
- Result:
|
||||
- LED could remain **faintly on** (dim glow) when LEDs were supposed to be off (e.g., presence timeout / backlight off).
|
||||
- Some patterns could appear “stuck” or inconsistent.
|
||||
- LED could remain **faintly on** (dim glow) when LEDs were supposed to be off.
|
||||
- Some patterns could appear "stuck" or inconsistent.
|
||||
|
||||
**Solution (v6.1.2):**
|
||||
|
||||
@@ -27,7 +671,7 @@ This release fixes a regression introduced in **v6.1.1** where the **white LED p
|
||||
- Use `analogWrite(whiteLedPin, 0)` instead of `digitalWrite(whiteLedPin, LOW)`.
|
||||
- Use `analogWrite(whiteLedPin, 255)` instead of `digitalWrite(whiteLedPin, HIGH)`.
|
||||
- Blink/double‑blink toggles now switch between PWM **0** and **255**.
|
||||
- This ensures the LED is **truly off** whenever LED output is gated off (no data, no time sync, or presence timeout).
|
||||
- This ensures the LED is **truly off** whenever LED output is gated off.
|
||||
|
||||
---
|
||||
|
||||
@@ -35,31 +679,25 @@ This release fixes a regression introduced in **v6.1.1** where the **white LED p
|
||||
|
||||
**Summary**
|
||||
|
||||
This release fixes a bug where the **daily lowest / highest hourly price marker** ignored negative prices (and also ignored 0.0), which could cause the ticker to incorrectly mark the **lowest positive[...]
|
||||
This release fixes a bug where the **daily lowest / highest hourly price marker** ignored negative prices (and also ignored 0.0), which could cause the ticker to incorrectly mark the **lowest positive** price as the daily minimum.
|
||||
|
||||
### Fixed: Daily Low/High Marker Ignored Negative & Zero Prices
|
||||
|
||||
**Problem (v6.1.0):**
|
||||
|
||||
- In `processJsonData()` the daily min/max scan used:
|
||||
|
||||
- `if (hourlyAvg > 0) { ... }`
|
||||
|
||||
```cpp
|
||||
if (hourlyAvg > 0) { ... }
|
||||
```
|
||||
- This had two side effects:
|
||||
1. **Negative** hourly averages were completely skipped.
|
||||
2. A true price of **0.0** was also skipped (even though 0 can be a valid market price).
|
||||
|
||||
- Additionally, the daily average was computed as `sum / 24.0` even though hours were being skipped from `sum`, making the daily average incorrect whenever any hour was excluded.
|
||||
|
||||
**Solution (v6.1.1):**
|
||||
|
||||
- The min/max and average scan now:
|
||||
- Treats an hour as valid based on **data availability** (having all 4×15‑minute entries), not based on value sign.
|
||||
- Includes **all values** (negative, zero, positive) when computing:
|
||||
- `lowestPriceIndex`
|
||||
- `highestPriceIndex`
|
||||
- `averagePrice`
|
||||
- Computes `averagePrice` using the number of valid hours (normally 24).
|
||||
- Includes **all values** (negative, zero, positive).
|
||||
|
||||
---
|
||||
|
||||
@@ -67,112 +705,21 @@ This release fixes a bug where the **daily lowest / highest hourly price marker*
|
||||
|
||||
**Summary**
|
||||
|
||||
This release fixes a bug where the ticker could remain indefinitely on the **“No data for today”** screen after midnight, even though the API was already returning fresh data. It also refines the [...]
|
||||
This release fixes a bug where the ticker could remain indefinitely on the **"No data for today"** screen after midnight, even though the API was already returning fresh data.
|
||||
|
||||
### Fixed: Stuck on NO_DATA_OFFSET After Midnight
|
||||
|
||||
**Problem (v6.0.0):**
|
||||
|
||||
- After midnight, the device:
|
||||
- Detected day rollover and entered a “no data” state.
|
||||
- Scheduled an immediate API fetch.
|
||||
- However:
|
||||
- The API can continue to serve **yesterday’s market day** for some time after local midnight.
|
||||
- The code only checked the **first** `unix_seconds` entry against the current local day.
|
||||
- HTTP + JSON success always incremented `apiSuccessCount`, even if `processJsonData()` subsequently decided the dataset was “not for today”.
|
||||
- The scheduler treated such fetches as **successful**, pushed `nextScheduledFetchTime` 24 hours into the future, and never retried.
|
||||
- Result: the ticker stayed on **NO_DATA_OFFSET** forever, until:
|
||||
- A manual long‑press triggered a fresh fetch at a time when the API finally returned recognized “today” data, or
|
||||
- The device was rebooted later in the day.
|
||||
- The API can continue to serve **yesterday's market day** for some time after local midnight.
|
||||
- HTTP + JSON success always incremented `apiSuccessCount`, even if the data was "not for today".
|
||||
- The scheduler treated such fetches as **successful** and never retried.
|
||||
|
||||
**Solution (v6.1.0):**
|
||||
|
||||
1. **Market Day Detection**:
|
||||
- `processJsonData()` now determines the “market day” using the **last** `unix_seconds` timestamp from the API’s dataset (assumed to cover one full day in 15‑minute steps).
|
||||
- It compares that calendar date (local time) against the current local date.
|
||||
- If they differ:
|
||||
- The dataset is treated as **“not for today”**.
|
||||
- `isTodayDataAvailable = false`.
|
||||
- The function returns **false**, and a new flag `lastProcessJsonAcceptedToday` remains `false`.
|
||||
|
||||
2. **Logical Failure vs HTTP/JSON Failure**:
|
||||
- New global flag:
|
||||
- `lastProcessJsonAcceptedToday` – `true` only when `processJsonData()` accepts the dataset as “today’s” data.
|
||||
- In `handleDataFetching()`:
|
||||
- A scheduled fetch is considered a **real success** only if:
|
||||
- HTTP + JSON succeeded, **and**
|
||||
- `lastProcessJsonAcceptedToday == true`.
|
||||
- In all other cases (including “HTTP 200 + parse OK but data still for yesterday”):
|
||||
- The fetch is treated as **failure** for scheduling purposes.
|
||||
- If `midnightPhaseActive == true`, `scheduleAfterMidnightFailure()` is invoked to plan a retry.
|
||||
|
||||
3. **Midnight Phase Cleanup**:
|
||||
- When a fetch finally provides a dataset for today:
|
||||
- `isTodayDataAvailable = true`.
|
||||
- `lastProcessJsonAcceptedToday = true`.
|
||||
- `midnightPhaseActive` is cleared; `midnightRetryCount` reset to 0.
|
||||
- `nextScheduledFetchTime` is set to 24 hours ahead.
|
||||
|
||||
As a result, the ticker will **keep retrying** after midnight until a correct market‑day dataset appears, instead of giving up after the first HTTP 200.
|
||||
|
||||
---
|
||||
|
||||
### Changed: After‑Midnight Retry Schedule
|
||||
|
||||
In `scheduleAfterMidnightFailure()` the retry strategy when `midnightPhaseActive == true` has been tuned.
|
||||
|
||||
**Old behavior (v6.0.0, conceptual):**
|
||||
|
||||
- First few failures after midnight:
|
||||
- Retried every 10 minutes up to 5 attempts.
|
||||
- Afterwards:
|
||||
- Switched to hourly retries (top of each hour).
|
||||
|
||||
**New behavior (v6.1.0 + user configuration):**
|
||||
|
||||
- Fast retry phase fully contained within the **first hour after midnight**.
|
||||
- You configured:
|
||||
|
||||
```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 – initial attempt (triggered by day rollover).
|
||||
- If dataset is still for previous day:
|
||||
- 1st retry at ~00:20.
|
||||
- 2nd retry at ~00:40.
|
||||
- After that:
|
||||
- Retries only at the **top of the next hours** (01:00, 02:00, …),
|
||||
until a dataset with market day = today is accepted.
|
||||
|
||||
This configuration significantly reduces overnight API load while still ensuring the ticker picks up the new day as soon as the API publishes it.
|
||||
|
||||
---
|
||||
|
||||
### Other Behavior (Retained From v6.0.0)
|
||||
|
||||
- **NVS caching** of daily data:
|
||||
- On boot, if NVS contains a dataset whose date matches today’s local date:
|
||||
- The JSON is deserialized and processed.
|
||||
- A new API call is **skipped**.
|
||||
- After each accepted “today” fetch:
|
||||
- Raw JSON payload, date, and a timestamp are stored in NVS.
|
||||
- **Manual long‑press refresh**:
|
||||
- Still triggers immediate fetch via `nextScheduledFetchTime = now;`.
|
||||
- Now also respects the improved “today” detection; “yesterday’s” data is not accepted as today.
|
||||
- **CET/CEST time handling**:
|
||||
- Unchanged, still uses `TZ_CET_CEST` with `configTzTime`.
|
||||
- **Secondary menu and NVS status lines**:
|
||||
- Retained from v6.0.0; updated only for version string and minor wording.
|
||||
- `processJsonData()` now determines the "market day" using the **last** `unix_seconds` timestamp.
|
||||
- A scheduled fetch is only successful if `lastProcessJsonAcceptedToday == true`.
|
||||
- Midnight retry logic keeps trying until valid "today" data is received.
|
||||
|
||||
---
|
||||
|
||||
@@ -184,30 +731,9 @@ First major redesign focused on reducing API traffic and improving resilience us
|
||||
|
||||
### New
|
||||
|
||||
- NVS namespace `"my-ticker"` introduced with keys:
|
||||
- `ssid`, `pass` – Wi‑Fi credentials.
|
||||
- `data_day`, `data_mon`, `data_year` – stored data calendar day.
|
||||
- `data_prc` – raw JSON string from API.
|
||||
- `data_last_store` – Unix time when data was stored.
|
||||
- Boot behavior:
|
||||
- Try to load and validate NVS data.
|
||||
- If date matches today → reuse it and **skip** initial API call.
|
||||
- After‑midnight behavior:
|
||||
- NVS is overwritten with each successful new‑day dataset.
|
||||
- In‑RAM data for yesterday is invalidated at day rollover.
|
||||
|
||||
### UI / Menu
|
||||
|
||||
- Primary list:
|
||||
- 15‑minute detail for current hour.
|
||||
- Hourly averages for current + next 2 hours.
|
||||
- Secondary list:
|
||||
- Expanded to 20 lines to include:
|
||||
- Time/date, last update, daily average.
|
||||
- Wi‑Fi RSSI, IP address.
|
||||
- API success rate, uptime.
|
||||
- NVS status block.
|
||||
- Credits & version line.
|
||||
- NVS namespace `"my-ticker"` with Wi‑Fi credentials and daily price data caching.
|
||||
- Boot behavior: Try to load and validate NVS data; if date matches today → reuse it and **skip** initial API call.
|
||||
- After‑midnight: NVS is overwritten with each successful new‑day dataset.
|
||||
|
||||
---
|
||||
|
||||
|
||||
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
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
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,26 +1,321 @@
|
||||

|
||||
|
||||
# Electricity Price Ticker for XIAO ESP32‑C3 (Energy‑Charts)
|
||||
|
||||
This project is an Arduino‑IDE‑friendly firmware for the **Seeed XIAO ESP32‑C3** that:
|
||||
|
||||
- Connects to Wi‑Fi.
|
||||
- Fetches **day‑ahead electricity prices** from [Energy‑Charts.info](https://energy-charts.info) (Bundesnetzagentur / SMARD.de).
|
||||
- Fetches **day‑ahead electricity prices** from [Energy‑Charts.info](https://energy-charts.info).
|
||||
- Computes final consumer prices (including configurable power‑company fee + VAT).
|
||||
- Displays current and upcoming prices on a **20x4 I²C 2004 LCD**.
|
||||
- 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.1.2**, focusing on:
|
||||
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
|
||||
|
||||
### 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)
|
||||
- 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)
|
||||
- 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:
|
||||
- Row 1 (current hour) now ALWAYS shows - suppression logic only applies to rows 2-3.
|
||||
- Row 0 (15-min details) also always shows for the current hour.
|
||||
|
||||
### v6.2.1 – Current Interval Fix (critical fix of v6.2.0 update)
|
||||
- Fixed display showing prices one hour ahead of the current time.
|
||||
- `findCurrentPriceIndex()` now correctly returns the current 15-minute interval.
|
||||
|
||||
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 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.
|
||||
- Robust **NVS storage** of daily price data.
|
||||
- Correct **CET/CEST** handling.
|
||||
- Resilient **after‑midnight refresh** (no more getting stuck on “No data for today”).
|
||||
- Resilient **after‑midnight refresh** (no more getting stuck on "No data for today").
|
||||
- Preserved UI and button behavior from v5.5.
|
||||
|
||||
---
|
||||
|
||||
## DST (Daylight Saving Time) – How It Works
|
||||
|
||||
### 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.
|
||||
|
||||
The firmware uses **timestamp-based price lookups** that work correctly regardless of whether the day has 23, 24, or 25 hours:
|
||||
|
||||
| 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) |
|
||||
|
||||
### Timezone Configuration
|
||||
|
||||
The firmware uses the `TZ_CET_CEST` timezone string for displaying local time:
|
||||
|
||||
```cpp
|
||||
const char* TZ_CET_CEST = "CET-1CEST,M3.5.0/02:00,M10.5.0/03:00";
|
||||
```
|
||||
|
||||
**Current behavior:**
|
||||
- Spring forward: Last Sunday of March at 02:00 → 03:00 (CEST, UTC+2)
|
||||
- Fall back: Last Sunday of October at 03:00 → 02:00 (CET, UTC+1)
|
||||
|
||||
### Future-Proof: If EU Cancels DST
|
||||
|
||||
If the EU parliament ever cancels DST switching, you only need to update **one line of code**:
|
||||
|
||||
```cpp
|
||||
// Option A - Stay on CET (UTC+1, winter time) permanently:
|
||||
const char* TZ_CET_CEST = "CET-1";
|
||||
|
||||
// Option B - Stay on CEST (UTC+2, summer time) permanently:
|
||||
const char* TZ_CET_CEST = "CEST-2";
|
||||
```
|
||||
|
||||
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:
|
||||
@@ -37,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
|
||||
@@ -91,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
|
||||
@@ -111,16 +406,17 @@ 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:**
|
||||
|
||||
- **LCD backpack → XIAO ESP32‑C3**
|
||||
- `VCC` → **5V** (or 3V3 if your module explicitly supports 3.3V I²C)
|
||||
- `GND` → **GND**
|
||||
- `SDA` → board I²C SDA pin (see XIAO ESP32‑C3 documentation)
|
||||
- `SCL` → board I²C SCL pin
|
||||
| LCD Backpack | XIAO ESP32‑C3 |
|
||||
|-------------|---------------|
|
||||
| VCC | 5V |
|
||||
| GND | GND |
|
||||
| SDA | I²C SDA |
|
||||
| SCL | I²C SCL |
|
||||
|
||||
> Note: On many XIAO ESP32‑C3 board definitions, SDA/SCL are mapped internally. Just use the default I²C pins as documented by Seeed.
|
||||
|
||||
@@ -185,12 +481,14 @@ The presence sensor is used to control LCD backlight and LEDs to save power and
|
||||
|
||||
Recommended module: **RCWL‑0516** microwave motion sensor.
|
||||
|
||||
**Wiring (from the v5.5 header, preserved in v6.x):**
|
||||
**Wiring:**
|
||||
|
||||
- `VCC` → **3.3V**
|
||||
- `GND` → **GND**
|
||||
- `OUT` → `GPIO 9` (`presencePin`)
|
||||
- **Required**: 10 kΩ pull‑down resistor between `GPIO 9` and `GND`.
|
||||
| RCWL‑0516 | Connection |
|
||||
|-----------|------------|
|
||||
| VCC | 3.3V |
|
||||
| GND | GND |
|
||||
| OUT | GPIO 9 (`presencePin`) |
|
||||
| **Required**: 10kΩ pull‑down | Between GPIO 9 and GND |
|
||||
|
||||
Characteristics:
|
||||
|
||||
@@ -225,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.
|
||||
|
||||
---
|
||||
|
||||
@@ -238,45 +536,51 @@ The LED is driven with various patterns to indicate price level; see “LED Pric
|
||||
|
||||
---
|
||||
|
||||
## Firmware Features (v6.1.2)
|
||||
## Firmware Features (v7.2)
|
||||
|
||||
### Core Display & Pricing
|
||||
|
||||
- Data source:
|
||||
- Data source: `https://api.energy-charts.info/price?bzn=SI`
|
||||
- Resolution: 15‑minute intervals with hourly averages
|
||||
- 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
|
||||
- **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:
|
||||
|
||||
```text
|
||||
https://api.energy-charts.info/price?bzn=SI
|
||||
```
|
||||
| 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)` |
|
||||
|
||||
- Resolution:
|
||||
- Prices in **15‑minute intervals** (`price[]`, `unix_seconds[]`).
|
||||
- Display shows:
|
||||
- **Row 0**: Current hour, four 15‑minute values in compact format (`XX XX XX XX`).
|
||||
- **Rows 1–3**: Current hour + next two hours as hourly averages.
|
||||
- Price calculation:
|
||||
- Raw MWh prices are converted to **EUR/kWh**.
|
||||
- Two configurable surcharges:
|
||||
- `POWER_COMPANY_FEE_PERCENTAGE` (default `12.0` %).
|
||||
- `VAT_PERCENTAGE` (default `22.0` %).
|
||||
- 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):
|
||||
|
||||
- Very cheap (`<= 0.05 EUR/kWh`) → smooth breathing.
|
||||
- Cheap / normal → steady on.
|
||||
- Moderately expensive → slow blink.
|
||||
- Expensive → faster blink.
|
||||
- Very expensive → complex “double‑blink with long on” pattern.
|
||||
- Negative price or no data → LED off.
|
||||
| Price Level | LED Behavior |
|
||||
|-------------|--------------|
|
||||
| Negative / no data | LED off |
|
||||
| ≤ 0.05 EUR/kWh | Smooth breathing |
|
||||
| 0.05 – 0.15 | Steady on |
|
||||
| 0.15 – 0.25 | Slow blink |
|
||||
| 0.25 – 0.35 | Fast blink |
|
||||
| 0.35 – 0.50 | Double blink |
|
||||
| > 0.50 | Triple blink pattern |
|
||||
|
||||
**Important implementation note (v6.1.2):**
|
||||
**Important implementation note (from v6.1.2 on):**
|
||||
|
||||
- On ESP32, avoid mixing PWM (`analogWrite`) and `digitalWrite` on the same LED pin.
|
||||
- The firmware now uses `analogWrite(pin, 0/255)` consistently to guarantee the LED is fully off when gated off.
|
||||
@@ -301,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
|
||||
|
||||
@@ -343,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
|
||||
|
||||
@@ -366,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()`:
|
||||
|
||||
@@ -386,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.
|
||||
|
||||
---
|
||||
|
||||
@@ -496,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
|
||||
@@ -514,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`
|
||||
|
||||
---
|
||||
|
||||
@@ -533,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.
|
||||
@@ -551,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.1 `.ino` file (e.g. `20260130_electricity_ticker_6_1_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.
|
||||
@@ -561,29 +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`).
|
||||
|
||||
@@ -591,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.
|
||||
|
||||
+219
-42
@@ -2,54 +2,231 @@
|
||||
|
||||
## Current firmware
|
||||
|
||||
- **Version:** 6.1.2
|
||||
- **Release date:** 2026-03-11
|
||||
- **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
|
||||
- **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 v7.2
|
||||
|
||||
### Button Robustness & Screen-Control Fixes
|
||||
|
||||
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
|
||||
|
||||
- **BUG FIX**: Display blank lines issue
|
||||
- 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:
|
||||
- Row 1 (current hour) now ALWAYS shows - suppression logic only applies to rows 2-3
|
||||
- Row 0 (15-min details) also always shows for the current hour
|
||||
|
||||
## Highlights of v6.2.1
|
||||
|
||||
- **BUG FIX**: Fixed `findCurrentPriceIndex()` to return the correct current interval.
|
||||
- Problem: At 17:57, it returned index for 18:00 instead of 17:45, causing display to show hour 18 instead of hour 17.
|
||||
- Fix: Now calculates next 15-minute boundary and finds the last entry before that boundary.
|
||||
|
||||
## Highlights of v6.2.0
|
||||
|
||||
- **CRITICAL FIX**: DST (Daylight Saving Time) handling is now fully fixed for all days.
|
||||
- Previously, the code assumed every day has exactly 96 price entries (24h × 4). This caused incorrect price display on DST switch days:
|
||||
- Spring forward (March): Only 92 entries → wrong prices displayed
|
||||
- Fall back (October): 100 entries → wrong prices displayed
|
||||
- **Solution**: All price lookups now use timestamp-based searching through the `unix_seconds` array instead of arithmetic calculation (`hourIndex * 4`).
|
||||
- New functions: `findPriceIndexForHour()`, `findCurrentPriceIndex()`, `getHourFromPriceIndex()`
|
||||
- Updated functions: `getHourlyAverage()`, `display15MinuteDetails()`, `displayPriceRow()`, `displayPrimaryList()`, `updateLeds()`
|
||||
- The ticker now works correctly on all days, including DST switch days, with no manual intervention.
|
||||
- **Future-proof**: If EU cancels DST, only the `TZ_CET_CEST` string needs updating (one line of code).
|
||||
|
||||
## Previous firmware
|
||||
|
||||
- **Version:** 6.1.2
|
||||
- **Release date:** 2026-03-11
|
||||
- **Target MCU:** Seeed XIAO ESP32‑C3
|
||||
|
||||
## Highlights of v6.1.2
|
||||
|
||||
- Fix: restore proper white LED price indicator behavior on ESP32 by avoiding mixing PWM (`analogWrite`) and `digitalWrite` on the same pin.
|
||||
- Fix: LED is now truly off when backlight/LED gating turns it off (no more “dim glow”).
|
||||
- Change: `updateLeds()` uses PWM only (`analogWrite(pin, 0/255)`) for off/on and blink toggles.
|
||||
- Fix: LED is now truly off when backlight/LED gating turns it off (no more "dim glow").
|
||||
|
||||
## Earlier firmware
|
||||
|
||||
- **Version:** 6.1.1 (2026-03-07) – Daily low/high marker includes negative and zero prices
|
||||
- **Version:** 6.1.0 (2026-01-30) – Midnight fetch and market day detection fixes
|
||||
- **Version:** 6.0.0 (2026-01-27) – NVS storage and daily fetch
|
||||
|
||||
For full details, see:
|
||||
|
||||
- [CHANGELOG.md](./CHANGELOG.md)
|
||||
- [README.md](./README.md)
|
||||
|
||||
## Previous firmware
|
||||
|
||||
- **Version:** 6.1.1
|
||||
- **Release date:** 2026-03-07
|
||||
- **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.1.1
|
||||
|
||||
- Fix: daily **lowest/highest hourly price marker** now includes **negative** and **0.0** prices.
|
||||
- Fix: daily average is computed over the number of valid hours (instead of always dividing by 24 even when hours were skipped).
|
||||
|
||||
## Earlier firmware
|
||||
|
||||
- **Version:** 6.0.0
|
||||
- **Release date:** 2026-01-27
|
||||
- **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.0.0
|
||||
|
||||
- Single **daily fetch** (on boot / after midnight) instead of hourly.
|
||||
- **NVS storage** of daily data for resilience to power outages.
|
||||
- Robust midnight rollover and retry logic:
|
||||
- First immediate fetch.
|
||||
- Up to 5 × 10‑minute retries.
|
||||
- Then top‑of‑hour retries until successful.
|
||||
- Prevents yesterday’s prices from ever being shown as today’s.
|
||||
- Secondary info menu extended with **NVS status** and clear version label.
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 434 KiB |
Reference in New Issue
Block a user