mirror of
https://github.com/Legolas-2025/Standalone-electricity-price-ticker.git
synced 2026-08-18 12:44:54 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a0fd42161 | ||
|
|
a7d479546d | ||
|
|
0f6fadd1f8 | ||
|
|
28f7be344f | ||
|
|
5c870f240c | ||
|
|
707ebcd99c | ||
|
|
4e9b9495cd | ||
|
|
1b17a7b80c | ||
|
|
91dea24da8 | ||
|
|
52c3727d39 | ||
|
|
824170a6d1 | ||
|
|
dfb11004ba | ||
|
|
739b21609e | ||
|
|
5ef53916ea | ||
|
|
b55a7b4945 | ||
|
|
d7e27b4f56 | ||
|
|
614be25a1f | ||
|
|
1e5fa661f0 | ||
|
|
13da0e0c87 | ||
|
|
8543493ac0 | ||
|
|
eb5ef5954b | ||
|
|
2fff8296c5 | ||
|
|
3f09e475fc | ||
|
|
eb34d07ccf | ||
|
|
dcc434f7e4 | ||
|
|
6c7a66be8e | ||
|
|
e1ad1b01c3 | ||
|
|
f77f50f1d4 | ||
|
|
45cabc17dd | ||
|
|
fedef66ae0 | ||
|
|
65fdb14cec | ||
|
|
3da3dec344 | ||
|
|
6a6cf113b3 | ||
|
|
26b1a309e1 | ||
|
|
4c5d7d2ebc | ||
|
|
b54c40cdf7 | ||
|
|
08db5888cb | ||
|
|
8f1f017232 | ||
|
|
189350e4fe | ||
|
|
827623112f | ||
|
|
fd5dad4cb5 | ||
|
|
25fc31fb3b | ||
|
|
aafe962c4c | ||
|
|
a57af528f8 | ||
|
|
ce52d6b9c0 |
File diff suppressed because it is too large
Load Diff
@@ -1160,7 +1160,7 @@ void displaySecondaryList() {
|
||||
snprintf(lines[16], sizeof(lines[16]), "energy-charts.info");
|
||||
snprintf(lines[17], sizeof(lines[17]), "dynamic electricity");
|
||||
snprintf(lines[18], sizeof(lines[18]), "price ticker v6.0 ");
|
||||
snprintf(lines[19], sizeof(lines[19]), "by Amir Toki^ 2025");
|
||||
snprintf(lines[19], sizeof(lines[19]), "by Legolas-2025");
|
||||
|
||||
// Render current window
|
||||
for (int i = 0; i < 4; i++) {
|
||||
|
||||
+247
-127
@@ -1,147 +1,267 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
All notable changes to this project are documented here.
|
||||
|
||||
This project follows a simple semantic versioning style:
|
||||
`MAJOR.MINOR.PATCH`
|
||||
## 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.
|
||||
|
||||
---
|
||||
|
||||
## [6.0.0] - 2026-01-27
|
||||
## v6.2.3 - State-based display refresh logic fix (2026-04-01):
|
||||
|
||||
### Added
|
||||
- **Daily fetch + NVS storage (ESP32‑C3 NVS)**
|
||||
- Introduced non‑volatile storage for daily price data using the existing `Preferences` (NVS) subsystem under the `"my-ticker"` namespace.
|
||||
- Stored fields:
|
||||
- `data_day`, `data_mon`, `data_year` (calendar date of the data)
|
||||
- `data_prc` (raw JSON returned from the Energy‑Charts API)
|
||||
- `data_last_store` (UNIX timestamp of last successful store)
|
||||
- On boot, after Wi‑Fi and NTP time sync:
|
||||
- The ticker checks NVS for stored data.
|
||||
- If the stored date matches **today**, the JSON is deserialized and used directly (no initial API call needed).
|
||||
- If the stored date is **not** today (or invalid), the data is ignored and the ticker behaves as if no data is available yet.
|
||||
**Summary**
|
||||
|
||||
- **Midnight rollover + daily fetch logic**
|
||||
- The system now performs **one daily fetch per day**, instead of hourly:
|
||||
- **On boot**: fetch only if NVS does not already contain valid data for today.
|
||||
- **After local midnight**: invalidate the previous day’s data and trigger a new fetch for the new day.
|
||||
- As soon as a local‑time day change is detected:
|
||||
- `isTodayDataAvailable` is set to `false`.
|
||||
- The display state is forced to `"No data for today"` (`NO_DATA_OFFSET`).
|
||||
- The white LED is turned off (same behavior as “no data”).
|
||||
- A midnight fetch phase is activated.
|
||||
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.
|
||||
|
||||
- **Midnight fetch retry strategy**
|
||||
- When midnight is detected:
|
||||
- First fetch is scheduled **immediately**.
|
||||
- If it fails (HTTP error or JSON/“day mismatch”), the system stays in `"No data for today"` and the LEDs remain off.
|
||||
- Retry policy during midnight phase:
|
||||
1. Retry every **10 minutes**, up to **5 attempts**.
|
||||
2. If still unsuccessful, retry only once **at the top of each following hour** until fresh data for the new day is retrieved.
|
||||
- As soon as a successful fetch for today is obtained:
|
||||
- `isTodayDataAvailable = true`.
|
||||
- The enforced `NO_DATA_OFFSET` state is cleared back to `CURRENT_PRICES`.
|
||||
- The white LED resumes indicating the current 15‑minute segment price.
|
||||
- The entire successful JSON payload and metadata are saved back into NVS.
|
||||
### 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.
|
||||
|
||||
- **Improved resilience after power loss**
|
||||
- If the device reboots during the same day:
|
||||
- It can **restore and reuse** the last successfully stored daily data from NVS.
|
||||
- This avoids unnecessary API calls and gives a fast “warm start” after power outages.
|
||||
- If the device reboots on the **next** day:
|
||||
- Yesterday’s stored data is **not** used for display (to avoid confusion).
|
||||
- The display starts in `"No data for today"` until the first successful fetch.
|
||||
|
||||
- **Extended secondary (info) menu**
|
||||
- The secondary info screen (reachable via double‑click on the button) is extended from 16 to **20 lines**, still shown as 4‑line pages.
|
||||
- Existing info retained:
|
||||
- Current time and date.
|
||||
- Last successful update timestamp.
|
||||
- Daily average price.
|
||||
- Wi‑Fi RSSI and device IP.
|
||||
- API success ratio.
|
||||
- Uptime.
|
||||
- Credits and version information.
|
||||
- **New NVS status section** (first defined around lines 12–15, later rearranged in your version):
|
||||
- Shows:
|
||||
- NVS data status (`NVS status:`)
|
||||
- Stored data date (`Data day: DD.MM.YYYY` or `Data day: none`)
|
||||
- Last store timestamp (`Last save: DD.MM.YY` or `Last save: none`)
|
||||
- Basic quick status:
|
||||
- `NVS: OK (today)` – valid data for today loaded from NVS
|
||||
- `NVS: old data` – NVS has data, but not from today (ignored for display)
|
||||
- `NVS: empty` – no stored price data present
|
||||
|
||||
### Changed
|
||||
- **API call strategy**
|
||||
- Removed regular **top‑of‑hour** automatic fetching.
|
||||
- API calls now occur only:
|
||||
- Once at boot (if NVS has no valid data for today).
|
||||
- After midnight (with the retry strategy described above).
|
||||
- On user‑initiated **long‑press** (manual refresh).
|
||||
- This significantly reduces network load while keeping behavior safe and predictable.
|
||||
|
||||
- **UI & behavior around day boundaries**
|
||||
- At midnight / day change:
|
||||
- Display is immediately set to:
|
||||
- Line 0: `No data for today`
|
||||
- Line 1: `Press & hold to`
|
||||
- Line 2: `refresh manually`
|
||||
- White LED is turned off (no price indication until new data arrives).
|
||||
- Once data for the new day is available:
|
||||
- The ticker returns to the usual 15‑minute detail + hourly display mode.
|
||||
|
||||
- **Versioning and info screens**
|
||||
- Version bumped to **v6.0** and reflected in:
|
||||
- Source file header comment.
|
||||
- Secondary menu text (`price ticker v6.0`).
|
||||
- New version documentation (`VERSION.md` / `CHANGELOG.md` / `README.md`).
|
||||
|
||||
- **Long‑press threshold (in repository version)**
|
||||
- Long‑press detection threshold extended from 2s to **3s** (in your repository copy) to avoid accidental manual refreshes.
|
||||
|
||||
### Fixed / Ensured
|
||||
- Yesterday’s data is **never shown** as if it were today’s:
|
||||
- On boot: previous‑day NVS data is ignored for display.
|
||||
- After midnight: in‑RAM data is invalidated and the UI explicitly shows `"No data for today"` until fresh data is fetched.
|
||||
- LED state is always consistent with the availability of **today’s** data:
|
||||
- LED off → no today data (or negative price).
|
||||
- LED patterns → valid today data and a positive price in the current 15‑minute interval.
|
||||
### 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.
|
||||
|
||||
---
|
||||
|
||||
## [5.5.0] - 2025-10-27
|
||||
## v6.2.2 - Display blank lines issue fix (2026-03-31):
|
||||
|
||||
> First 15‑minute detail version and DST‑fixed base, which v6.0 builds upon.
|
||||
**Summary**
|
||||
|
||||
### Added
|
||||
- **15‑minute detail mode**:
|
||||
- Primary display shows:
|
||||
- Current hour average.
|
||||
- Four 15‑minute prices in compact format (`XX XX XX XX`).
|
||||
- Next 2 hours’ averages.
|
||||
- LED behavior switched from hourly‑based to **15‑minute‑segment based**.
|
||||
- **Compact price format**:
|
||||
- 15‑minute values shown as 2‑digit hundredths (e.g. `+99 -07 24 11`).
|
||||
- **DST / timezone handling**:
|
||||
- `configTzTime()` with `TZ_CET_CEST` for automatic CET ↔ CEST switching.
|
||||
- **Aggressive retry and state enforcement**:
|
||||
- Improved logic for:
|
||||
- Handling stale data.
|
||||
- Enforcing `"No data for today"` state when needed.
|
||||
- Aggressively retrying fetches after HTTP/JSON failures.
|
||||
- **UI and usability tweaks**:
|
||||
- Button:
|
||||
- Single click: scroll primary list (hourly view).
|
||||
- Double click: switch primary/secondary list.
|
||||
- Long press: manual refresh.
|
||||
- Secondary info list:
|
||||
- Date/time, last update, daily average, Wi‑Fi and API stats, uptime, credits.
|
||||
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
|
||||
|
||||
---
|
||||
|
||||
## Older versions
|
||||
## v6.2.1 – Current Interval Fix (2026‑03‑29)
|
||||
|
||||
Earlier versions (≤5.4) introduced the basic ticker behavior, LCD layout, Energy‑Charts API integration, and the initial presence sensor / backlight / LED logic.
|
||||
**Summary**
|
||||
|
||||
Those versions are not fully documented here, but key user‑visible behavior is maintained in v6.0 unless explicitly noted in this changelog.
|
||||
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)
|
||||
|
||||
**Summary**
|
||||
|
||||
This release fixes a regression introduced in **v6.1.1** where the **white LED price indicator** could remain **dimly lit** even when the LCD backlight turned off, and the intended **blink/breathe patterns** no longer behaved correctly.
|
||||
|
||||
### Fixed: White LED Stuck Dim / Patterns Broken (PWM vs Digital)
|
||||
|
||||
**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`.
|
||||
- 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.
|
||||
- Some patterns could appear "stuck" or inconsistent.
|
||||
|
||||
**Solution (v6.1.2):**
|
||||
|
||||
- LED control is now **PWM‑only** inside `updateLeds()`:
|
||||
- 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.
|
||||
|
||||
---
|
||||
|
||||
## v6.1.1 – Daily Min/Max Includes Negative & Zero Prices (2026‑03‑07)
|
||||
|
||||
**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** 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:
|
||||
```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).
|
||||
|
||||
**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).
|
||||
|
||||
---
|
||||
|
||||
## v6.1.0 – Midnight Fetch & Market Day Fix (2026‑01‑30)
|
||||
|
||||
**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.
|
||||
|
||||
### Fixed: Stuck on NO_DATA_OFFSET After Midnight
|
||||
|
||||
**Problem (v6.0.0):**
|
||||
|
||||
- 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):**
|
||||
|
||||
- `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.
|
||||
|
||||
---
|
||||
|
||||
## v6.0.0 – NVS Storage & Daily Fetch (2026‑01‑27)
|
||||
|
||||
**Summary**
|
||||
|
||||
First major redesign focused on reducing API traffic and improving resilience using non‑volatile storage.
|
||||
|
||||
### New
|
||||
|
||||
- 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.
|
||||
|
||||
---
|
||||
|
||||
## v5.x – Earlier Versions
|
||||
|
||||
Earlier versions (v5.x and below) had:
|
||||
|
||||
- No NVS‑based caching of daily API data.
|
||||
- More frequent API calls (e.g., hourly refresh pattern).
|
||||
- Less robust handling of DST and daily boundaries.
|
||||
|
||||
For exact details, see older `.ino` files and their header comments in this repository.
|
||||
|
||||
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
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,92 @@
|
||||
# Hardware Wiring Diagram (Seeed XIAO ESP32‑C3 Electricity Price Ticker)
|
||||
|
||||
This document depicts the wiring for the **Electricity Price Ticker** project based on the connection instructions in `README.md`.
|
||||
|
||||
It includes:
|
||||
- Required peripherals (LCD + button)
|
||||
- Supported optional peripherals (presence sensor + white LED + optional TTP223 touch button alternative)
|
||||
|
||||
> Further reference (official board documentation):
|
||||
> - https://wiki.seeedstudio.com/XIAO_ESP32C3_Getting_Started/
|
||||
|
||||
> Notes:
|
||||
> - Always connect **all grounds together** (ESP32 GND, LCD GND, sensor GND, LED GND).
|
||||
> - Verify your **XIAO ESP32‑C3 pinout** in the Seeed documentation link above.
|
||||
> - **Important:** The **10 kΩ pull-down resistor on D9 is mandatory at all times** (even if the presence sensor is not connected).
|
||||
> - Button input is on **D2**. You may use either a **mechanical pushbutton** (default) *or* a **TTP223 touch module** (alternative), but not both in parallel unless you know what you’re doing.
|
||||
|
||||
---
|
||||
|
||||
## 1) Wiring overview (diagram)
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
MCU["Seeed XIAO ESP32-C3"]:::mcu
|
||||
|
||||
LCD["20x4 I2C LCD 2004<br/>PCF8574 backpack<br/>I2C addr 0x27"]:::lcd
|
||||
|
||||
%% Button input options (XIAO D2)
|
||||
D2NODE["D2 node<br/>(buttonPin input)"]:::io
|
||||
BTN["Mechanical pushbutton<br/>default option<br/>active LOW to GND"]:::btn
|
||||
TTP["TTP223 capacitive touch<br/>optional alternative<br/>OUT is HIGH when touched"]:::touch
|
||||
|
||||
%% Presence-sensor input stage (XIAO D9) - always present
|
||||
D9NODE["D9 node<br/>(presencePin input)"]:::io
|
||||
RPD["10k pull-down resistor<br/>D9 to GND<br/>MANDATORY"]:::res
|
||||
PRES["RCWL-0516 presence sensor<br/>optional"]:::pres
|
||||
|
||||
%% White LED output (XIAO D3)
|
||||
LED1["White indicator LED<br/>optional"]:::led
|
||||
|
||||
PSU5V["5V supply<br/>USB-C or regulated 5V"]:::pwr
|
||||
V33["3.3V rail from XIAO"]:::pwr
|
||||
GND["Common GND"]:::gnd
|
||||
|
||||
%% Power
|
||||
PSU5V -->|"5V"| MCU
|
||||
PSU5V -->|"5V or 3V3 if LCD supports"| LCD
|
||||
MCU -->|"3V3"| V33
|
||||
V33 -->|"3V3"| PRES
|
||||
V33 -->|"3V3"| TTP
|
||||
|
||||
%% Grounds
|
||||
MCU --- GND
|
||||
LCD --- GND
|
||||
BTN --- GND
|
||||
TTP --- GND
|
||||
D2NODE --- GND
|
||||
|
||||
D9NODE --- GND
|
||||
PRES --- GND
|
||||
|
||||
LED1 --- GND
|
||||
PSU5V --- GND
|
||||
|
||||
%% I2C
|
||||
MCU -->|"SDA D4"| LCD
|
||||
MCU -->|"SCL D5"| LCD
|
||||
|
||||
%% Button input stage (always)
|
||||
MCU -->|"D2 (buttonPin)"| D2NODE
|
||||
BTN -->|"button to GND"| D2NODE
|
||||
TTP -->|"OUT to D2 node"| D2NODE
|
||||
|
||||
%% Presence input stage (always)
|
||||
MCU -->|"D9 (presencePin)"| D9NODE
|
||||
D9NODE ---|"10k"| RPD
|
||||
RPD -->|"to GND"| GND
|
||||
PRES -->|"OUT to D9 node"| D9NODE
|
||||
|
||||
%% White LED (single LED, directly from pin with series resistor)
|
||||
MCU -->|"D3 (whiteLedPin)"| LED1
|
||||
|
||||
classDef mcu fill:#e8f0ff,stroke:#2b5fd9,stroke-width:1px,color:#000;
|
||||
classDef lcd fill:#fff4e5,stroke:#cc7a00,stroke-width:1px,color:#000;
|
||||
classDef btn fill:#eaffea,stroke:#2d8a2d,stroke-width:1px,color:#000;
|
||||
classDef touch fill:#e6fcff,stroke:#0077b6,stroke-width:1px,color:#000;
|
||||
classDef pres fill:#f3e8ff,stroke:#7b2cbf,stroke-width:1px,color:#000;
|
||||
classDef led fill:#ffe8ef,stroke:#c9184a,stroke-width:1px,color:#000;
|
||||
classDef res fill:#f5f5f5,stroke:#444,stroke-width:1px,color:#000;
|
||||
classDef pwr fill:#fff,stroke:#444,stroke-width:1px,color:#000;
|
||||
classDef gnd fill:#fff,stroke:#000,stroke-width:1.5px,color:#000;
|
||||
classDef io fill:#f5f5f5,stroke:#111,stroke-width:1px,color:#000;
|
||||
@@ -1,309 +1,666 @@
|
||||
# Electricity Price Ticker for XIAO ESP32‑C3 (Slovenia, Energy‑Charts)
|
||||

|
||||
|
||||
# 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 for **Slovenia** from [Energy‑Charts.info](https://energy-charts.info).
|
||||
- Computes final consumer prices (including configurable fees + VAT).
|
||||
- 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 an LED and presence sensor to give quick visual feedback.
|
||||
- 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 code currently implements **Version 6.0** of the ticker, focusing on:
|
||||
The latest sketch implements **Version 6.2.4**.
|
||||
|
||||
---
|
||||
|
||||
## Version Highlights
|
||||
|
||||
### v6.2.4 - Exact-boundary display refresh bug (critical fix of v6.2.3 update)
|
||||
- Problem: At the exact top of the hour (e.g., 20:00:00), the display automatically refreshed but showed the PREVIOUS hour's data (19:00). This happened because the "next-boundary" rounding logic in findCurrentPriceIndex() incorrectly excluded the current interval if the time was exactly on the boundary.
|
||||
- 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 price lookups instead of arithmetic calculations.
|
||||
- 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 for resilience to reboots.
|
||||
- Automatic CET/CEST handling.
|
||||
- Robust **NVS storage** of daily price data.
|
||||
- Correct **CET/CEST** handling.
|
||||
- Resilient **after‑midnight refresh** (no more getting stuck on "No data for today").
|
||||
- Preserved UI and button behavior from v5.5.
|
||||
|
||||
All available bidding zones (modify in the code):
|
||||
- AT - Austria
|
||||
- BE - Belgium
|
||||
- BG - Bulgaria
|
||||
- CH - Switzerland
|
||||
- CZ - Czech Republic
|
||||
- DE-LU - Germany, Luxembourg
|
||||
- DE-AT-LU - Germany, Austria, Luxembourg
|
||||
- DK1 - Denmark 1
|
||||
- DK2 - Denmark 2
|
||||
- EE - Estionia
|
||||
- ES - Spain
|
||||
- FI - Finland
|
||||
- FR - France
|
||||
- GR - Greece
|
||||
- HR - Croatia
|
||||
- HU - Hungary
|
||||
- IT-Calabria - Italy Calabria
|
||||
- IT-Centre-North - Italy Centre North
|
||||
- IT-Centre-South - Italy Centre South
|
||||
- IT-North - Italy North
|
||||
- IT-SACOAC - Italy Sardinia Corsica AC
|
||||
- IT-SACODC - Italy Sardinia Corsica DC
|
||||
- IT-Sardinia - Italy Sardinia
|
||||
- IT-Sicily - Italy Sicily
|
||||
- IT-South - Italy South
|
||||
- LT - Lithuania
|
||||
- LV - Latvia
|
||||
- ME - Montenegro
|
||||
- NL - Netherlands
|
||||
- NO1 - Norway 1
|
||||
- NO2 - Norway 2
|
||||
- NO2NSL - Norway North Sea Link
|
||||
- NO3 - Norway 3
|
||||
- NO4 - Norway 4
|
||||
- NO5 - Norway 5
|
||||
- PL - Poland
|
||||
- PT - Portugal
|
||||
- RO - Romania
|
||||
- RS - Serbia
|
||||
- SE1 - Sweden 1
|
||||
- SE2 - Sweden 2
|
||||
- SE3 - Sweden 3
|
||||
- SE4 - Sweden 4
|
||||
- SI - Slovenia
|
||||
- SK - Slovakia
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
## Bidding Zones (BZN) / Region Selection
|
||||
|
||||
### Core display & pricing
|
||||
The firmware currently uses:
|
||||
|
||||
- Data source: [Energy‑Charts.info day‑ahead price API](https://api.energy-charts.info/price?bzn=SI) (bidding zone = `SI`).
|
||||
- Resolution:
|
||||
- Prices in **15‑minute intervals** (4 per hour).
|
||||
- Display shows:
|
||||
- Current hour:
|
||||
- Hourly average.
|
||||
- Four 15‑minute prices in compact format (`XX XX XX XX`).
|
||||
- Next 2 hours’ averages.
|
||||
- Price calculation:
|
||||
- Raw MWh prices from API are converted to EUR/kWh.
|
||||
- Optional surcharges:
|
||||
- `POWER_COMPANY_FEE_PERCENTAGE` (default 12%).
|
||||
- `VAT_PERCENTAGE` (default 22%).
|
||||
- LCD:
|
||||
- **20x4 I²C (PCF8574 / 0x27)**.
|
||||
- Custom characters for local language and low/high price markers.
|
||||
```text
|
||||
https://api.energy-charts.info/price?bzn=SI
|
||||
```
|
||||
|
||||
### LED behavior
|
||||
Where `bzn` is the **bidding zone** code. You can change this in the `.ino`:
|
||||
|
||||
- One **white LED** connected to GPIO 5.
|
||||
- LED indicates **current 15‑minute segment** price:
|
||||
- Very cheap (`<= 0.05 EUR/kWh`): smooth breathing.
|
||||
- Cheap, normal, expensive, very expensive: different steady / blinking / double‑blink patterns.
|
||||
- Negative or no data: LED off.
|
||||
```cpp
|
||||
const char* api_url = "https://api.energy-charts.info/price?bzn=SI";
|
||||
```
|
||||
|
||||
### Presence sensor & backlight
|
||||
to any supported BZN.
|
||||
|
||||
- Optional **RCWL‑0516** presence sensor on GPIO 9:
|
||||
- Presence = backlight on, LED enabled.
|
||||
- No presence for a while = LCD backlight off, LED disabled.
|
||||
- If no presence sensor is detected, the backlight is kept on.
|
||||
All available bidding zones (from the original README):
|
||||
|
||||
### Button behavior
|
||||
- `AT` ‑ Austria
|
||||
- `BE` ‑ Belgium
|
||||
- `BG` ‑ Bulgaria
|
||||
- `CH` ‑ Switzerland
|
||||
- `CZ` ‑ Czech Republic
|
||||
- `DE-LU` ‑ Germany, Luxembourg
|
||||
- `DE-AT-LU` ‑ Germany, Austria, Luxembourg
|
||||
- `DK1` ‑ Denmark 1
|
||||
- `DK2` ‑ Denmark 2
|
||||
- `EE` ‑ Estonia
|
||||
- `ES` ‑ Spain
|
||||
- `FI` ‑ Finland
|
||||
- `FR` ‑ France
|
||||
- `GR` ‑ Greece
|
||||
- `HR` ‑ Croatia
|
||||
- `HU` ‑ Hungary
|
||||
- `IT-Calabria` ‑ Italy Calabria
|
||||
- `IT-Centre-North` ‑ Italy Centre North
|
||||
- `IT-Centre-South` ‑ Italy Centre South
|
||||
- `IT-North` ‑ Italy North
|
||||
- `IT-SACOAC` ‑ Italy Sardinia Corsica AC
|
||||
- `IT-SACODC` ‑ Italy Sardinia Corsica DC
|
||||
- `IT-Sardinia` ‑ Italy Sardinia
|
||||
- `IT-Sicily` ‑ Italy Sicily
|
||||
- `IT-South` ‑ Italy South
|
||||
- `LT` ‑ Lithuania
|
||||
- `LV` ‑ Latvia
|
||||
- `ME` ‑ Montenegro
|
||||
- `NL` ‑ Netherlands
|
||||
- `NO1` ‑ Norway 1
|
||||
- `NO2` ‑ Norway 2
|
||||
- `NO2NSL` ‑ Norway North Sea Link
|
||||
- `NO3` ‑ Norway 3
|
||||
- `NO4` ‑ Norway 4
|
||||
- `NO5` ‑ Norway 5
|
||||
- `PL` ‑ Poland
|
||||
- `PT` ‑ Portugal
|
||||
- `RO` ‑ Romania
|
||||
- `RS` ‑ Serbia
|
||||
- `SE1` ‑ Sweden 1
|
||||
- `SE2` ‑ Sweden 2
|
||||
- `SE3` ‑ Sweden 3
|
||||
- `SE4` ‑ Sweden 4
|
||||
- `SI` ‑ Slovenia
|
||||
- `SK` ‑ Slovakia
|
||||
|
||||
- One button (or capacitive touch) on GPIO 4.
|
||||
- Uses internal pull‑up by default.
|
||||
- Actions:
|
||||
- **Single short press**:
|
||||
- Scrolls through the hourly view on the **primary** screen.
|
||||
- **Double press**:
|
||||
- Toggles between:
|
||||
- Primary price view
|
||||
- Secondary info view
|
||||
- **Long press (≈3s in your current repo)**:
|
||||
- Forces a **manual data refresh** (API fetch), regardless of daily schedule.
|
||||
> Always verify up‑to‑date BZN support in the Energy‑Charts API docs.
|
||||
|
||||
---
|
||||
|
||||
## Version 6.0 – Daily Fetch + NVS Storage
|
||||
## Hardware Setup (Detailed)
|
||||
|
||||
Version 6.0 is focused on:
|
||||
This section merges the original v5.5 instructions with the current v6.1 hardware expectations.
|
||||
Follow it carefully to reproduce the working setup.
|
||||
|
||||
1. **Reducing API calls**
|
||||
2. **Persisting daily data in NVS**
|
||||
3. **Making the device robust to power outages**
|
||||
|
||||
### 1. Daily fetch instead of hourly
|
||||
|
||||
Previously (v5.5), the firmware fetched data **every hour** at the top of the hour.
|
||||
|
||||
In **v6.0**, the behavior is:
|
||||
|
||||
- **On boot (after Wi‑Fi + time sync):**
|
||||
- Try to load **today’s** data from NVS:
|
||||
- If found: use it directly, **no API call** at boot.
|
||||
- If not found or invalid: schedule an immediate API fetch.
|
||||
- **After midnight (local time):**
|
||||
- Detect day rollover via localtime.
|
||||
- Immediately:
|
||||
- Invalidate yesterday’s data (`isTodayDataAvailable = false`).
|
||||
- Force display to show **“No data for today”**.
|
||||
- Turn off the white LED (no price indication).
|
||||
- Enter **midnight fetch phase**.
|
||||
|
||||
#### Midnight fetch phase
|
||||
|
||||
When the date changes:
|
||||
|
||||
1. First fetch is scheduled **immediately**.
|
||||
2. If it fails:
|
||||
- The device **stays in “No data for today”**, LED off.
|
||||
- Retry schedule:
|
||||
- 10‑minute interval, up to 5 attempts.
|
||||
- After 5 failures, retry once at each **top of the hour** until it succeeds.
|
||||
|
||||
As soon as a fetch for **today** succeeds:
|
||||
|
||||
- `isTodayDataAvailable` is set to `true`.
|
||||
- The enforced `NO_DATA_OFFSET` state is cleared back to `CURRENT_PRICES`.
|
||||
- The LCD and LED resume showing today’s prices.
|
||||
|
||||
> Importantly, **yesterday’s data is never re‑used** for today, both at boot and after midnight.
|
||||
|
||||
### 2. NVS (non‑volatile) storage of daily data
|
||||
|
||||
The ESP32‑C3’s built‑in NVS is used via `Preferences`:
|
||||
|
||||
- Namespace: `"my-ticker"`.
|
||||
- Keys (in addition to the existing Wi‑Fi credentials):
|
||||
- `data_day`, `data_mon`, `data_year` – calendar date of the stored data.
|
||||
- `data_prc` – full raw JSON response from the Energy‑Charts API.
|
||||
- `data_last_store` – UNIX timestamp when the data was last written.
|
||||
|
||||
#### On boot
|
||||
|
||||
After time sync:
|
||||
|
||||
- If NVS has data whose date **matches today**:
|
||||
- `data_prc` JSON is deserialized into the in‑RAM `doc` (`StaticJsonDocument`).
|
||||
- `processJsonData()` is run, just like after a live fetch.
|
||||
- `isTodayDataAvailable = true`.
|
||||
- No initial API call is needed.
|
||||
- If the stored date does **not** match today (or parsing fails):
|
||||
- The stored data is **ignored** for display.
|
||||
- The system starts from “No data for today” and will fetch new data.
|
||||
|
||||
#### After each successful fetch for today
|
||||
|
||||
- The entire JSON payload is stored as `data_prc` with the corresponding date and timestamp.
|
||||
- This means:
|
||||
- A reboot **later in the same day** can immediately restore the latest prices from NVS.
|
||||
- Reboots the next morning will detect the date mismatch and fetch new data instead of showing stale data.
|
||||
|
||||
### 3. Secondary info (status) screen
|
||||
|
||||
The secondary menu (double‑click to toggle) has **20 lines**, shown in 4‑line pages.
|
||||
|
||||
It typically includes:
|
||||
|
||||
- Current date and time.
|
||||
- Last successful update timestamp.
|
||||
- Daily average price in EUR/kWh (with company fee + VAT applied).
|
||||
- Wi‑Fi RSSI and IP address.
|
||||
- API success ratio and total calls.
|
||||
- Device uptime.
|
||||
- **NVS status section**:
|
||||
- `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`
|
||||
- Credits + version line:
|
||||
- `energy-charts.info`
|
||||
- `dynamic electricity`
|
||||
- `price ticker v6.0`
|
||||
- `by Amir Toki^ 2025`
|
||||
|
||||
(Exact ordering and layout may differ slightly, based on your latest edits.)
|
||||
|
||||
---
|
||||
|
||||
## Hardware setup
|
||||
|
||||
### Microcontroller
|
||||
### 1. Microcontroller
|
||||
|
||||
- **Seeed XIAO ESP32‑C3**
|
||||
|
||||
### LCD (I²C 2004)
|
||||
Typical pins used in the sketch:
|
||||
|
||||
- 20x4 (2004) character LCD with I²C backpack (PCF8574).
|
||||
- Default I²C address: `0x27` (configurable in code).
|
||||
- Library: `LiquidCrystal_I2C`.
|
||||
- `GPIO 5` → white LED (`whiteLedPin`)
|
||||
- `GPIO 21` → built‑in LED (`builtinLedPin`)
|
||||
- `GPIO 4` → user button / touch input (`buttonPin`)
|
||||
- `GPIO 9` → presence sensor (`presencePin`)
|
||||
- I²C pins → board‑default SDA/SCL (check XIAO ESP32‑C3 pinout)
|
||||
|
||||
### Button / touch input
|
||||
---
|
||||
|
||||
- GPIO 4, internal pull‑up enabled.
|
||||
### 2. 20x4 I²C LCD (2004) – PCF8574 Backpack
|
||||
|
||||
**Mechanical pushbutton (default)**
|
||||
- LCD: **20x4 2004 character display** with I²C backpack (PCF8574 or compatible).
|
||||
- Default I²C address (in code): `0x27`
|
||||
(Change in the sketch if your module differs: `LiquidCrystal_I2C lcd(0x27, 20, 4);`)
|
||||
|
||||
- One leg to GPIO 4, other leg to GND.
|
||||
**Connections:**
|
||||
|
||||
**TTP223 capacitive touch (alternative)**
|
||||
| LCD Backpack | XIAO ESP32‑C3 |
|
||||
|-------------|---------------|
|
||||
| VCC | 5V |
|
||||
| GND | GND |
|
||||
| SDA | I²C SDA |
|
||||
| SCL | I²C SCL |
|
||||
|
||||
- VCC → 3.3 V
|
||||
- GND → GND
|
||||
- OUT → GPIO 4
|
||||
> Note: On many XIAO ESP32‑C3 board definitions, SDA/SCL are mapped internally. Just use the default I²C pins as documented by Seeed.
|
||||
|
||||
Code change required for TTP223:
|
||||
---
|
||||
|
||||
### 3. Pushbutton (Default) / Capacitive Touch Alternative
|
||||
|
||||
The firmware assumes a **momentary pushbutton** on `GPIO 4` by default.
|
||||
|
||||
#### Mechanical Pushbutton (default config)
|
||||
|
||||
- One leg → `GPIO 4`
|
||||
- Other leg → `GND`
|
||||
- No external pull‑up is required; code uses:
|
||||
|
||||
```cpp
|
||||
// In handleButton() or wherever the button is read:
|
||||
int reading = !digitalRead(buttonPin);
|
||||
// Change to:
|
||||
int reading = digitalRead(buttonPin); // if TTP223 output is HIGH when touched
|
||||
pinMode(buttonPin, INPUT_PULLUP);
|
||||
```
|
||||
|
||||
(Your current code uses `!digitalRead(buttonPin)` assuming an active‑LOW mechanical button.)
|
||||
And reads the button as **active‑LOW**:
|
||||
|
||||
### Presence sensor (RCWL‑0516)
|
||||
```cpp
|
||||
int reading = !digitalRead(buttonPin);
|
||||
```
|
||||
|
||||
- VCC → 3.3 V
|
||||
- GND → GND
|
||||
- OUT → GPIO 9
|
||||
- 10 kΩ pull‑down resistor between GPIO 9 and GND.
|
||||
So:
|
||||
|
||||
If no sensor is detected at boot, the firmware keeps the LCD backlight always on.
|
||||
- Button **pressed** ⇒ `reading == 1`
|
||||
- Button **released** ⇒ `reading == 0`
|
||||
|
||||
### White LED
|
||||
#### Alternative: TTP223 Capacitive Touch Button
|
||||
|
||||
- LED (with appropriate series resistor) on GPIO 5.
|
||||
If you prefer a TTP223 capacitive touch input instead of a mechanical button:
|
||||
|
||||
**Wiring:**
|
||||
|
||||
- `VCC` → **3.3V**
|
||||
- `GND` → **GND**
|
||||
- `OUT` → `GPIO 4` (same as the pushbutton pin)
|
||||
|
||||
**Logic:**
|
||||
|
||||
- TTP223 output is **HIGH when touched**.
|
||||
|
||||
If you use TTP223, you may want to **remove the logical inversion** in the code:
|
||||
|
||||
```cpp
|
||||
// For mechanical button (active LOW):
|
||||
int reading = !digitalRead(buttonPin);
|
||||
|
||||
// For TTP223 (active HIGH), change to:
|
||||
int reading = digitalRead(buttonPin);
|
||||
```
|
||||
|
||||
Everything else (debounce, long‑press, double‑click) remains compatible.
|
||||
|
||||
---
|
||||
|
||||
## Wi‑Fi provisioning
|
||||
### 4. Presence Sensor (RCWL‑0516, optional but supported)
|
||||
|
||||
If stored Wi‑Fi credentials are invalid or missing:
|
||||
The presence sensor is used to control LCD backlight and LEDs to save power and avoid annoying blinking when nobody is around.
|
||||
|
||||
1. Device starts in AP mode with SSID: `MyTicker_Setup`.
|
||||
2. DNS server redirects to a simple captive “Wi‑Fi Setup” page.
|
||||
3. You can enter SSID and password, which are stored in NVS (`Preferences`).
|
||||
4. The device restarts and attempts to connect with the new credentials.
|
||||
Recommended module: **RCWL‑0516** microwave motion sensor.
|
||||
|
||||
**Wiring:**
|
||||
|
||||
| RCWL‑0516 | Connection |
|
||||
|-----------|------------|
|
||||
| VCC | 3.3V |
|
||||
| GND | GND |
|
||||
| OUT | GPIO 9 (`presencePin`) |
|
||||
| **Required**: 10kΩ pull‑down | Between GPIO 9 and GND |
|
||||
|
||||
Characteristics:
|
||||
|
||||
- The module can be hidden behind non‑metallic surfaces.
|
||||
- Firmware automatically detects if the presence sensor is connected at boot:
|
||||
- If **detected**:
|
||||
- Presence toggles backlight on and enables LED output.
|
||||
- Absence for `backlightOffDelay` (default 30 s) turns the backlight off and disables LED output.
|
||||
- If **not detected**:
|
||||
- Backlight is kept on permanently.
|
||||
- LEDs are allowed to operate normally.
|
||||
|
||||
---
|
||||
|
||||
## Building and flashing
|
||||
### 5. White LED / LED Strip Output
|
||||
|
||||
1. Open the `.ino` file in **Arduino IDE** (ensure ESP32 board support is installed).
|
||||
2. Select:
|
||||
- Board: `Seeed XIAO ESP32C3`
|
||||
- Port: correct serial port
|
||||
3. Install the required libraries if missing:
|
||||
The sketch uses a **white LED** (or LED strip control line) on `GPIO 5` (`whiteLedPin`).
|
||||
|
||||
**Basic single LED wiring:**
|
||||
|
||||
- `GPIO 5` → series resistor (e.g. 220–470 Ω) → LED anode
|
||||
- LED cathode → GND
|
||||
|
||||
**For LED strips or higher currents:**
|
||||
|
||||
- Use a suitable NPN transistor / MOSFET:
|
||||
|
||||
- GPIO 5 → gate/base (with proper gate/base resistor)
|
||||
- LED strip or load → external supply (with common GND)
|
||||
- Transistor sink/source → GND / load as per standard MOSFET wiring
|
||||
|
||||
- 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.
|
||||
|
||||
---
|
||||
|
||||
### 6. Power
|
||||
|
||||
- XIAO ESP32‑C3:
|
||||
- Via USB‑C (recommended for development).
|
||||
- Or via 5V pin if you have a regulated 5V supply (check Seeed docs).
|
||||
- Ensure **all modules** (LCD, presence sensor, LED driver) share a **common ground** with the XIAO.
|
||||
|
||||
---
|
||||
|
||||
## Firmware Features (v6.2.0)
|
||||
|
||||
### Core Display & Pricing
|
||||
|
||||
- Data source: `https://api.energy-charts.info/price?bzn=SI`
|
||||
- Resolution: 15‑minute intervals with hourly averages
|
||||
- Display shows:
|
||||
- **Row 0**: Current hour, four 15‑minute values
|
||||
- **Rows 1–3**: Current hour + next two hours as hourly averages
|
||||
- Price calculation: Raw MWh → EUR/kWh with configurable surcharges (power company fee + VAT)
|
||||
- Two configurable surcharges:
|
||||
- `POWER_COMPANY_FEE_PERCENTAGE` (default `12.0` %).
|
||||
- `VAT_PERCENTAGE` (default `22.0` %).
|
||||
- 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).
|
||||
|
||||
### LED Price Signalling
|
||||
|
||||
The white LED (GPIO 5) reflects the **current 15‑minute interval** price:
|
||||
|
||||
| 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 (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.
|
||||
|
||||
LED is **disabled** when:
|
||||
|
||||
- No data for today.
|
||||
- Time is not synced.
|
||||
- Presence sensor has timed out (no presence, if installed).
|
||||
|
||||
### Presence Sensor & Backlight
|
||||
|
||||
- If presence sensor is **connected**:
|
||||
- Presence detected → LCD backlight on, LEDs enabled.
|
||||
- No presence for `backlightOffDelay` (30 s by default) → LCD backlight off, LEDs disabled.
|
||||
- If **no presence sensor** is detected at boot:
|
||||
- LCD backlight is always on.
|
||||
- LEDs are not gated by presence.
|
||||
|
||||
### Button Behavior
|
||||
|
||||
One button (or touch) on GPIO 4 controls the UI:
|
||||
|
||||
- **Single short press**:
|
||||
- On primary screen: scrolls the time offset (future hours).
|
||||
- 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)**:
|
||||
- While held:
|
||||
- LCD shows: “Long press detected! Release to refresh”.
|
||||
- On release:
|
||||
- Forces a **manual data refresh**:
|
||||
- Sets `nextScheduledFetchTime = now`.
|
||||
- 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.
|
||||
|
||||
---
|
||||
|
||||
## NVS Storage (Daily Data Cache)
|
||||
|
||||
This firmware uses ESP32‑C3 **Preferences API** (`Preferences`) under namespace `"my-ticker"`.
|
||||
|
||||
Stored keys:
|
||||
|
||||
- **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
|
||||
|
||||
### On Boot
|
||||
|
||||
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.
|
||||
- Set `isTodayDataAvailable = true`.
|
||||
- **Skip** the initial API call to save traffic.
|
||||
3. 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”.
|
||||
- Schedules an immediate API fetch.
|
||||
|
||||
### After Each Successful Fetch for Today
|
||||
|
||||
- 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.
|
||||
|
||||
---
|
||||
|
||||
## Daily Fetch Strategy (v6.1.0+)
|
||||
|
||||
### Goals
|
||||
|
||||
- **Avoid hourly polling** of the API.
|
||||
- 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.
|
||||
|
||||
### 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…”.
|
||||
- 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.
|
||||
|
||||
### Day‑Rollover Detection
|
||||
|
||||
In the main `loop()`:
|
||||
|
||||
- `trackedDay` holds the last seen local day.
|
||||
- Each iteration:
|
||||
- Get `localtime()` for `now`.
|
||||
- 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”.
|
||||
|
||||
### “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.
|
||||
3. Convert it to local time (`localtime()`).
|
||||
4. Compare its date (day, month, year) to the current local date.
|
||||
- If they **match**:
|
||||
- Dataset is accepted as “today’s” data.
|
||||
- `isTodayDataAvailable = true`.
|
||||
- `lastProcessJsonAcceptedToday = true`.
|
||||
- Prices are processed (hourly averages, min/max, daily average).
|
||||
- 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).
|
||||
|
||||
---
|
||||
|
||||
## Secondary Status Screen (Debug / Info)
|
||||
|
||||
A **secondary screen** (toggled via **double‑click**) provides 20 lines of status information, displayed 4 lines at a time:
|
||||
|
||||
Typical content:
|
||||
|
||||
1. Current date and time (`HH:MM DD.MM.YYYY`)
|
||||
2. Separator line (`--------------------`)
|
||||
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.”
|
||||
8. Blank
|
||||
9. Wi‑Fi status and RSSI
|
||||
10. Local IP address
|
||||
11. API success rate (`API: xx% (succ/fail)`)
|
||||
12. Device uptime in days, hours, minutes
|
||||
13–16. **NVS status block**:
|
||||
- `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`
|
||||
17–20. Credits and version:
|
||||
- `energy-charts.info`
|
||||
- `dynamic electricity`
|
||||
- `price ticker v6.1`
|
||||
- `by Legolas-2025` (or your preferred credit line)
|
||||
|
||||
---
|
||||
|
||||
## Wi‑Fi Provisioning
|
||||
|
||||
If NVS does not contain valid Wi‑Fi credentials, or if connecting fails repeatedly:
|
||||
|
||||
1. The device starts an **Access Point** with SSID:
|
||||
|
||||
```text
|
||||
MyTicker_Setup
|
||||
```
|
||||
|
||||
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.
|
||||
- Values are stored in NVS: `ssid`, `pass`.
|
||||
- Device reboots and attempts to connect with the new credentials.
|
||||
|
||||
---
|
||||
|
||||
## Building & Uploading
|
||||
|
||||
1. Install **Arduino IDE** with ESP32 board support (including XIAO ESP32‑C3).
|
||||
2. Install required libraries:
|
||||
- `LiquidCrystal_I2C`
|
||||
- `ArduinoJson`
|
||||
4. Compile and upload.
|
||||
- `DNSServer` (from ESP32 core)
|
||||
- `WebServer` (from ESP32 core)
|
||||
- `Preferences` (built‑in for ESP32)
|
||||
3. Open the v6.2.0 `.ino` file (e.g. `ESP32_standalone_electricity_ticker_6_1_2_nvs_daily_fetch.ino`).
|
||||
4. In Tools:
|
||||
- Board: `Seeed XIAO ESP32C3`
|
||||
- Port: choose the correct serial port.
|
||||
5. Upload the sketch.
|
||||
6. Open Serial Monitor at **115200 baud** to see:
|
||||
- Wi‑Fi connection logs.
|
||||
- NTP sync messages.
|
||||
- NVS load/save status.
|
||||
- Midnight rollover and retry debug output.
|
||||
|
||||
---
|
||||
|
||||
## Versioning and files
|
||||
|
||||
Key documentation files:
|
||||
## Versioning & Changelog
|
||||
|
||||
- [`VERSION.md`](./VERSION.md) – current firmware version and compatibility notes.
|
||||
- [`CHANGELOG.md`](./CHANGELOG.md) – detailed list of changes between versions.
|
||||
- `*.ino` – main firmware source file (v6.0 and later).
|
||||
- **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:
|
||||
- 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.
|
||||
- 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.
|
||||
- **v6.0.0** – NVS storage & daily fetch:
|
||||
- Store daily price data in NVS.
|
||||
- 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`).
|
||||
|
||||
See [`CHANGELOG.md`](./CHANGELOG.md) for more details.
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License – see the [LICENSE](./LICENSE) file for details.
|
||||
This project is licensed under the MIT License – see the [`LICENSE`](./LICENSE) file for details.
|
||||
|
||||
+109
-15
@@ -2,23 +2,117 @@
|
||||
|
||||
## Current 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
|
||||
- **Version:** 7.0
|
||||
- **Release date:** 2026-04-03
|
||||
- **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
|
||||
## Highlights of v7.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.
|
||||
### 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").
|
||||
|
||||
## 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:
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 434 KiB |
Reference in New Issue
Block a user