Updated Uptime description to include bucketed format and logbook entry frequency.
EPrices
An ESPHome-based ESP32 firmware that fetches day-ahead electricity spot prices from the public Energy-Charts API and exposes them as Home Assistant sensors — with no cloud subscription, no API token, and no external dependencies beyond your WiFi network.
All core logic runs entirely on the ESP32 — midnight bridge, boot recovery, retry scheduling, and NVS persistence are all on-device. No external Home Assistant automations are required for any core functionality.
Features
- Fetches 15-minute resolution spot prices for today and tomorrow
- Automatically applies your provider fee and VAT rate to raw €/MWh prices — with separate fee configuration for negative spot prices
- Exposes 96 per-day price points plus 24-hour averages as JSON text sensors for use in HA automations and dashboards
- NVS persistence — prices survive device reboots without re-fetching
- Midnight bridge — tomorrow's data automatically becomes today at 00:00, fully on-device
- Auto-retry logic — up to 8 HTTP fetch attempts for both today and tomorrow, with a 120-second stuck-fetch watchdog that unblocks retries after TCP-level stalls
- Task watchdog stability — watchdog timeout increased to 40s, idle task checking disabled;
periodic
yield()calls and optimised JSON building prevent spontaneous reboots during heavy parsing when full price data arrives (~13:55) - DST-safe — uses UNIX timestamps and binary search throughout, no hour-slot arithmetic
- Staleness detection —
Today Current Price StatusshowsStaleif stored date mismatches today - Tomorrow live sensors evaluate at
now + 86400s— reflecting tomorrow at the same local time - Supports any Energy-Charts bidding zone (SI, DE-LU, AT, FR, HR, HU and more)
- Full diagnostic sensor suite — NVS status, fetch attempts, API fetch times, data loaded times, WiFi signal, human-readable bucketed uptime (one HA logbook entry per hour / day / month — no per-minute churn)
- All fee and VAT settings configurable via
secrets.yaml— no code changes needed
Hardware
- ESP32 development board (tested on
esp32dev) - ESPHome with
esp-idfframework (required for NVS flash support)
Note on ESP32 variants: EPrices is compiled and tested on the original dual-core ESP32 (
esp32dev). It also runs on ESP32-S3 with a one-line board change. Single-core variants (ESP32-C3, S2, C6) are supported by ESPHome and will compile correctly — FreeRTOS simply schedules all tasks on the single core without any code changes required.
Files
| File | Purpose |
|---|---|
eprices.yaml |
Main ESPHome configuration |
eprices_nvs.h |
NVS helper — save/load price arrays to ESP32 flash |
secrets.yaml |
Your local secrets (not committed to git) |
CHANGELOG.md |
Complete sensor and entity ID reference |
VERSION.md |
Version history and release notes |
ENTSO-E-PRICES-MIGRATION.md |
Optional — migration guide from the predecessor project |
Installation
1) Clone or download this repository
2) Create your secrets.yaml
Place secrets.yaml in the same directory as eprices.yaml.
It must contain the following keys:
wifi_ssid: "your_wifi_ssid"
wifi_password: "your_wifi_password"
eprices_fallback_ap_ssid: "EPrices-Fallback"
eprices_fallback_ap_password: "your_fallback_ap_password"
eprices_api_encryption_key: "your_base64_api_encryption_key_here"
eprices_timezone: "Europe/Ljubljana"
eprices_country_bzn: "SI"
eprices_prov_fee: "0.12"
eprices_vat_rate: "0.22"
eprices_neg_prov_fee: "0.30"
Key descriptions:
| Key | Description |
|---|---|
eprices_timezone |
Your local timezone — must match a valid IANA tz name |
eprices_country_bzn |
Energy-Charts bidding zone code — see supported zones |
eprices_prov_fee |
Provider fee for positive spot prices — decimal multiplier, e.g. "0.12" = 12% |
eprices_vat_rate |
VAT rate — decimal multiplier, e.g. "0.22" = 22% |
eprices_neg_prov_fee |
Provider fee kept on negative spot prices — decimal multiplier, e.g. "0.30" means provider keeps 30%, pays you 70%; use "0.00" if provider passes the full negative price to you |
eprices_api_encryption_key |
32-byte base64 key for HA API encryption — generate with openssl rand -base64 32 |
3) Flash to your ESP32
esphome run eprices.yaml
Or use the ESPHome Dashboard or the HA ESPHome add-on.
4) Add to Home Assistant
The device will appear automatically in HA via the ESPHome integration. Accept the device and enter your API encryption key when prompted.
Supported bidding zones
Any zone supported by the Energy-Charts API
/price endpoint. Common examples:
| Code | Zone |
|---|---|
SI |
Slovenia |
DE-LU |
Germany / Luxembourg |
AT |
Austria |
FR |
France |
IT-North |
Italy North |
HR |
Croatia |
HU |
Hungary |
For the full list see the Energy-Charts API documentation.
How it works
Price fetch schedule
| Event | Action |
|---|---|
| Boot | Load today + tomorrow from NVS flash; HTTP fetch if NVS miss or stale |
| 00:00 midnight | Promote tomorrow → today; clear tomorrow; schedule tomorrow fetch if in window |
| 00:05, 00:15, 00:30, then hourly :30 | Auto-retry today fetch if previous attempt failed (max 8) |
| 13:25 | First auto-fetch attempt for tomorrow |
| 13:55, 14:55 … 19:55 | Retry tomorrow fetch if previous attempt failed (max 8 total) |
| Manual button press | Force immediate fetch for today or tomorrow |
Stuck-fetch watchdog
If an HTTP request stalls at the TCP level (connection accepted but data
delivery suspended), the 25 s application timeout may not fire. In that case
the is_updating_* flag can stay true for several minutes, blocking all
subsequent retries and manual button presses.
The worker loop checks every 10 seconds: if is_updating_today or
is_updating_tomorrow has been true for more than 120 seconds, the flag
is force-cleared and the status message is set to "Fetch timeout – will retry".
The next scheduled trigger or manual press then proceeds normally.
Price calculation
Raw prices from the API are in €/MWh. EPrices converts them to €/kWh
and applies your provider fee and VAT. Positive and negative spot prices use
separate fee multipliers, configurable independently in secrets.yaml:
| Market price | Formula |
|---|---|
Positive (raw ≥ 0) |
(raw / 1000) × (1 + prov_fee) × (1 + vat_rate) |
Negative (raw < 0) |
(raw / 1000) × (1 - neg_prov_fee) × (1 + vat_rate) |
The switch happens on the raw API price before any multiplier is applied. VAT is applied to both cases, consistent with net billing contracts where VAT is calculated on the monthly net sum (mathematically equivalent due to VAT being a linear multiplier).
Example with prov_fee = 0.12, neg_prov_fee = 0.30, vat_rate = 0.22:
- Spot price
+100 €/MWh→(100/1000) × 1.12 × 1.22= 0.1366 €/kWh (you pay) - Spot price
−50 €/MWh→(−50/1000) × 0.70 × 1.22= −0.0427 €/kWh (you receive)
Negative prices
On days with high solar or wind generation, spot prices can go negative.
EPrices handles negative prices correctly throughout. In the 15-min JSON
sensors, negative values are formatted with 3 decimal places (e.g. -0.057)
instead of 4, to stay within the 255-character Home Assistant text sensor
state limit. Precision loss is at most 0.0001 €/kWh (0.01 cent).
NVS persistence
Prices are stored in ESP32 NVS (non-volatile storage) under the eprices
namespace using dynamic timestamp + price arrays. On reboot, stored prices
are validated against today's and tomorrow's date before use. Stale data
is discarded and a fresh HTTP fetch is triggered automatically.
DST safety
All price indexing uses UNIX timestamps and binary search. There is no hour-slot arithmetic anywhere in the codebase, making the firmware fully safe on DST transition days (23-hour and 25-hour days).
Sensors
Today — numeric
| Sensor | Entity ID | Description |
|---|---|---|
| Today Current Price | sensor.eprices_today_current_price |
Current 15-min slot price €/kWh |
| Today Next Price | sensor.eprices_today_next_price |
Next 15-min slot price €/kWh |
| Today Average Price | sensor.eprices_today_average_price |
Average of all hourly prices €/kWh |
| Today Highest Price | sensor.eprices_today_highest_price |
Highest 15-min price €/kWh |
| Today Lowest Price | sensor.eprices_today_lowest_price |
Lowest 15-min price €/kWh |
| Today Current Hourly Price | sensor.eprices_today_current_hourly_price |
Current hour average €/kWh |
| Today Next Hourly Price | sensor.eprices_today_next_hourly_price |
Next hour average €/kWh |
| Today Highest Hourly Price | sensor.eprices_today_highest_hourly_price |
Highest hourly average €/kWh |
| Today Lowest Hourly Price | sensor.eprices_today_lowest_hourly_price |
Lowest hourly average €/kWh |
| Today Current Max Hourly Price Percentage | sensor.eprices_today_current_max_hourly_price_percentage |
Current hour as % of daily max |
Today — text
| Sensor | Entity ID | Description |
|---|---|---|
| Today JSON Hourly Prices EUR⁄kWh | sensor.eprices_today_json_hourly_prices_eur_kwh |
JSON array of 24 hourly averages; "" when no data |
| Today JSON 15-Min Prices EUR⁄kWh (P1 00:00-07:45) | sensor.eprices_today_json_15_min_prices_eur_kwh_p1_00_00_07_45 |
JSON array of 32 prices; "" when no data |
| Today JSON 15-Min Prices EUR⁄kWh (P2 08:00-15:45) | sensor.eprices_today_json_15_min_prices_eur_kwh_p2_08_00_15_45 |
JSON array of 32 prices; "" when no data |
| Today JSON 15-Min Prices EUR⁄kWh (P3 16:00-23:45) | sensor.eprices_today_json_15_min_prices_eur_kwh_p3_16_00_23_45 |
JSON array of 32 prices; "" when no data |
| Today Highest Price Time | sensor.eprices_today_highest_price_time |
Time of highest 15-min price (HH:MM) |
| Today Lowest Price Time | sensor.eprices_today_lowest_price_time |
Time of lowest 15-min price (HH:MM) |
| Today Highest Hourly Price Time | sensor.eprices_today_highest_hourly_price_time |
Time of highest hourly average (HH:00) |
| Today Lowest Hourly Price Time | sensor.eprices_today_lowest_hourly_price_time |
Time of lowest hourly average (HH:00) |
| Today Current Price Status | sensor.eprices_today_current_price_status |
Valid / Missing / Stale |
| Today Data Loaded Time | sensor.eprices_today_data_loaded_time |
Last time today data entered memory (NVS or HTTP) |
Tomorrow — numeric
| Sensor | Entity ID | Description |
|---|---|---|
| Tomorrow Current Price | sensor.eprices_tomorrow_current_price |
Tomorrow at same local time as now |
| Tomorrow Next Price | sensor.eprices_tomorrow_next_price |
Tomorrow next 15-min slot |
| Tomorrow Average Price | sensor.eprices_tomorrow_average_price |
Average of all hourly prices €/kWh |
| Tomorrow Highest Price | sensor.eprices_tomorrow_highest_price |
Highest 15-min price €/kWh |
| Tomorrow Lowest Price | sensor.eprices_tomorrow_lowest_price |
Lowest 15-min price €/kWh |
| Tomorrow Current Hourly Price | sensor.eprices_tomorrow_current_hourly_price |
Tomorrow same hour average €/kWh |
| Tomorrow Next Hourly Price | sensor.eprices_tomorrow_next_hourly_price |
Tomorrow next hour average €/kWh |
| Tomorrow Highest Hourly Price | sensor.eprices_tomorrow_highest_hourly_price |
Highest hourly average €/kWh |
| Tomorrow Lowest Hourly Price | sensor.eprices_tomorrow_lowest_hourly_price |
Lowest hourly average €/kWh |
| Tomorrow Current Max Hourly Price Percentage | sensor.eprices_tomorrow_current_max_hourly_price_percentage |
Same-time hour as % of tomorrow max |
Tomorrow — text
| Sensor | Entity ID | Description |
|---|---|---|
| Tomorrow JSON Hourly Prices EUR⁄kWh | sensor.eprices_tomorrow_json_hourly_prices_eur_kwh |
JSON array of 24 hourly averages; "" when no data |
| Tomorrow JSON 15-Min Prices EUR⁄kWh (P1 00:00-07:45) | sensor.eprices_tomorrow_json_15_min_prices_eur_kwh_p1_00_00_07_45 |
JSON array of 32 prices; "" when no data |
| Tomorrow JSON 15-Min Prices EUR⁄kWh (P2 08:00-15:45) | sensor.eprices_tomorrow_json_15_min_prices_eur_kwh_p2_08_00_15_45 |
JSON array of 32 prices; "" when no data |
| Tomorrow JSON 15-Min Prices EUR⁄kWh (P3 16:00-23:45) | sensor.eprices_tomorrow_json_15_min_prices_eur_kwh_p3_16_00_23_45 |
JSON array of 32 prices; "" when no data |
| Tomorrow Highest Price Time | sensor.eprices_tomorrow_highest_price_time |
Time of highest 15-min price (HH:MM) |
| Tomorrow Lowest Price Time | sensor.eprices_tomorrow_lowest_price_time |
Time of lowest 15-min price (HH:MM) |
| Tomorrow Highest Hourly Price Time | sensor.eprices_tomorrow_highest_hourly_price_time |
Time of highest hourly average (HH:00) |
| Tomorrow Lowest Hourly Price Time | sensor.eprices_tomorrow_lowest_hourly_price_time |
Time of lowest hourly average (HH:00) |
| Tomorrow Current Price Status | sensor.eprices_tomorrow_current_price_status |
Valid / Missing / Waiting... |
| Tomorrow Data Loaded Time | sensor.eprices_tomorrow_data_loaded_time |
Last time tomorrow data entered memory; Outside fetch window before 13:20 |
Diagnostic
| Sensor | Entity ID | Description |
|---|---|---|
| Last Reboot | sensor.eprices_last_reboot |
Timestamp of last device boot |
| Uptime | sensor.eprices_uptime |
Human-readable bucketed: < 1 hour / > N hours / > N days / > N months / > N year(s) N month(s) — one logbook entry per hour, day, or month |
| WiFi Signal | sensor.eprices_wifi_signal |
RSSI in dBm, updated every 60s |
| Last Update Source | sensor.eprices_last_update_source |
NVS_boot / HTTP_today / midnight_bridge / NVS_api etc. |
| Today Data Date | sensor.eprices_today_data_date |
Date of currently stored today data |
| Tomorrow Data Date | sensor.eprices_tomorrow_data_date |
Date of currently stored tomorrow data |
| Today Entry Count | sensor.eprices_today_entry_count |
Number of price points stored for today |
| Tomorrow Entry Count | sensor.eprices_tomorrow_entry_count |
Number of price points stored for tomorrow |
| Today NVS Status | sensor.eprices_today_nvs_status |
NVS load/store result for today |
| Tomorrow NVS Status | sensor.eprices_tomorrow_nvs_status |
NVS load/store result for tomorrow |
| Today Last API Fetch Time | sensor.eprices_today_last_api_fetch_time |
Timestamp of last successful HTTP fetch for today; Never if NVS only |
| Tomorrow Last API Fetch Time | sensor.eprices_tomorrow_last_api_fetch_time |
Timestamp of last successful HTTP fetch for tomorrow; Never if NVS only |
| Today API Fetch Attempts | sensor.eprices_today_api_fetch_attempts |
HTTP fetch attempt count for today; resets at midnight |
| Tomorrow API Fetch Attempts | sensor.eprices_tomorrow_api_fetch_attempts |
HTTP fetch attempt count for tomorrow; resets at 13:25 |
| Today Price Update Status | sensor.eprices_today_price_update_status |
SUCCESS / FAILED/WAITING |
| Tomorrow Price Update Status | sensor.eprices_tomorrow_price_update_status |
SUCCESS / FAILED/WAITING |
| Today Price Update Status Message | sensor.eprices_today_price_update_status_message |
Detailed status string |
| Tomorrow Price Update Status Message | sensor.eprices_tomorrow_price_update_status_message |
Detailed status string; "Fetch timeout – will retry" after watchdog trigger |
Buttons
| Button | Entity ID | Description |
|---|---|---|
| Force Today's Update | button.eprices_force_today_s_update |
Trigger immediate today HTTP fetch |
| Force Tomorrow's Update | button.eprices_force_tomorrow_s_update |
Trigger immediate tomorrow fetch (window: 13:20–23:50) |
| Reboot Device | button.eprices_reboot_device |
Restart the ESP32 |
Migrating from entso-e-prices
If you are migrating from the predecessor project entso-e-prices v4.3.1, see ENTSO-E-PRICES-MIGRATION.md for the complete rationale, all sensor rename mappings, and a migration checklist.
Data attribution
Electricity price data provided by Energy-Charts (Fraunhofer ISE) via the Energy-Charts API, licensed under CC BY 4.0.
License
This project (ESPHome YAML and C++ helper) is released under the MIT License.
