10 Commits
Author SHA1 Message Date
Amir 7cd38a500a Enhance README with stuck-fetch watchdog details (v1.2)
Added explanation for stuck-fetch watchdog mechanism and updated status message details.
2026-04-06 18:44:48 +02:00
Amir a2ad24c017 Document version 1.2 release notes in VERSION.md
Added version 1.2 release notes including fixes for HTTP fetch reliability, tomorrow auto-retry count, and status message improvements.
2026-04-06 18:42:21 +02:00
Amir 007e31ae69 Update changelog for v1.2 enhancements and fixes
Added a watchdog for HTTP fetch timeout, fixed tomorrow retry count, and improved status messages.
2026-04-06 18:41:33 +02:00
Amir 84f24700b8 Upgrade EPrices to version 1.2 with new features
Updated version from 1.1 to 1.2 with new features including HTTP fetch watchdog, new globals for tracking updates, and improved status messages.
2026-04-06 18:40:10 +02:00
Amir 7faaaceed0 Update README with fee configuration details (v1.1)
Clarify configuration for provider fees and VAT rates:
negative price provider fee support added in EPrices v1.1
2026-04-06 03:27:38 +02:00
Amir 6f3ba88394 Add negative price provider fee support in VERSION.md
Introduced support for negative provider fees with a new secret key and updated price calculation methods for both positive and negative prices.
2026-04-06 03:19:58 +02:00
Amir 134cb7bbb1 Add negative price provider fee support in v1.1
Added support for negative price provider fees, configurable via secrets.yaml. Updated price calculation formulas and modified eprices.yaml for new global and substitutions.
2026-04-06 03:18:17 +02:00
Amir ee48ef13f4 Update secrets.yaml with negative price fee multiplier
Added provider fee multiplier for negative spot prices.
2026-04-06 03:15:59 +02:00
Amir a09de2a71b Upgrade EPrices to version 1.1 with new features
Updated version to 1.1 with new features including separate provider fee for negative prices and new substitutions. Adjusted calculations for electricity prices based on the new fee structure.
2026-04-06 03:14:23 +02:00
Amir fa507ff8ef Add files via upload 2026-04-05 21:48:35 +02:00
5 changed files with 390 additions and 31 deletions
+112 -1
View File
@@ -1,4 +1,115 @@
# EPrices v1.0 Changelog # EPrices Changelog
## v1.2 — 2026-04-06
### HTTP fetch stuck-flag watchdog
Fixed a reliability issue where an HTTP fetch that stalled at the TCP level
(connection accepted by the server but data delivery suspended) could hold
`is_updating_today` or `is_updating_tomorrow` locked at `true` indefinitely.
While the flag was stuck, all subsequent auto-retry triggers and manual button
presses were silently blocked.
**Root cause:** The application-level 25 s HTTP timeout does not fire when the
TCP connection is established but the server stops sending data mid-transfer.
The socket never goes idle from the OS perspective, so the timeout never
triggers, and the fetch hangs until the TCP stack eventually resets the
connection — which can take several minutes.
**Fix:** Added a 120-second watchdog to both the today and tomorrow worker
loops (`seconds: /10`). If `is_updating_*` has been `true` for more than 120
seconds, the worker force-clears the flag and sets the status message to
`"Fetch timeout will retry"`. The next scheduled retry or manual press then
proceeds normally.
**New globals:**
- `is_updating_today_since` (`time_t`) — timestamp when `is_updating_today` was last set to `true`
- `is_updating_tomorrow_since` (`time_t`) — timestamp when `is_updating_tomorrow` was last set to `true`
Both timestamps are set immediately alongside `is_updating_* = true` inside
`smart_price_update` and `smart_tomorrow_price_update`.
**Changed locations in `eprices.yaml`:**
- `globals:` — added `is_updating_today_since` and `is_updating_tomorrow_since`
- `smart_price_update` script — added `id(is_updating_today_since) = ...` alongside flag set
- `smart_tomorrow_price_update` script — added `id(is_updating_tomorrow_since) = ...` alongside flag set
- Today worker (`seconds: /10`) — watchdog block added before existing worker logic
- Tomorrow worker (`seconds: /10`) — watchdog block added before existing worker logic
---
### Tomorrow auto-retry attempt counter fix
Fixed a bug where `tomorrow_retry_count` was not incremented in the 13:55 and
14:5519:55 hourly retry triggers. Those triggers called
`smart_tomorrow_price_update` directly without incrementing the counter first,
causing the `Tomorrow API Fetch Attempts` diagnostic sensor to undercount
attempts after the first one at 13:25.
**Changed locations in `eprices.yaml`:**
- 13:55 trigger — added `id(tomorrow_retry_count)++` before `.execute()`
- 14:5519:55 hourly trigger — added `id(tomorrow_retry_count)++` before `.execute()`
---
### Status message improvements
Two status message strings that contained a hardcoded time reference have been
corrected. Both were misleading after a device reboot in the afternoon or a
manual `clear_tomorrow_prices` call outside the normal fetch window.
| Location | Old value | New value |
|---|---|---|
| `tomorrow_update_status_message` global `initial_value` | `"Waiting for 13:20"` | `"No data yet"` |
| `clear_tomorrow_prices` script end | `"Cleared awaiting next 13:20 window"` | `"Cleared awaiting fetch window"` |
---
## v1.1 — 2026-04-06
### Negative price provider fee support
Added a separate provider fee multiplier for negative spot prices, configurable
via `secrets.yaml`. This correctly models contracts where the provider fee
structure differs between positive and negative market prices.
**New secret key:**
```yaml
eprices_neg_prov_fee: "0.30"
```
**New global:** `neg_prov_fee` (type: `double`)
**Price calculation is now:**
| Market price | Formula |
|---|---|
| Positive (`raw_mwh >= 0`) | `(raw / 1000) × (1 + prov_fee) × (1 + vat_rate)` |
| Negative (`raw_mwh < 0`) | `(raw / 1000) × (1 - neg_prov_fee) × (1 + vat_rate)` |
**Key values for `eprices_neg_prov_fee`:**
| Value | Meaning |
|---|---|
| `"0.30"` | Provider keeps 30%, pays you 70% of negative price |
| `"0.00"` | Provider passes full negative price to you (no fee) |
**Switch point:** raw API price (`raw_mwh`) before any multiplier is applied.
**VAT:** applied symmetrically to both positive and negative prices, consistent
with net billing models where VAT is calculated on the monthly net sum
(mathematically equivalent due to VAT being a linear multiplier).
**Changed files:** `eprices.yaml`, `secrets.yaml`
**Changed locations in `eprices.yaml`:**
- `substitutions:` — added `neg_prov_fee_value`
- `globals:` — added `neg_prov_fee`
- `parse_energy_charts_today_script``MULT` split into `MULT_POS` / `MULT_NEG`
- `parse_energy_charts_tomorrow_script``MULT` split into `MULT_POS` / `MULT_NEG`
---
## v1.0 — 2026-04-05 ## v1.0 — 2026-04-05
+38 -11
View File
@@ -16,19 +16,21 @@ Assistant automations are required for any core functionality.
## Features ## Features
- Fetches **15-minute resolution** spot prices for **today** and **tomorrow** - Fetches **15-minute resolution** spot prices for **today** and **tomorrow**
- Automatically applies your **provider fee** and **VAT rate** to raw €/MWh prices - 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 - Exposes **96 per-day price points** plus **24-hour averages** as JSON text sensors
for use in HA automations and dashboards for use in HA automations and dashboards
- **NVS persistence** — prices survive device reboots without re-fetching - **NVS persistence** — prices survive device reboots without re-fetching
- **Midnight bridge** — tomorrow's data automatically becomes today at 00:00, fully on-device - **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 - **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
- **DST-safe** — uses UNIX timestamps and binary search throughout, no hour-slot arithmetic - **DST-safe** — uses UNIX timestamps and binary search throughout, no hour-slot arithmetic
- **Staleness detection** — `Today Current Price Status` shows `Stale` if stored date mismatches today - **Staleness detection** — `Today Current Price Status` shows `Stale` if stored date mismatches today
- **Tomorrow live sensors** evaluate at `now + 86400s` — reflecting tomorrow at the same local time - **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) - 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, - Full **diagnostic sensor suite** — NVS status, fetch attempts, API fetch times,
data loaded times, WiFi signal, human-readable uptime data loaded times, WiFi signal, human-readable uptime
- Provider fee and VAT rate configurable via `secrets.yaml`**no code changes needed** - All fee and VAT settings configurable via `secrets.yaml`**no code changes needed**
--- ---
@@ -46,7 +48,7 @@ Assistant automations are required for any core functionality.
| `eprices.yaml` | Main ESPHome configuration | | `eprices.yaml` | Main ESPHome configuration |
| `eprices_nvs.h` | NVS helper — save/load price arrays to ESP32 flash | | `eprices_nvs.h` | NVS helper — save/load price arrays to ESP32 flash |
| `secrets.yaml` | Your local secrets (not committed to git) | | `secrets.yaml` | Your local secrets (not committed to git) |
| `CHANGELOG.md` | Complete sensor and entity ID reference for v1.0 | | `CHANGELOG.md` | Complete sensor and entity ID reference |
| `VERSION.md` | Version history and release notes | | `VERSION.md` | Version history and release notes |
| `ENTSO-E-PRICES-MIGRATION.md` | Optional — migration guide from the predecessor project | | `ENTSO-E-PRICES-MIGRATION.md` | Optional — migration guide from the predecessor project |
@@ -71,6 +73,7 @@ eprices_timezone: "Europe/Ljubljana"
eprices_country_bzn: "SI" eprices_country_bzn: "SI"
eprices_prov_fee: "0.12" eprices_prov_fee: "0.12"
eprices_vat_rate: "0.22" eprices_vat_rate: "0.22"
eprices_neg_prov_fee: "0.30"
``` ```
**Key descriptions:** **Key descriptions:**
@@ -79,8 +82,9 @@ eprices_vat_rate: "0.22"
|---|---| |---|---|
| `eprices_timezone` | Your local timezone — must match a valid [IANA tz name](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) | | `eprices_timezone` | Your local timezone — must match a valid [IANA tz name](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) |
| `eprices_country_bzn` | Energy-Charts bidding zone code — see [supported zones](#supported-bidding-zones) | | `eprices_country_bzn` | Energy-Charts bidding zone code — see [supported zones](#supported-bidding-zones) |
| `eprices_prov_fee` | Provider fee as a decimal multiplier, e.g. `"0.12"` = 12% | | `eprices_prov_fee` | Provider fee for **positive** spot prices — decimal multiplier, e.g. `"0.12"` = 12% |
| `eprices_vat_rate` | VAT rate as a decimal multiplier, e.g. `"0.22"` = 22% | | `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` | | `eprices_api_encryption_key` | 32-byte base64 key for HA API encryption — generate with `openssl rand -base64 32` |
### 3) Flash to your ESP32 ### 3) Flash to your ESP32
@@ -130,14 +134,37 @@ For the full list see the [Energy-Charts API documentation](https://api.energy-c
| 13:55, 14:55 … 19:55 | Retry tomorrow fetch if previous attempt failed (max 8 total) | | 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 | | 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 ### Price calculation
Raw prices from the API are in **€/MWh**. EPrices converts them to **€/kWh** Raw prices from the API are in **€/MWh**. EPrices converts them to **€/kWh**
and applies your provider fee and VAT: and applies your provider fee and VAT. Positive and negative spot prices use
separate fee multipliers, configurable independently in `secrets.yaml`:
``` | Market price | Formula |
price_eur_kwh = (raw_eur_mwh / 1000) × (1 + prov_fee) × (1 + vat_rate) |---|---|
``` | 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 ### Negative prices
@@ -245,7 +272,7 @@ safe on DST transition days (23-hour and 25-hour days).
| Today Price Update Status | `sensor.eprices_today_price_update_status` | `SUCCESS` / `FAILED/WAITING` | | 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` | | 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 | | 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 | | Tomorrow Price Update Status Message | `sensor.eprices_tomorrow_price_update_status_message` | Detailed status string; `"Fetch timeout will retry"` after watchdog trigger |
### Buttons ### Buttons
+55
View File
@@ -1,5 +1,60 @@
# EPrices Version History # EPrices Version History
## v1.2 — 2026-04-06
### HTTP fetch stuck-flag watchdog
Fixed a production-observed reliability issue where a TCP-level stall during
an HTTP fetch could lock `is_updating_today` or `is_updating_tomorrow` at
`true` for several minutes, silently blocking all auto-retry triggers and
manual button presses for the duration.
A 120-second watchdog was added to both worker loops. If either `is_updating_*`
flag has been held for more than 120 seconds, the worker force-clears it and
sets the status message to `"Fetch timeout will retry"`, allowing the next
scheduled retry or manual press to proceed immediately.
New globals: `is_updating_today_since`, `is_updating_tomorrow_since`.
### Tomorrow auto-retry attempt counter fix
`tomorrow_retry_count` was not being incremented in the 13:55 and 14:5519:55
hourly retry triggers, causing the `Tomorrow API Fetch Attempts` diagnostic
sensor to undercount after the first scheduled attempt at 13:25.
### Status message improvements
- `tomorrow_update_status_message` initial value changed from
`"Waiting for 13:20"` to `"No data yet"` — removes misleading time
reference shown after afternoon reboots
- `clear_tomorrow_prices` end message changed from
`"Cleared awaiting next 13:20 window"` to
`"Cleared awaiting fetch window"`
See `CHANGELOG.md` for full implementation details.
---
## v1.1 — 2026-04-06
### Negative price provider fee
Added separate provider fee support for negative spot prices via a new
`eprices_neg_prov_fee` secret key. Positive and negative market prices
now use independent fee multipliers, correctly modelling contracts where
the provider's fee structure differs between the two cases.
Price calculation:
- Positive: `(raw / 1000) × (1 + prov_fee) × (1 + vat_rate)`
- Negative: `(raw / 1000) × (1 - neg_prov_fee) × (1 + vat_rate)`
VAT is applied to both, consistent with net billing where VAT is calculated
on the monthly net sum (linear equivalence applies).
See `CHANGELOG.md` for full implementation details.
---
## v1.0 — 2026-04-05 ## v1.0 — 2026-04-05
First public release. First public release.
+75 -16
View File
@@ -1,13 +1,37 @@
# ============================================================================= # =============================================================================
# ESPHome: EPrices v1.0 # ESPHome: EPrices v1.2
#
# ESP32 firmware that fetches day-ahead electricity spot prices from the
# public Energy-Charts API (https://api.energy-charts.info) and exposes
# them as Home Assistant sensors. No API token required.
# #
# Electricity price data provided by Energy-Charts (https://energy-charts.info) # Electricity price data provided by Energy-Charts (https://energy-charts.info)
# under CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/). # under CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/).
# #
# v1.2 changes (2026-04-06):
# - HTTP fetch stuck-flag watchdog (120 s) added to today and tomorrow workers:
# if is_updating_today / is_updating_tomorrow stays true for >120 s (TCP
# stall not caught by app-level timeout), the flag is force-cleared and
# status message set to "Fetch timeout will retry"
# - New globals: is_updating_today_since, is_updating_tomorrow_since
# set alongside is_updating_* = true in smart_price_update and
# smart_tomorrow_price_update
# - tomorrow_retry_count++ added before .execute() in 13:55 and 14:5519:55
# hourly retry triggers (counter was not incrementing after attempt #1)
# - tomorrow_update_status_message initial_value: "Waiting for 13:20"
# → "No data yet" (removes misleading time reference after afternoon reboot)
# - clear_tomorrow_prices end message: "Cleared awaiting next 13:20 window"
# → "Cleared awaiting fetch window"
#
# v1.1 changes (2026-04-06):
# - Separate provider fee for negative spot prices (eprices_neg_prov_fee):
# positive prices: (raw / 1000) × (1 + prov_fee) × (1 + vat_rate)
# negative prices: (raw / 1000) × (1 - neg_prov_fee) × (1 + vat_rate)
# switch happens on raw API price before any multiplier is applied
# VAT applied to both cases (consistent with net billing + end-of-month VAT)
# 0.30 = provider keeps 30%, pays you 70% of negative price
# 0.00 = provider passes full negative price to you (no fee)
# - New substitution: neg_prov_fee_value (!secret eprices_neg_prov_fee)
# - New global: neg_prov_fee (double)
# - MULT split into MULT_POS / MULT_NEG in parse_energy_charts_today_script
# and parse_energy_charts_tomorrow_script
#
# v1.0 release notes: # v1.0 release notes:
# - Fetches 15-min and hourly spot prices for today and tomorrow # - Fetches 15-min and hourly spot prices for today and tomorrow
# - Prices converted from raw €/MWh to €/kWh with provider fee and VAT applied # - Prices converted from raw €/MWh to €/kWh with provider fee and VAT applied
@@ -33,6 +57,7 @@
# eprices_country_bzn e.g. "SI" # eprices_country_bzn e.g. "SI"
# eprices_prov_fee e.g. "0.12" (provider fee, decimal multiplier) # eprices_prov_fee e.g. "0.12" (provider fee, decimal multiplier)
# eprices_vat_rate e.g. "0.22" (VAT rate, decimal multiplier) # eprices_vat_rate e.g. "0.22" (VAT rate, decimal multiplier)
# eprices_neg_prov_fee e.g. "0.30" (neg price provider fee, decimal multiplier)
# #
# See README.md for full installation instructions and sensor reference. # See README.md for full installation instructions and sensor reference.
# See CHANGELOG.md for complete sensor and entity ID listing. # See CHANGELOG.md for complete sensor and entity ID listing.
@@ -40,9 +65,10 @@
# ============================================================================= # =============================================================================
substitutions: substitutions:
country_bzn_value: !secret eprices_country_bzn country_bzn_value: !secret eprices_country_bzn
prov_fee_value: !secret eprices_prov_fee prov_fee_value: !secret eprices_prov_fee
vat_rate_value: !secret eprices_vat_rate vat_rate_value: !secret eprices_vat_rate
neg_prov_fee_value: !secret eprices_neg_prov_fee
esphome: esphome:
name: eprices name: eprices
@@ -175,6 +201,15 @@ time:
- seconds: /10 - seconds: /10
then: then:
- lambda: |- - lambda: |-
if (id(is_updating_today) && id(is_updating_today_since) > 0) {
time_t now_ts = (time_t)id(ha_time).now().timestamp;
if (now_ts - id(is_updating_today_since) > 120) {
ESP_LOGW("eprices", "Worker: is_updating_today stuck >120s, force-clearing");
id(is_updating_today) = false;
id(is_updating_today_since) = 0;
id(update_status_message) = "Fetch timeout will retry";
}
}
if (id(need_today_update) && !id(is_updating_today)) { if (id(need_today_update) && !id(is_updating_today)) {
ESP_LOGI("eprices", "Worker: starting Today update"); ESP_LOGI("eprices", "Worker: starting Today update");
id(need_today_update) = false; id(need_today_update) = false;
@@ -184,6 +219,15 @@ time:
- seconds: /10 - seconds: /10
then: then:
- lambda: |- - lambda: |-
if (id(is_updating_tomorrow) && id(is_updating_tomorrow_since) > 0) {
time_t now_ts = (time_t)id(ha_time).now().timestamp;
if (now_ts - id(is_updating_tomorrow_since) > 120) {
ESP_LOGW("eprices", "Worker: is_updating_tomorrow stuck >120s, force-clearing");
id(is_updating_tomorrow) = false;
id(is_updating_tomorrow_since) = 0;
id(tomorrow_update_status_message) = "Fetch timeout will retry";
}
}
auto t = id(ha_time).now(); auto t = id(ha_time).now();
bool in_window = (t.hour > 13 && t.hour < 23) || bool in_window = (t.hour > 13 && t.hour < 23) ||
(t.hour == 13 && t.minute >= 20) || (t.hour == 13 && t.minute >= 20) ||
@@ -312,7 +356,8 @@ time:
return; return;
} }
ESP_LOGW("eprices", "Auto tomorrow: attempt #%d at 13:55", id(tomorrow_retry_count) + 1); id(tomorrow_retry_count)++;
ESP_LOGW("eprices", "Auto tomorrow: attempt #%d at 13:55", id(tomorrow_retry_count));
id(smart_tomorrow_price_update).execute(); id(smart_tomorrow_price_update).execute();
# Attempts #3.. at 14:55, 15:55, 16:55, 17:55, 18:55, 19:55 (capped to 8) # Attempts #3.. at 14:55, 15:55, 16:55, 17:55, 18:55, 19:55 (capped to 8)
@@ -333,7 +378,8 @@ time:
return; return;
} }
ESP_LOGW("eprices", "Auto tomorrow: attempt #%d at %02d:55", id(tomorrow_retry_count) + 1, t.hour); id(tomorrow_retry_count)++;
ESP_LOGW("eprices", "Auto tomorrow: attempt #%d at %02d:55", id(tomorrow_retry_count), t.hour);
id(smart_tomorrow_price_update).execute(); id(smart_tomorrow_price_update).execute();
- minutes: /1 - minutes: /1
@@ -452,6 +498,9 @@ globals:
- id: is_updating_today - id: is_updating_today
type: bool type: bool
initial_value: 'false' initial_value: 'false'
- id: is_updating_today_since
type: time_t
initial_value: '0'
- id: tomorrow_last_successful_update - id: tomorrow_last_successful_update
type: time_t type: time_t
@@ -468,7 +517,7 @@ globals:
initial_value: 'false' initial_value: 'false'
- id: tomorrow_update_status_message - id: tomorrow_update_status_message
type: std::string type: std::string
initial_value: '"Waiting for 13:20"' initial_value: '"No data yet"'
- id: tomorrow_retry_count - id: tomorrow_retry_count
type: int type: int
initial_value: '0' initial_value: '0'
@@ -484,6 +533,9 @@ globals:
- id: is_updating_tomorrow - id: is_updating_tomorrow
type: bool type: bool
initial_value: 'false' initial_value: 'false'
- id: is_updating_tomorrow_since
type: time_t
initial_value: '0'
# --- Country code for Energy-Charts API (wired from secret) --- # --- Country code for Energy-Charts API (wired from secret) ---
- id: country_bzn - id: country_bzn
@@ -497,6 +549,9 @@ globals:
- id: vat_rate - id: vat_rate
type: double type: double
initial_value: '${vat_rate_value}' initial_value: '${vat_rate_value}'
- id: neg_prov_fee
type: double
initial_value: '${neg_prov_fee_value}'
# --- Pre-computed fetch URLs (url: !lambda not supported in scripts) --- # --- Pre-computed fetch URLs (url: !lambda not supported in scripts) ---
- id: fetch_url_today - id: fetch_url_today
@@ -1261,7 +1316,8 @@ script:
raw_body: string raw_body: string
then: then:
- lambda: |- - lambda: |-
const double MULT = (1.0 + id(prov_fee)) * (1.0 + id(vat_rate)); const double MULT_POS = (1.0 + id(prov_fee)) * (1.0 + id(vat_rate));
const double MULT_NEG = (1.0 - id(neg_prov_fee)) * (1.0 + id(vat_rate));
id(price_values_today).clear(); id(price_values_today).clear();
id(price_timestamps_today).clear(); id(price_timestamps_today).clear();
@@ -1350,7 +1406,7 @@ script:
if (end1 == ts_toks[i].c_str() || end2 == prc_toks[i].c_str()) continue; if (end1 == ts_toks[i].c_str() || end2 == prc_toks[i].c_str()) continue;
if (unix_ts <= 0) continue; if (unix_ts <= 0) continue;
float eur_kwh = (float)((double)raw_mwh * MULT / 1000.0); float eur_kwh = (float)((double)raw_mwh * (raw_mwh >= 0 ? MULT_POS : MULT_NEG) / 1000.0);
id(price_timestamps_today).push_back(unix_ts); id(price_timestamps_today).push_back(unix_ts);
id(price_values_today).push_back(eur_kwh); id(price_values_today).push_back(eur_kwh);
} }
@@ -1369,7 +1425,8 @@ script:
raw_body: string raw_body: string
then: then:
- lambda: |- - lambda: |-
const double MULT = (1.0 + id(prov_fee)) * (1.0 + id(vat_rate)); const double MULT_POS = (1.0 + id(prov_fee)) * (1.0 + id(vat_rate));
const double MULT_NEG = (1.0 - id(neg_prov_fee)) * (1.0 + id(vat_rate));
id(price_values_tomorrow).clear(); id(price_values_tomorrow).clear();
id(price_timestamps_tomorrow).clear(); id(price_timestamps_tomorrow).clear();
@@ -1437,7 +1494,7 @@ script:
if (end1 == ts_toks[i].c_str() || end2 == prc_toks[i].c_str()) continue; if (end1 == ts_toks[i].c_str() || end2 == prc_toks[i].c_str()) continue;
if (unix_ts <= 0) continue; if (unix_ts <= 0) continue;
float eur_kwh = (float)((double)raw_mwh * MULT / 1000.0); float eur_kwh = (float)((double)raw_mwh * (raw_mwh >= 0 ? MULT_POS : MULT_NEG) / 1000.0);
id(price_timestamps_tomorrow).push_back(unix_ts); id(price_timestamps_tomorrow).push_back(unix_ts);
id(price_values_tomorrow).push_back(eur_kwh); id(price_values_tomorrow).push_back(eur_kwh);
} }
@@ -1754,6 +1811,7 @@ script:
return; return;
} }
id(is_updating_today) = true; id(is_updating_today) = true;
id(is_updating_today_since) = (time_t)id(ha_time).now().timestamp;
id(last_update_attempt) = id(ha_time).now().timestamp; id(last_update_attempt) = id(ha_time).now().timestamp;
id(retry_count)++; id(retry_count)++;
{ char buf[8]; snprintf(buf, sizeof(buf), "%d", id(retry_count)); id(today_price_update_attempts).publish_state(buf); } { char buf[8]; snprintf(buf, sizeof(buf), "%d", id(retry_count)); id(today_price_update_attempts).publish_state(buf); }
@@ -1857,6 +1915,7 @@ script:
return; return;
} }
id(is_updating_tomorrow) = true; id(is_updating_tomorrow) = true;
id(is_updating_tomorrow_since) = (time_t)id(ha_time).now().timestamp;
id(tomorrow_last_update_attempt) = id(ha_time).now().timestamp; id(tomorrow_last_update_attempt) = id(ha_time).now().timestamp;
id(tomorrow_retry_count)++; id(tomorrow_retry_count)++;
{ char buf[8]; snprintf(buf, sizeof(buf), "%d", id(tomorrow_retry_count)); id(tomorrow_price_update_attempts).publish_state(buf); } { char buf[8]; snprintf(buf, sizeof(buf), "%d", id(tomorrow_retry_count)); id(tomorrow_price_update_attempts).publish_state(buf); }
@@ -2086,7 +2145,7 @@ script:
id(tomorrow_min_hourly_price_time_str) = "--:--"; id(tomorrow_min_hourly_price_time_str) = "--:--";
id(tomorrow_max_hourly_price_time_str) = "--:--"; id(tomorrow_max_hourly_price_time_str) = "--:--";
id(tomorrow_last_update_success) = false; id(tomorrow_last_update_success) = false;
id(tomorrow_update_status_message) = "Cleared awaiting next 13:20 window"; id(tomorrow_update_status_message) = "Cleared awaiting fetch window";
id(tomorrow_current_price_status_str) = "Cleared"; id(tomorrow_current_price_status_str) = "Cleared";
id(tomorrow_price_update_status).update(); id(tomorrow_price_update_status).update();
id(tomorrow_price_status_message).update(); id(tomorrow_price_status_message).update();
+107
View File
@@ -0,0 +1,107 @@
# =============================================================================
# EPrices Template secrets file
# Copy this file to your ESPHome config directory as "secrets.yaml"
# and fill in your own values.
#
# Notes on value formats:
# - All values are strings in secrets.yaml.
# - Numeric values (prov_fee, vat_rate) must use a DECIMAL DOT (e.g. 0.12),
# not a decimal comma. They are loaded as substitutions and passed directly
# into C++ double globals, so the dot is mandatory.
# =============================================================================
# --- WiFi credentials (shared with other ESPHome devices - remove if already present) ---
wifi_ssid: "your_wifi_ssid"
wifi_password: "your_wifi_password"
# --- EPrices fallback AP (shown when WiFi is unavailable) ---
eprices_fallback_ap_ssid: "EPrices-Fallback"
eprices_fallback_ap_password: "your_fallback_ap_password"
# --- ESPHome API encryption key (generate with: esphome generate-api-key) ---
eprices_api_encryption_key: "your_base64_api_encryption_key_here"
# --- Timezone string (IANA format, must match your location) ---
# Examples: "Europe/Ljubljana", "Europe/Berlin", "Europe/London"
eprices_timezone: "Europe/Ljubljana"
# --- Bidding zone for Energy-Charts API ---
# This determines which country's spot prices are fetched.
# Examples: "SI" (Slovenia), "DE-LU" (Germany/Luxembourg), "AT" (Austria),
# "FR" (France), "IT-North" (Italy North), "HR" (Croatia)
# Full list: https://api.energy-charts.info/
eprices_country_bzn: "SI"
# --- Provider fee multiplier for POSITIVE spot prices ---
# Added on top of the raw spot price to account for your energy provider's fee.
# Value is a DECIMAL MULTIPLIER, not a percentage.
# Example: 0.12 means 12% provider fee added to the spot price.
# Use decimal DOT, not comma (correct: 0.12 | wrong: 0,12).
eprices_prov_fee: "0.12"
# --- Provider fee multiplier for NEGATIVE spot prices ---
# When the market spot price is negative, the provider pays you a share of
# the negative price. This value is the fraction the provider keeps.
# Example: 0.30 means provider keeps 30%, pays you 70% of the negative price.
# Example: 0.00 means provider passes the full negative price to you (no fee).
# Use decimal DOT, not comma (correct: 0.30 | wrong: 0,30).
eprices_neg_prov_fee: "0.30"
# --- VAT rate multiplier ---
# Value added to the price to account for Value Added Tax in your country.
# Value is a DECIMAL MULTIPLIER, not a percentage.
# Example: 0.22 means 22% VAT (Slovenia). Germany is 0.19, Austria is 0.20.
# Use decimal DOT, not comma (correct: 0.22 | wrong: 0,22).
eprices_vat_rate: "0.22"
# ==============================================================================
# --- ADDITIONAL INFO: Bidding zone list for Energy-Charts API on 2026-04-04 ---
# (Check https://api.energy-charts.info/ for potential future changes)
#
# AT - Austria (click to show all available bidding zones)
# 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
# ==============================================================================