Compare commits

..
4 Commits
Author SHA1 Message Date
Amir 25fc31fb3b Revise README for version 6.1.0
Updated project details and version information in README.
2026-02-01 22:07:41 +01:00
Amir aafe962c4c Price ticker v6.1.0 - latest 2026-02-01 21:50:38 +01:00
Amir a57af528f8 Update CHANGELOG for v6.1.0 release
Documented the changes and fixes in version 6.1.0, including improvements to market day detection and retry logic after midnight.
2026-01-31 01:59:03 +01:00
Amir ce52d6b9c0 VERSION 6.0 CHANGES (2026-01-27)
Update credits in electricity ticker.
2026-01-28 01:17:15 +01:00
4 changed files with 2315 additions and 376 deletions
@@ -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++) {
+138 -122
View File
@@ -1,147 +1,163 @@
# Changelog
All notable changes to this project will be documented in this file.
This project follows a simple semantic versioning style:
`MAJOR.MINOR.PATCH`
All notable changes to this project are documented here.
---
## [6.0.0] - 2026-01-27
## v6.1.0 Midnight Fetch & Market Day Fix (20260130)
### Added
- **Daily fetch + NVS storage (ESP32C3 NVS)**
- Introduced nonvolatile 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 EnergyCharts API)
- `data_last_store` (UNIX timestamp of last successful store)
- On boot, after WiFi 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 days data and trigger a new fetch for the new day.
- As soon as a localtime 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.
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 aftermidnight retry schedule.
- **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 15minute segment price.
- The entire successful JSON payload and metadata are saved back into NVS.
### Fixed: Stuck on NO_DATA_OFFSET After Midnight
- **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:
- Yesterdays stored data is **not** used for display (to avoid confusion).
- The display starts in `"No data for today"` until the first successful fetch.
**Problem (v6.0.0):**
- **Extended secondary (info) menu**
- The secondary info screen (reachable via doubleclick on the button) is extended from 16 to **20 lines**, still shown as 4line pages.
- Existing info retained:
- Current time and date.
- Last successful update timestamp.
- Daily average price.
- WiFi RSSI and device IP.
- API success ratio.
- Uptime.
- Credits and version information.
- **New NVS status section** (first defined around lines 1215, 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
- 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 **yesterdays 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 longpress triggered a fresh fetch at a time when the API finally returned recognized “today” data, or
- The device was rebooted later in the day.
### Changed
- **API call strategy**
- Removed regular **topofhour** 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 userinitiated **longpress** (manual refresh).
- This significantly reduces network load while keeping behavior safe and predictable.
**Solution (v6.1.0):**
- **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 15minute detail + hourly display mode.
1. **Market Day Detection**:
- `processJsonData()` now determines the “market day” using the **last** `unix_seconds` timestamp from the APIs dataset (assumed to cover one full day in 15minute 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`.
- **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`).
2. **Logical Failure vs HTTP/JSON Failure**:
- New global flag:
- `lastProcessJsonAcceptedToday` `true` only when `processJsonData()` accepts the dataset as “todays” 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.
- **Longpress threshold (in repository version)**
- Longpress detection threshold extended from 2s to **3s** (in your repository copy) to avoid accidental manual refreshes.
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.
### Fixed / Ensured
- Yesterdays data is **never shown** as if it were todays:
- On boot: previousday NVS data is ignored for display.
- After midnight: inRAM 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 **todays** data:
- LED off → no today data (or negative price).
- LED patterns → valid today data and a positive price in the current 15minute interval.
As a result, the ticker will **keep retrying** after midnight until a correct marketday dataset appears, instead of giving up after the first HTTP 200.
---
## [5.5.0] - 2025-10-27
### Changed: AfterMidnight Retry Schedule
> First 15minute detail version and DSTfixed base, which v6.0 builds upon.
In `scheduleAfterMidnightFailure()` the retry strategy when `midnightPhaseActive == true` has been tuned.
### Added
- **15minute detail mode**:
- Primary display shows:
- Current hour average.
- Four 15minute prices in compact format (`XX XX XX XX`).
- Next 2 hours averages.
- LED behavior switched from hourlybased to **15minutesegment based**.
- **Compact price format**:
- 15minute values shown as 2digit 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, WiFi and API stats, uptime, credits.
**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.
---
## Older versions
### Other Behavior (Retained From v6.0.0)
Earlier versions (≤5.4) introduced the basic ticker behavior, LCD layout, EnergyCharts API integration, and the initial presence sensor / backlight / LED logic.
- **NVS caching** of daily data:
- On boot, if NVS contains a dataset whose date matches todays 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 longpress refresh**:
- Still triggers immediate fetch via `nextScheduledFetchTime = now;`.
- Now also respects the improved “today” detection; “yesterdays” 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.
Those versions are not fully documented here, but key uservisible behavior is maintained in v6.0 unless explicitly noted in this changelog.
---
## v6.0.0 NVS Storage & Daily Fetch (20260127)
**Summary**
First major redesign focused on reducing API traffic and improving resilience using nonvolatile storage.
### New
- NVS namespace `"my-ticker"` introduced with keys:
- `ssid`, `pass` WiFi 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.
- Aftermidnight behavior:
- NVS is overwritten with each successful newday dataset.
- InRAM data for yesterday is invalidated at day rollover.
### UI / Menu
- Primary list:
- 15minute 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.
- WiFi RSSI, IP address.
- API success rate, uptime.
- NVS status block.
- Credits & version line.
---
## v5.x Earlier Versions
Earlier versions (v5.x and below) had:
- No NVSbased 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
+525 -253
View File
@@ -1,309 +1,581 @@
# Electricity Price Ticker for XIAO ESP32C3 (Slovenia, EnergyCharts)
# Electricity Price Ticker for XIAO ESP32C3 (EnergyCharts)
This project is an ArduinoIDEfriendly firmware for the **Seeed XIAO ESP32C3** that:
- Connects to WiFi.
- Fetches dayahead electricity prices for **Slovenia** from [EnergyCharts.info](https://energy-charts.info).
- Computes final consumer prices (including configurable fees + VAT).
- Fetches **dayahead electricity prices** from [EnergyCharts.info](https://energy-charts.info) (Bundesnetzagentur / SMARD.de).
- Computes final consumer prices (including configurable powercompany 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.1.0**, focusing on:
- 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 **aftermidnight 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
---
## Bidding Zones (BZN) / Region Selection
The firmware currently uses:
```text
https://api.energy-charts.info/price?bzn=SI
```
Where `bzn` is the **bidding zone** code. You can change this in the `.ino`:
```cpp
const char* api_url = "https://api.energy-charts.info/price?bzn=SI";
```
to any supported BZN.
All available bidding zones (from the original README):
- `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
> Always verify uptodate BZN support in the EnergyCharts API docs.
---
## Features
## Hardware Setup (Detailed)
### Core display & pricing
This section merges the original v5.5 instructions with the current v6.1 hardware expectations.
Follow it carefully to reproduce the working setup.
- Data source: [EnergyCharts.info dayahead price API](https://api.energy-charts.info/price?bzn=SI) (bidding zone = `SI`).
- Resolution:
- Prices in **15minute intervals** (4 per hour).
- Display shows:
- Current hour:
- Hourly average.
- Four 15minute 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.
### LED behavior
- One **white LED** connected to GPIO 5.
- LED indicates **current 15minute segment** price:
- Very cheap (`<= 0.05 EUR/kWh`): smooth breathing.
- Cheap, normal, expensive, very expensive: different steady / blinking / doubleblink patterns.
- Negative or no data: LED off.
### Presence sensor & backlight
- Optional **RCWL0516** 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.
### Button behavior
- One button (or capacitive touch) on GPIO 4.
- Uses internal pullup 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.
---
## Version 6.0 Daily Fetch + NVS Storage
Version 6.0 is focused on:
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 WiFi + time sync):**
- Try to load **todays** 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 yesterdays 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:
- 10minute 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 todays prices.
> Importantly, **yesterdays data is never reused** for today, both at boot and after midnight.
### 2. NVS (nonvolatile) storage of daily data
The ESP32C3s builtin NVS is used via `Preferences`:
- Namespace: `"my-ticker"`.
- Keys (in addition to the existing WiFi credentials):
- `data_day`, `data_mon`, `data_year` calendar date of the stored data.
- `data_prc` full raw JSON response from the EnergyCharts 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 inRAM `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 (doubleclick to toggle) has **20 lines**, shown in 4line pages.
It typically includes:
- Current date and time.
- Last successful update timestamp.
- Daily average price in EUR/kWh (with company fee + VAT applied).
- WiFi 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 ESP32C3**
### 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` → builtin LED (`builtinLedPin`)
- `GPIO 4` → user button / touch input (`buttonPin`)
- `GPIO 9` → presence sensor (`presencePin`)
- I²C pins → boarddefault SDA/SCL (check XIAO ESP32C3 pinout)
### Button / touch input
---
- GPIO 4, internal pullup 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 ESP32C3**
- `VCC`**5V** (or 3V3 if your module explicitly supports 3.3V I²C)
- `GND`**GND**
- `SDA` → board I²C SDA pin (see XIAO ESP32C3 documentation)
- `SCL` → board I²C SCL pin
- VCC → 3.3 V
- GND → GND
- OUT → GPIO 4
> Note: On many XIAO ESP32C3 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 pullup 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 activeLOW mechanical button.)
And reads the button as **activeLOW**:
### Presence sensor (RCWL0516)
```cpp
int reading = !digitalRead(buttonPin);
```
- VCC → 3.3 V
- GND → GND
- OUT → GPIO 9
- 10 kΩ pulldown 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, longpress, doubleclick) remains compatible.
---
## WiFi provisioning
### 4. Presence Sensor (RCWL0516, optional but supported)
If stored WiFi 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 “WiFi 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: **RCWL0516** microwave motion sensor.
**Wiring (from the v5.5 header, preserved in v6.x):**
- `VCC`**3.3V**
- `GND`**GND**
- `OUT``GPIO 9` (`presencePin`)
- **Required**: 10 kΩ pulldown resistor between `GPIO 9` and `GND`.
Characteristics:
- The module can be hidden behind nonmetallic 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. 220470 Ω) → 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 ESP32C3 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 ESP32C3:
- Via USBC (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.1.0)
### Core Display & Pricing
- Data source:
```text
https://api.energy-charts.info/price?bzn=SI
```
- Resolution:
- Prices in **15minute intervals** (`price[]`, `unix_seconds[]`).
- Display shows:
- **Row 0**: Current hour, four 15minute values in compact format (`XX XX XX XX`).
- **Rows 13**: 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.
- Lowprice and highprice indicators.
### LED Price Signalling
The white LED (GPIO 5) reflects the **current 15minute interval** price:
- Very cheap (`<= 0.05 EUR/kWh`) → smooth breathing.
- Cheap / normal → steady on.
- Moderately expensive → slow blink.
- Expensive → faster blink.
- Very expensive → complex “doubleblink with long on” pattern.
- Negative price or no data → LED 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 20line 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 **autoscroll timeout** resets the view to “current hour / top of lists” after inactivity.
---
## NVS Storage (Daily Data Cache)
This firmware uses ESP32C3 **Preferences API** (`Preferences`) under namespace `"my-ticker"`.
Stored keys:
- **WiFi credentials:**
- `ssid`
- `pass`
- **Daily price data:**
- `data_day` calendar day (131)
- `data_mon` month (011)
- `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 nextday 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 todays data) or an initial fetch is scheduled.
### DayRollover 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 EnergyCharts API can keep serving **yesterdays** market day for some time after local midnight.
To avoid accidentally accepting yesterdays data as todays, v6.1 uses a more robust rule.
In `processJsonData()`:
1. Read `unix_seconds[]`.
2. Interpret the **LAST** timestamp as representing the end of the datasets 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 “todays” 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 “yesterdays dayahead 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 yesterdays:
- 00:20 1st retry.
- 00:40 2nd retry.
- All “fast retries” remain fully within the first postmidnight 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` (datasets last timestamps 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 **doubleclick**) 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. WiFi status and RSSI
10. Local IP address
11. API success rate (`API: xx% (succ/fail)`)
12. Device uptime in days, hours, minutes
1316. **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`
1720. Credits and version:
- `energy-charts.info`
- `dynamic electricity`
- `price ticker v6.1`
- `by Legolas-2025` (or your preferred credit line)
---
## WiFi Provisioning
If NVS does not contain valid WiFi credentials, or if connecting fails repeatedly:
1. The device starts an **Access Point** with SSID:
```text
MyTicker_Setup
```
2. LCD shows “No WiFi access! Setup WiFi: 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 ESP32C3).
2. Install required libraries:
- `LiquidCrystal_I2C`
- `ArduinoJson`
4. Compile and upload.
- `DNSServer` (from ESP32 core)
- `WebServer` (from ESP32 core)
- `Preferences` (builtin for ESP32)
3. Open the v6.1 `.ino` file (e.g. `20260130_electricity_ticker_6_1_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:
- WiFi connection logs.
- NTP sync messages.
- NVS load/save status.
- Midnight rollover and retry debug output.
---
## Versioning and files
## Versioning & Changelog
Key documentation files:
- **v5.5** 15minute detail mode, LED based on current 15minute slot, improved DST handling (see file `20251027a_electricity_ticker_10_5_5_latest_DST_and_midnight_fix.ino`).
- **v6.0.0** First NVSenabled version:
- Store daily price data in NVS.
- Reduce API calls to “boot + aftermidnight”.
- Add NVS status section to secondary menu.
- **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 (topofhour) until todays dataset is available.
- Behavior on reboot and manual longpress is unchanged, but now respects the improved “today” logic.
- [`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).
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.