13 Commits
Author SHA1 Message Date
Amir 9d22713eb4 Release v1.2.2 with stability improvements and optimizations
This version includes a stability patch to address spontaneous reboots during price fetch. Key updates include increased ESP32 task watchdog timeout, optimized JSON string building, periodic yield calls, and added heap monitoring logs. Added v1.2.2 section with 4-bullet summary of all changes.
2026-04-28 21:48:13 +02:00
Amir f7b2513b40 Document changes for version 1.2.2
Updated README to reflect changes in task watchdog stability and other features (added new feature bullet).
2026-04-28 21:46:54 +02:00
Amir 344b1e8294 Update changelog for v1.2.2 enhancements
Increased ESP-IDF task watchdog timeout, optimized JSON string building, added periodic yield calls to prevent watchdog triggers, and implemented heap monitoring logs for debugging.  Added complete v1.2.2 section with 4 subsections (watchdog timeout, JSON optimisation, yield() calls, heap monitoring).
2026-04-28 21:44:21 +02:00
Amir 275f7cebbf Bump version to 1.2.2 and add stability fixes
Updated version from 1.2.1 to 1.2.2 with stability fixes and optimizations.
2026-04-28 21:42:03 +02:00
Amir 8ded49eb80 Document changes for version 1.2.1
Updated sensor descriptions to indicate when no data is available.
2026-04-08 01:11:13 +02:00
Amir 420a745129 Document version 1.2.1 release in VERSION.md
Added version 1.2.1 release notes detailing stability improvements, stack size increase, heap pre-allocation, and unified JSON sensor states.
2026-04-08 01:05:56 +02:00
Amir 70c0abcf9e Document changes for version 1.2.1
Updated changelog for version 1.2.1 with detailed changes including stack size increase, pre-allocation of price vectors, and unified JSON sensor states.
2026-04-08 01:05:09 +02:00
Amir 8c68afdc80 Bump version to 1.2.1 and improve price handling
Updated version to 1.2.1 and added changes for price vector pre-allocation and JSON sensor behavior.
2026-04-08 01:01:28 +02:00
Amir 951d99bca1 Update sensor names and migration instructions for EPrices
Renamed sensors and updated entity IDs for EPrices v1.2. Added migration checklist and clarified changes in sensor outputs and NVS data.
2026-04-06 21:50:03 +02:00
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
5 changed files with 700 additions and 143 deletions
+238 -8
View File
@@ -1,5 +1,234 @@
# EPrices Changelog
## v1.2.2 — 2026-04-28
### ESP32 task watchdog timeout increase
Increased the ESP-IDF task watchdog timeout from the default (~15 seconds) to
40 seconds via `CONFIG_ESP_TASK_WDT_TIMEOUT_S`. Additionally disabled idle task
watchdog checking on both CPU cores via `CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0`
and `CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1`.
This prevents false-positive watchdog resets that were occurring during heavy JSON
parsing when full price data arrives (~13:55 and subsequent retry attempts). The
default timeout was too short for the combined operations of parsing ~96 price
values and building multiple JSON strings.
**Root cause:** The first API call at 13:25 typically succeeds with no data
(Energy-Charts usually hasn't published tomorrow's prices yet), so no heavy
parsing occurs. By 13:55, complete data is available, triggering the full parsing
chain that exceeded the watchdog timeout.
**Changed location in `eprices.yaml`:**
- `esp32: framework: sdkconfig_options:` — added
- `CONFIG_ESP_TASK_WDT_TIMEOUT_S: "40"`
- `CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0: n`
- `CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1: n`
---
### JSON string building optimised — eliminated O(n²) heap fragmentation
Replaced the O(n²) string concatenation pattern in `recompute_today` and
`recompute_tomorrow` with pre-allocated fixed-size character buffers using
`snprintf()`. Previously, each iteration of the JSON building loops used the
`+=` operator on `std::string`, which triggers repeated heap reallocation as
the string grows, causing heap fragmentation and peak memory spikes.
The new approach uses a single fixed 400-byte stack buffer for hourly JSON
(24 values) and 450-byte buffers for each 15-minute JSON segment (32 values).
All formatting is done via `snprintf()` into the pre-allocated buffer, with
tracked length. This eliminates all heap allocations during JSON building.
**Before (problematic):**
```cpp
std::string json_h = "[";
for (int i = 0; i < 24; i++) {
// ...
json_h += pb; // Each += may trigger reallocation!
}
```
**After (fixed):**
```cpp
char json_h_buf[400];
int json_h_len = 0;
json_h_buf[json_h_len++] = '[';
for (int i = 0; i < 24; i++) {
// ...
int written = snprintf(json_h_buf + json_h_len, sizeof(json_h_buf) - json_h_len, "%.4f", ha);
json_h_len += written;
}
```
**Changed locations in `eprices.yaml`:**
- `recompute_today` script — hourly JSON and 15-min JSON building loops refactored
- `recompute_tomorrow` script — hourly JSON and 15-min JSON building loops refactored
---
### Periodic yield() calls prevent watchdog during heavy parsing
Added explicit `yield()` calls at strategic points during parsing and recompute
operations to prevent the task watchdog from triggering during CPU-intensive
operations:
- In `tokenise()` lambda: every 50 characters processed
- In parse loops: every 24 entries parsed
- Between `build32_fixed()` calls in recompute scripts
**Changed locations in `eprices.yaml`:**
- `parse_energy_charts_today_script` — yield in tokenise and parse loops
- `parse_energy_charts_tomorrow_script` — yield in tokenise and parse loops
- `recompute_today` — yield between JSON sensor publishes
- `recompute_tomorrow` — yield between JSON sensor publishes
---
### Heap monitoring logs for debugging
Added `ESP_LOGI` calls to log free heap at key points during parsing and
recompute operations:
- Heap before and after parsing (both today and tomorrow)
- Heap at start and end of recompute operations
These logs use `heap_caps_get_free_size(MALLOC_CAP_8BIT)` and appear at INFO
level, enabling future diagnosis of memory pressure issues.
**Changed locations in `eprices.yaml`:**
- `on_boot` lambda — added boot heap log
- `parse_energy_charts_today_script` — heap before/after parsing
- `parse_energy_charts_tomorrow_script` — heap before/after parsing
- `recompute_today` — heap at start/end
- `recompute_tomorrow` — heap at start/end
---
## v1.2.1 — 2026-04-07
### ESP32 main task stack size increase
Increased the FreeRTOS main application task stack from the ESP-IDF default
of 8192 bytes to 16384 bytes. This eliminates the stack overflow scenario
that most likely caused the spontaneous reboot observed in production on
2026-04-06, where the FreeRTOS idle task faulted following memory pressure
during a simultaneous NVS load and HTTP fetch/parse cycle.
The 8 KB cost is well within the ESP32's 320 KB available RAM.
**Changed location in `eprices.yaml`:**
- `esp32: framework: sdkconfig_options:` — added
`CONFIG_ESP_MAIN_TASK_STACK_SIZE: "16384"`
---
### Price vector heap pre-allocation at boot
Pre-allocated capacity for all four price vectors at boot using `.reserve(96)`.
Previously, each vector started empty and grew incrementally on every NVS load
and HTTP fetch, triggering repeated heap reallocations and leaving the heap
fragmented before the first fetch cycle completed.
With capacity reserved upfront, no reallocation occurs during any subsequent
NVS load or HTTP parse operation. This reduces heap fragmentation and peak
allocation spikes, particularly during the boot sequence where NVS load and
the first HTTP fetch can overlap.
**Vectors pre-allocated:**
- `price_timestamps_today`
- `price_values_today`
- `price_timestamps_tomorrow`
- `price_values_tomorrow`
**Changed location in `eprices.yaml`:**
- `on_boot:` lambda — added `.reserve(96)` on all four price vectors
---
### Hourly JSON sensor cleared state unified with 15-min sensors
The two hourly JSON text sensors previously published `"[]"` when cleared
(at midnight bridge and on `clear_today_prices` / `clear_tomorrow_prices`).
The six 15-minute JSON sensors already published `""` in the same situation.
All eight JSON sensors now publish `""` when cleared, giving consistent
behaviour across the full sensor set.
**Affected sensors:**
- `Today JSON Hourly Prices EURkWh`
- `Tomorrow JSON Hourly Prices EURkWh`
**Changed locations in `eprices.yaml`:**
- `clear_today_prices` script — `"[]"``""`
- `clear_tomorrow_prices` script — `"[]"``""`
- `midnight_bridge_promotion` script — `"[]"``""` (where applicable)
---
## 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
@@ -114,10 +343,10 @@ eprices_vat_rate # e.g. "0.22" (VAT rate as decimal multiplier)
| Name | Entity ID | Notes |
|---|---|---|
| Today JSON Hourly Prices EURkWh | `sensor.eprices_today_json_hourly_prices_eur_kwh` | JSON array, 24 values |
| Today JSON 15-Min Prices EURkWh (P1 00:00-07:45) | `sensor.eprices_today_json_15_min_prices_eur_kwh_p1_00_00_07_45` | JSON array, 32 values |
| Today JSON 15-Min Prices EURkWh (P2 08:00-15:45) | `sensor.eprices_today_json_15_min_prices_eur_kwh_p2_08_00_15_45` | JSON array, 32 values |
| Today JSON 15-Min Prices EURkWh (P3 16:00-23:45) | `sensor.eprices_today_json_15_min_prices_eur_kwh_p3_16_00_23_45` | JSON array, 32 values |
| Today JSON Hourly Prices EURkWh | `sensor.eprices_today_json_hourly_prices_eur_kwh` | JSON array, 24 values; `""` when no data |
| Today JSON 15-Min Prices EURkWh (P1 00:00-07:45) | `sensor.eprices_today_json_15_min_prices_eur_kwh_p1_00_00_07_45` | JSON array, 32 values; `""` when no data |
| Today JSON 15-Min Prices EURkWh (P2 08:00-15:45) | `sensor.eprices_today_json_15_min_prices_eur_kwh_p2_08_00_15_45` | JSON array, 32 values; `""` when no data |
| Today JSON 15-Min Prices EURkWh (P3 16:00-23:45) | `sensor.eprices_today_json_15_min_prices_eur_kwh_p3_16_00_23_45` | JSON array, 32 values; `""` when no data |
| Today Highest Price Time | `sensor.eprices_today_highest_price_time` | HH:MM |
| Today Lowest Price Time | `sensor.eprices_today_lowest_price_time` | HH:MM |
| Today Highest Hourly Price Time | `sensor.eprices_today_highest_hourly_price_time` | HH:00 |
@@ -129,10 +358,10 @@ eprices_vat_rate # e.g. "0.22" (VAT rate as decimal multiplier)
| Today Price Update Status Message | `sensor.eprices_today_price_update_status_message` | Detailed status string; diagnostic |
| Today API Fetch Attempts | `sensor.eprices_today_api_fetch_attempts` | HTTP fetch count; resets at midnight; diagnostic |
| Today Entry Count | `sensor.eprices_today_entry_count` | Number of stored price points; diagnostic |
| Tomorrow JSON Hourly Prices EURkWh | `sensor.eprices_tomorrow_json_hourly_prices_eur_kwh` | JSON array, 24 values |
| Tomorrow JSON 15-Min Prices EURkWh (P1 00:00-07:45) | `sensor.eprices_tomorrow_json_15_min_prices_eur_kwh_p1_00_00_07_45` | JSON array, 32 values |
| Tomorrow JSON 15-Min Prices EURkWh (P2 08:00-15:45) | `sensor.eprices_tomorrow_json_15_min_prices_eur_kwh_p2_08_00_15_45` | JSON array, 32 values |
| Tomorrow JSON 15-Min Prices EURkWh (P3 16:00-23:45) | `sensor.eprices_tomorrow_json_15_min_prices_eur_kwh_p3_16_00_23_45` | JSON array, 32 values |
| Tomorrow JSON Hourly Prices EURkWh | `sensor.eprices_tomorrow_json_hourly_prices_eur_kwh` | JSON array, 24 values; `""` when no data |
| Tomorrow JSON 15-Min Prices EURkWh (P1 00:00-07:45) | `sensor.eprices_tomorrow_json_15_min_prices_eur_kwh_p1_00_00_07_45` | JSON array, 32 values; `""` when no data |
| Tomorrow JSON 15-Min Prices EURkWh (P2 08:00-15:45) | `sensor.eprices_tomorrow_json_15_min_prices_eur_kwh_p2_08_00_15_45` | JSON array, 32 values; `""` when no data |
| Tomorrow JSON 15-Min Prices EURkWh (P3 16:00-23:45) | `sensor.eprices_tomorrow_json_15_min_prices_eur_kwh_p3_16_00_23_45` | JSON array, 32 values; `""` when no data |
| Tomorrow Highest Price Time | `sensor.eprices_tomorrow_highest_price_time` | HH:MM |
| Tomorrow Lowest Price Time | `sensor.eprices_tomorrow_lowest_price_time` | HH:MM |
| Tomorrow Highest Hourly Price Time | `sensor.eprices_tomorrow_highest_hourly_price_time` | HH:00 |
@@ -173,3 +402,4 @@ eprices_vat_rate # e.g. "0.22" (VAT rate as decimal multiplier)
- **API fetch times** reset to `Never` at midnight bridge and on `clear_tomorrow_prices`
- **API fetch attempt counters** reset to `0` at midnight and publish `0` immediately on boot
- **Uptime** displayed as human-readable string: `45 s` / `5 min` / `3 h 22 min` / `12 d 4 h` / `4 months 12 d`
- **JSON sensors** publish `""` (empty string) when no data is available — all eight JSON sensors behave consistently
+103 -77
View File
@@ -85,96 +85,114 @@ The goal of EPrices was to:
| Manual force update | Button | Button (unchanged) |
| Midnight bridge | External HA automation at 00:00 | On-device `on_time: 00:00:00` |
---
### Sensor naming
All sensors were renamed with a systematic **Today / Tomorrow** prefix.
The words "electricity" and "energy" were removed from all sensor names.
"Next Day" was replaced with "Tomorrow" throughout.
> **Note on entity ID slugs:** HA derives entity IDs by lower-casing the
> sensor name and replacing spaces and special characters with underscores.
> The `` character in JSON sensor names may have been rendered differently
> by older ESPHome versions — verify those specific IDs against your actual
> HA instance if needed.
---
#### Numeric sensors
| entso-e-prices v4.3.1 name | EPrices v1.0 name |
|---|---|
| `Current Electricity Price` | `Today Current Price` |
| `Next Electricity Price` | `Today Next Price` |
| `Average Electricity Price Today` | `Today Average Price` |
| `Highest Electricity Price Today` | `Today Highest Price` |
| `Lowest Electricity Price Today` | `Today Lowest Price` |
| `Current Hourly Electricity Price` | `Today Current Hourly Price` |
| `Next Hourly Electricity Price` | `Today Next Hourly Price` |
| `Highest Hourly Electricity Price Today` | `Today Highest Hourly Price` |
| `Lowest Hourly Electricity Price Today` | `Today Lowest Hourly Price` |
| `Current Max Hourly Price Percentage` | `Today Current Max Hourly Price Percentage` |
| `Daily Price Update Attempts` | `Today API Fetch Attempts` *(moved to text_sensor)* |
| `Today Entry Count` | `Today Entry Count` *(moved to text_sensor)* |
| `Next Day Current Electricity Price` | `Tomorrow Current Price` |
| `Next Day Next Electricity Price` | `Tomorrow Next Price` |
| `Average Electricity Price Tomorrow` | `Tomorrow Average Price` |
| `Highest Electricity Price Tomorrow` | `Tomorrow Highest Price` |
| `Lowest Electricity Price Tomorrow` | `Tomorrow Lowest Price` |
| `Current Hourly Electricity Price Tomorrow` | `Tomorrow Current Hourly Price` |
| `Next Hourly Electricity Price Tomorrow` | `Tomorrow Next Hourly Price` |
| `Highest Hourly Electricity Price Tomorrow` | `Tomorrow Highest Hourly Price` |
| `Lowest Hourly Electricity Price Tomorrow` | `Tomorrow Lowest Hourly Price` |
| `Next Day Current Max Hourly Price Percentage` | `Tomorrow Current Max Hourly Price Percentage` |
| `Next Day Price Update Attempts` | `Tomorrow API Fetch Attempts` *(moved to text_sensor)* |
| `Tomorrow Entry Count` | `Tomorrow Entry Count` *(moved to text_sensor)* |
| entso-e-prices v4.3.1 name | v4.3.1 entity ID | EPrices v1.2 name | EPrices v1.2 entity ID |
|---|---|---|---|
| `Current Electricity Price` | `sensor.entso_e_prices_current_electricity_price` | `Today Current Price` | `sensor.eprices_today_current_price` |
| `Next Electricity Price` | `sensor.entso_e_prices_next_electricity_price` | `Today Next Price` | `sensor.eprices_today_next_price` |
| `Average Electricity Price Today` | `sensor.entso_e_prices_average_electricity_price_today` | `Today Average Price` | `sensor.eprices_today_average_price` |
| `Highest Electricity Price Today` | `sensor.entso_e_prices_highest_electricity_price_today` | `Today Highest Price` | `sensor.eprices_today_highest_price` |
| `Lowest Electricity Price Today` | `sensor.entso_e_prices_lowest_electricity_price_today` | `Today Lowest Price` | `sensor.eprices_today_lowest_price` |
| `Current Hourly Electricity Price` | `sensor.entso_e_prices_current_hourly_electricity_price` | `Today Current Hourly Price` | `sensor.eprices_today_current_hourly_price` |
| `Next Hourly Electricity Price` | `sensor.entso_e_prices_next_hourly_electricity_price` | `Today Next Hourly Price` | `sensor.eprices_today_next_hourly_price` |
| `Highest Hourly Electricity Price Today` | `sensor.entso_e_prices_highest_hourly_electricity_price_today` | `Today Highest Hourly Price` | `sensor.eprices_today_highest_hourly_price` |
| `Lowest Hourly Electricity Price Today` | `sensor.entso_e_prices_lowest_hourly_electricity_price_today` | `Today Lowest Hourly Price` | `sensor.eprices_today_lowest_hourly_price` |
| `Current Max Hourly Price Percentage` | `sensor.entso_e_prices_current_max_hourly_price_percentage` | `Today Current Max Hourly Price Percentage` | `sensor.eprices_today_current_max_hourly_price_percentage` |
| `Daily Price Update Attempts` | `sensor.entso_e_prices_daily_price_update_attempts` | `Today API Fetch Attempts` *(moved to text_sensor)* | `sensor.eprices_today_api_fetch_attempts` |
| `Today Entry Count` | `sensor.entso_e_prices_today_entry_count` | `Today Entry Count` *(moved to text_sensor)* | `sensor.eprices_today_entry_count` |
| `Next Day Current Electricity Price` | `sensor.entso_e_prices_next_day_current_electricity_price` | `Tomorrow Current Price` | `sensor.eprices_tomorrow_current_price` |
| `Next Day Next Electricity Price` | `sensor.entso_e_prices_next_day_next_electricity_price` | `Tomorrow Next Price` | `sensor.eprices_tomorrow_next_price` |
| `Average Electricity Price Tomorrow` | `sensor.entso_e_prices_average_electricity_price_tomorrow` | `Tomorrow Average Price` | `sensor.eprices_tomorrow_average_price` |
| `Highest Electricity Price Tomorrow` | `sensor.entso_e_prices_highest_electricity_price_tomorrow` | `Tomorrow Highest Price` | `sensor.eprices_tomorrow_highest_price` |
| `Lowest Electricity Price Tomorrow` | `sensor.entso_e_prices_lowest_electricity_price_tomorrow` | `Tomorrow Lowest Price` | `sensor.eprices_tomorrow_lowest_price` |
| `Current Hourly Electricity Price Tomorrow` | `sensor.entso_e_prices_current_hourly_electricity_price_tomorrow` | `Tomorrow Current Hourly Price` | `sensor.eprices_tomorrow_current_hourly_price` |
| `Next Hourly Electricity Price Tomorrow` | `sensor.entso_e_prices_next_hourly_electricity_price_tomorrow` | `Tomorrow Next Hourly Price` | `sensor.eprices_tomorrow_next_hourly_price` |
| `Highest Hourly Electricity Price Tomorrow` | `sensor.entso_e_prices_highest_hourly_electricity_price_tomorrow` | `Tomorrow Highest Hourly Price` | `sensor.eprices_tomorrow_highest_hourly_price` |
| `Lowest Hourly Electricity Price Tomorrow` | `sensor.entso_e_prices_lowest_hourly_electricity_price_tomorrow` | `Tomorrow Lowest Hourly Price` | `sensor.eprices_tomorrow_lowest_hourly_price` |
| `Next Day Current Max Hourly Price Percentage` | `sensor.entso_e_prices_next_day_current_max_hourly_price_percentage` | `Tomorrow Current Max Hourly Price Percentage` | `sensor.eprices_tomorrow_current_max_hourly_price_percentage` |
| `Next Day Price Update Attempts` | `sensor.entso_e_prices_next_day_price_update_attempts` | `Tomorrow API Fetch Attempts` *(moved to text_sensor)* | `sensor.eprices_tomorrow_api_fetch_attempts` |
| `Tomorrow Entry Count` | `sensor.entso_e_prices_tomorrow_entry_count` | `Tomorrow Entry Count` *(moved to text_sensor)* | `sensor.eprices_tomorrow_entry_count` |
---
#### Text sensors
| entso-e-prices v4.3.1 name | EPrices v1.0 name |
|---|---|
| `ENTSO-E Hourly Prices EURkWh JSON` | `Today JSON Hourly Prices EURkWh` |
| `ENTSO-E 15-Min Prices EURkWh JSON (P1 00:00-07:45)` | `Today JSON 15-Min Prices EURkWh (P1 00:00-07:45)` |
| `ENTSO-E 15-Min Prices EURkWh JSON (P2 08:00-15:45)` | `Today JSON 15-Min Prices EURkWh (P2 08:00-15:45)` |
| `ENTSO-E 15-Min Prices EURkWh JSON (P3 16:00-23:45)` | `Today JSON 15-Min Prices EURkWh (P3 16:00-23:45)` |
| `Time Of Highest Energy Price Today` | `Today Highest Price Time` |
| `Time Of Lowest Energy Price Today` | `Today Lowest Price Time` |
| `Time Of Highest Hourly Energy Price Today` | `Today Highest Hourly Price Time` |
| `Time Of Lowest Hourly Energy Price Today` | `Today Lowest Hourly Price Time` |
| `Price Update Status` | `Today Price Update Status` |
| `Last Price Update Time` | `Today Data Loaded Time` |
| `Price Update Status Message` | `Today Price Update Status Message` |
| `Current Price Status` | `Today Current Price Status` |
| `ENTSO-E Next Day Hourly Prices EURkWh JSON` | `Tomorrow JSON Hourly Prices EURkWh` |
| `ENTSO-E Next Day 15-Min Prices EURkWh JSON (P1 00:00-07:45)` | `Tomorrow JSON 15-Min Prices EURkWh (P1 00:00-07:45)` |
| `ENTSO-E Next Day 15-Min Prices EURkWh JSON (P2 08:00-15:45)` | `Tomorrow JSON 15-Min Prices EURkWh (P2 08:00-15:45)` |
| `ENTSO-E Next Day 15-Min Prices EURkWh JSON (P3 16:00-23:45)` | `Tomorrow JSON 15-Min Prices EURkWh (P3 16:00-23:45)` |
| `Time Of Highest Energy Price Tomorrow` | `Tomorrow Highest Price Time` |
| `Time Of Lowest Energy Price Tomorrow` | `Tomorrow Lowest Price Time` |
| `Time Of Highest Hourly Energy Price Tomorrow` | `Tomorrow Highest Hourly Price Time` |
| `Time Of Lowest Hourly Energy Price Tomorrow` | `Tomorrow Lowest Hourly Price Time` |
| `Next Day Price Update Status` | `Tomorrow Price Update Status` |
| `Next Day Last Price Update Time` | `Tomorrow Data Loaded Time` |
| `Next Day Price Update Status Message` | `Tomorrow Price Update Status Message` |
| `Next Day Current Price Status` | `Tomorrow Current Price Status` |
| `ENTSO-E Last Reboot` | `Last Reboot` |
| `Entso-E Today NVS Status` | `Today NVS Status` |
| `Entso-E Tomorrow NVS Status` | `Tomorrow NVS Status` |
| `ENTSO-E Last Update Source` | `Last Update Source` |
| `Today Data Date` | `Today Data Date` *(unchanged)* |
| `Tomorrow Data Date` | `Tomorrow Data Date` *(unchanged)* |
| entso-e-prices v4.3.1 name | v4.3.1 entity ID | EPrices v1.2 name | EPrices v1.2 entity ID |
|---|---|---|---|
| `ENTSO-E Hourly Prices EURkWh JSON` | `sensor.entso_e_prices_entso_e_hourly_prices_eur_kwh_json` | `Today JSON Hourly Prices EURkWh` | `sensor.eprices_today_json_hourly_prices_eur_kwh` |
| `ENTSO-E 15-Min Prices EURkWh JSON (P1 00:00-07:45)` | `sensor.entso_e_prices_entso_e_15_min_prices_eur_kwh_json_p1_00_00_07_45` | `Today JSON 15-Min Prices EURkWh (P1 00:00-07:45)` | `sensor.eprices_today_json_15_min_prices_eur_kwh_p1_00_00_07_45` |
| `ENTSO-E 15-Min Prices EURkWh JSON (P2 08:00-15:45)` | `sensor.entso_e_prices_entso_e_15_min_prices_eur_kwh_json_p2_08_00_15_45` | `Today JSON 15-Min Prices EURkWh (P2 08:00-15:45)` | `sensor.eprices_today_json_15_min_prices_eur_kwh_p2_08_00_15_45` |
| `ENTSO-E 15-Min Prices EURkWh JSON (P3 16:00-23:45)` | `sensor.entso_e_prices_entso_e_15_min_prices_eur_kwh_json_p3_16_00_23_45` | `Today JSON 15-Min Prices EURkWh (P3 16:00-23:45)` | `sensor.eprices_today_json_15_min_prices_eur_kwh_p3_16_00_23_45` |
| `Time Of Highest Energy Price Today` | `sensor.entso_e_prices_time_of_highest_energy_price_today` | `Today Highest Price Time` | `sensor.eprices_today_highest_price_time` |
| `Time Of Lowest Energy Price Today` | `sensor.entso_e_prices_time_of_lowest_energy_price_today` | `Today Lowest Price Time` | `sensor.eprices_today_lowest_price_time` |
| `Time Of Highest Hourly Energy Price Today` | `sensor.entso_e_prices_time_of_highest_hourly_energy_price_today` | `Today Highest Hourly Price Time` | `sensor.eprices_today_highest_hourly_price_time` |
| `Time Of Lowest Hourly Energy Price Today` | `sensor.entso_e_prices_time_of_lowest_hourly_energy_price_today` | `Today Lowest Hourly Price Time` | `sensor.eprices_today_lowest_hourly_price_time` |
| `Price Update Status` | `sensor.entso_e_prices_price_update_status` | `Today Price Update Status` | `sensor.eprices_today_price_update_status` |
| `Last Price Update Time` | `sensor.entso_e_prices_last_price_update_time` | `Today Data Loaded Time` | `sensor.eprices_today_data_loaded_time` |
| `Price Update Status Message` | `sensor.entso_e_prices_price_update_status_message` | `Today Price Update Status Message` | `sensor.eprices_today_price_update_status_message` |
| `Current Price Status` | `sensor.entso_e_prices_current_price_status` | `Today Current Price Status` | `sensor.eprices_today_current_price_status` |
| `ENTSO-E Next Day Hourly Prices EURkWh JSON` | `sensor.entso_e_prices_entso_e_next_day_hourly_prices_eur_kwh_json` | `Tomorrow JSON Hourly Prices EURkWh` | `sensor.eprices_tomorrow_json_hourly_prices_eur_kwh` |
| `ENTSO-E Next Day 15-Min Prices EURkWh JSON (P1 00:00-07:45)` | `sensor.entso_e_prices_entso_e_next_day_15_min_prices_eur_kwh_json_p1_00_00_07_45` | `Tomorrow JSON 15-Min Prices EURkWh (P1 00:00-07:45)` | `sensor.eprices_tomorrow_json_15_min_prices_eur_kwh_p1_00_00_07_45` |
| `ENTSO-E Next Day 15-Min Prices EURkWh JSON (P2 08:00-15:45)` | `sensor.entso_e_prices_entso_e_next_day_15_min_prices_eur_kwh_json_p2_08_00_15_45` | `Tomorrow JSON 15-Min Prices EURkWh (P2 08:00-15:45)` | `sensor.eprices_tomorrow_json_15_min_prices_eur_kwh_p2_08_00_15_45` |
| `ENTSO-E Next Day 15-Min Prices EURkWh JSON (P3 16:00-23:45)` | `sensor.entso_e_prices_entso_e_next_day_15_min_prices_eur_kwh_json_p3_16_00_23_45` | `Tomorrow JSON 15-Min Prices EURkWh (P3 16:00-23:45)` | `sensor.eprices_tomorrow_json_15_min_prices_eur_kwh_p3_16_00_23_45` |
| `Time Of Highest Energy Price Tomorrow` | `sensor.entso_e_prices_time_of_highest_energy_price_tomorrow` | `Tomorrow Highest Price Time` | `sensor.eprices_tomorrow_highest_price_time` |
| `Time Of Lowest Energy Price Tomorrow` | `sensor.entso_e_prices_time_of_lowest_energy_price_tomorrow` | `Tomorrow Lowest Price Time` | `sensor.eprices_tomorrow_lowest_price_time` |
| `Time Of Highest Hourly Energy Price Tomorrow` | `sensor.entso_e_prices_time_of_highest_hourly_energy_price_tomorrow` | `Tomorrow Highest Hourly Price Time` | `sensor.eprices_tomorrow_highest_hourly_price_time` |
| `Time Of Lowest Hourly Energy Price Tomorrow` | `sensor.entso_e_prices_time_of_lowest_hourly_energy_price_tomorrow` | `Tomorrow Lowest Hourly Price Time` | `sensor.eprices_tomorrow_lowest_hourly_price_time` |
| `Next Day Price Update Status` | `sensor.entso_e_prices_next_day_price_update_status` | `Tomorrow Price Update Status` | `sensor.eprices_tomorrow_price_update_status` |
| `Next Day Last Price Update Time` | `sensor.entso_e_prices_next_day_last_price_update_time` | `Tomorrow Data Loaded Time` | `sensor.eprices_tomorrow_data_loaded_time` |
| `Next Day Price Update Status Message` | `sensor.entso_e_prices_next_day_price_update_status_message` | `Tomorrow Price Update Status Message` | `sensor.eprices_tomorrow_price_update_status_message` |
| `Next Day Current Price Status` | `sensor.entso_e_prices_next_day_current_price_status` | `Tomorrow Current Price Status` | `sensor.eprices_tomorrow_current_price_status` |
| `ENTSO-E Last Reboot` | `sensor.entso_e_prices_entso_e_last_reboot` | `Last Reboot` | `sensor.eprices_last_reboot` |
| `Entso-E Today NVS Status` | `sensor.entso_e_prices_entso_e_today_nvs_status` | `Today NVS Status` | `sensor.eprices_today_nvs_status` |
| `Entso-E Tomorrow NVS Status` | `sensor.entso_e_prices_entso_e_tomorrow_nvs_status` | `Tomorrow NVS Status` | `sensor.eprices_tomorrow_nvs_status` |
| `ENTSO-E Last Update Source` | `sensor.entso_e_prices_entso_e_last_update_source` | `Last Update Source` | `sensor.eprices_last_update_source` |
| `Today Data Date` | `sensor.entso_e_prices_today_data_date` | `Today Data Date` *(unchanged)* | `sensor.eprices_today_data_date` |
| `Tomorrow Data Date` | `sensor.entso_e_prices_tomorrow_data_date` | `Tomorrow Data Date` *(unchanged)* | `sensor.eprices_tomorrow_data_date` |
---
#### Buttons
| entso-e-prices v4.3.1 name | EPrices v1.0 name |
|---|---|
| `Entso-E Force Update` | `Force Today's Update` |
| `Entso-E Force Next Day Update` | `Force Tomorrow's Update` |
| `Entso-E Reboot Device` | `Reboot Device` |
| entso-e-prices v4.3.1 name | v4.3.1 entity ID | EPrices v1.2 name | EPrices v1.2 entity ID |
|---|---|---|---|
| `Entso-E Force Update` | `button.entso_e_prices_entso_e_force_update` | `Force Today's Update` | `button.eprices_force_today_s_update` |
| `Entso-E Force Next Day Update` | `button.entso_e_prices_entso_e_force_next_day_update` | `Force Tomorrow's Update` | `button.eprices_force_tomorrow_s_update` |
| `Entso-E Reboot Device` | `button.entso_e_prices_entso_e_reboot_device` | `Reboot Device` | `button.eprices_reboot_device` |
---
### New sensors in EPrices v1.0 (no equivalent in v4.3.1)
| Sensor | Purpose |
|---|---|
| `Today Last API Fetch Time` | Timestamp of last successful HTTP fetch for today |
| `Tomorrow Last API Fetch Time` | Timestamp of last successful HTTP fetch for tomorrow |
| `WiFi Signal` | RSSI in dBm |
| `Uptime` | Human-readable uptime string |
| EPrices v1.2 name | EPrices v1.2 entity ID | Purpose |
|---|---|---|
| `Today Last API Fetch Time` | `sensor.eprices_today_last_api_fetch_time` | Timestamp of last successful HTTP fetch for today |
| `Tomorrow Last API Fetch Time` | `sensor.eprices_tomorrow_last_api_fetch_time` | Timestamp of last successful HTTP fetch for tomorrow |
| `WiFi Signal` | `sensor.eprices_wifi_signal` | RSSI in dBm |
| `Uptime` | `sensor.eprices_uptime` | Human-readable uptime string |
---
### Sensor output changes
| Sensor | v4.3.1 output | EPrices v1.0 output |
| Sensor | v4.3.1 output | EPrices v1.2 output |
|---|---|---|
| `Last Reboot` | `Last reboot: 2026-04-04 20:10:10` | `2026-04-04 20:10:10` |
| `Today NVS Status` | `Today NVS: Stored 96 pts for 2026-04-04` | `Stored 96 pts for 2026-04-04` |
@@ -186,23 +204,30 @@ The words "electricity" and "energy" were removed from all sensor names.
| `Today Current Price Status` | `Valid` / `Missing` | `Valid` / `Missing` / `Stale` |
| `Today Data Loaded Time` | Stamped on HTTP fetch only | Stamped on NVS load and HTTP fetch |
| `Tomorrow Data Loaded Time` | `Never` after NVS boot load | Stamped on NVS load and HTTP fetch |
| `Tomorrow Price Update Status Message` | *(various)* | `"No data yet"` on boot / after reboot (v1.2) |
---
### HA entity ID changes
All entity IDs changed due to the node name change from `entso-e-prices` to `eprices`.
**Find and replace in all your automations and dashboards:**
**Step 1 — bulk find and replace in all your automations and dashboards:**
```
sensor.entso_e_prices_ → sensor.eprices_
button.entso_e_prices_ → button.eprices_
```
Then apply the individual sensor name slug changes from the tables above.
**Step 2 — apply the individual sensor name slug renames** from the tables above
for sensors whose names changed (not just the prefix).
> **Tip:** After the bulk replace in Step 1, search for any remaining
> `entso_e_prices_` strings to catch any that were missed.
### NVS data
The NVS namespace changed from `entsoe2` to `eprices`. On first boot after
flashing EPrices v1.0 the device will not find any stored data and will
flashing EPrices the device will not find any stored data and will
trigger a fresh HTTP fetch automatically. This is expected and safe.
No manual NVS erase is required.
@@ -211,9 +236,10 @@ No manual NVS erase is required.
## Migration checklist
- [ ] Flash `eprices.yaml` to the device
- [ ] Update `secrets.yaml` — rename all `entsoe_` keys to `eprices_`; add `eprices_prov_fee` and `eprices_vat_rate`
- [ ] In all HA automations: replace `entso_e_prices_` with `eprices_` in all entity IDs
- [ ] Apply individual sensor name slug renames from the tables above
- [ ] Update `secrets.yaml` — rename all `entsoe_` keys to `eprices_`; add `eprices_prov_fee`, `eprices_vat_rate`, and `eprices_neg_prov_fee`
- [ ] In all HA automations: bulk replace `sensor.entso_e_prices_` `sensor.eprices_` and `button.entso_e_prices_``button.eprices_`
- [ ] Apply individual sensor slug renames from the tables above for sensors whose names changed
- [ ] Remove any external HA automations that handled midnight bridge, boot recovery, or fetch scheduling — these are now all on-device
- [ ] Verify the device fetches fresh data on first boot (check `Today NVS Status` and `Today Price Update Status Message`)
- [ ] Update any HA dashboard cards referencing old entity IDs
- [ ] Update any HA dashboard cards referencing old entity IDs
- [ ] Verify the JSON sensor entity IDs in dashboards — the `` character slug may differ from expected; check against actual HA entity registry if needed
+32 -10
View File
@@ -22,7 +22,11 @@ Assistant automations are required for any core functionality.
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
- **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 Status` shows `Stale` if stored date mismatches today
- **Tomorrow live sensors** evaluate at `now + 86400s` — reflecting tomorrow at the same local time
@@ -38,6 +42,12 @@ Assistant automations are required for any core functionality.
- **ESP32** development board (tested on `esp32dev`)
- ESPHome with **`esp-idf` framework** (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
@@ -133,6 +143,18 @@ 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) |
| 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**
@@ -197,10 +219,10 @@ safe on DST transition days (23-hour and 25-hour days).
| Sensor | Entity ID | Description |
|---|---|---|
| Today JSON Hourly Prices EURkWh | `sensor.eprices_today_json_hourly_prices_eur_kwh` | JSON array of 24 hourly averages |
| Today JSON 15-Min Prices EURkWh (P1 00:00-07:45) | `sensor.eprices_today_json_15_min_prices_eur_kwh_p1_00_00_07_45` | JSON array of 32 prices |
| Today JSON 15-Min Prices EURkWh (P2 08:00-15:45) | `sensor.eprices_today_json_15_min_prices_eur_kwh_p2_08_00_15_45` | JSON array of 32 prices |
| Today JSON 15-Min Prices EURkWh (P3 16:00-23:45) | `sensor.eprices_today_json_15_min_prices_eur_kwh_p3_16_00_23_45` | JSON array of 32 prices |
| Today JSON Hourly Prices EURkWh | `sensor.eprices_today_json_hourly_prices_eur_kwh` | JSON array of 24 hourly averages; `""` when no data |
| Today JSON 15-Min Prices EURkWh (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 EURkWh (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 EURkWh (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) |
@@ -227,10 +249,10 @@ safe on DST transition days (23-hour and 25-hour days).
| Sensor | Entity ID | Description |
|---|---|---|
| Tomorrow JSON Hourly Prices EURkWh | `sensor.eprices_tomorrow_json_hourly_prices_eur_kwh` | JSON array of 24 hourly averages |
| Tomorrow JSON 15-Min Prices EURkWh (P1 00:00-07:45) | `sensor.eprices_tomorrow_json_15_min_prices_eur_kwh_p1_00_00_07_45` | JSON array of 32 prices |
| Tomorrow JSON 15-Min Prices EURkWh (P2 08:00-15:45) | `sensor.eprices_tomorrow_json_15_min_prices_eur_kwh_p2_08_00_15_45` | JSON array of 32 prices |
| Tomorrow JSON 15-Min Prices EURkWh (P3 16:00-23:45) | `sensor.eprices_tomorrow_json_15_min_prices_eur_kwh_p3_16_00_23_45` | JSON array of 32 prices |
| Tomorrow JSON Hourly Prices EURkWh | `sensor.eprices_tomorrow_json_hourly_prices_eur_kwh` | JSON array of 24 hourly averages; `""` when no data |
| Tomorrow JSON 15-Min Prices EURkWh (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 EURkWh (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 EURkWh (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) |
@@ -259,7 +281,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` |
| 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 |
| Tomorrow Price Update Status Message | `sensor.eprices_tomorrow_price_update_status_message` | Detailed status string; `"Fetch timeout will retry"` after watchdog trigger |
### Buttons
+103
View File
@@ -1,5 +1,108 @@
# EPrices Version History
## v1.2.2 — 2026-04-28
Stability patch addressing spontaneous reboots during tomorrow price fetch at ~13:55.
No new sensors, no secrets changes, no entity ID changes. Drop-in replacement for v1.2.1.
### ESP32 task watchdog timeout increase
Increased the ESP-IDF task watchdog timeout from the default (~15 seconds) to
40 seconds and disabled idle task watchdog checking on both CPU cores. This
prevents false-positive watchdog resets during heavy JSON parsing when full price
data arrives (~13:55 and subsequent retry attempts).
**Root cause:** The first API call at 13:25 typically returns no data
(Energy-Charts usually hasn't published tomorrow's prices yet), so no heavy
parsing occurs. By 13:55, complete data is available, triggering the full
parsing chain that exceeded the default watchdog timeout.
### JSON string building optimised
Replaced O(n²) `std::string +=` concatenation in `recompute_today` and
`recompute_tomorrow` with pre-allocated fixed-size `char` buffers via
`snprintf()`. Eliminates heap fragmentation and peak memory spikes during JSON
building for the 8 JSON text sensors.
### Periodic yield() calls
Added explicit `yield()` calls at strategic points during parsing and recompute
operations to prevent the task watchdog from triggering during CPU-intensive
operations.
### Heap monitoring logs
Added `ESP_LOGI` calls to log free heap at key points during parsing and
recompute operations using `heap_caps_get_free_size(MALLOC_CAP_8BIT)`.
See `CHANGELOG.md` for full implementation details.
---
## v1.2.1 — 2026-04-07
Stability and housekeeping patch. No new sensors, no secrets changes,
no entity ID changes. Drop-in replacement for v1.2.
### ESP32 main task stack size increase
Doubled the FreeRTOS main task stack from 8192 to 16384 bytes via
`CONFIG_ESP_MAIN_TASK_STACK_SIZE`. Eliminates the stack overflow scenario
most likely responsible for the spontaneous reboot observed in production
on 2026-04-06 during a simultaneous NVS load + HTTP fetch/parse cycle.
### Price vector heap pre-allocation at boot
Added `.reserve(96)` on all four price vectors (`price_timestamps_today`,
`price_values_today`, `price_timestamps_tomorrow`, `price_values_tomorrow`)
in the `on_boot` lambda. Prevents repeated heap reallocation during NVS load
and HTTP parse operations, reducing heap fragmentation and peak allocation
pressure during the boot sequence.
### Hourly JSON sensor cleared state unified
`Today JSON Hourly Prices EURkWh` and `Tomorrow JSON Hourly Prices EURkWh`
now publish `""` when cleared, matching the existing behaviour of all six
15-minute JSON sensors. Previously they published `"[]"`.
See `CHANGELOG.md` for full implementation details.
---
## 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"`
- `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
+224 -48
View File
@@ -1,10 +1,51 @@
# =============================================================================
# ESPHome: EPrices v1.1
# ESPHome: EPrices v1.2.2
#
# Electricity price data provided by Energy-Charts (https://energy-charts.info)
# under CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/).
#
# v1.1 changes:
# v1.2.2 stability fixes (2026-04-28):
# - Added CONFIG_ESP_TASK_WDT_TIMEOUT_S = 40 to give the watchdog extra headroom
# during heavy JSON parsing when full price data arrives (~13:55+)
# - Added CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0/1 = n to disable task watchdog
# for idle tasks, preventing false positives during blocked states
# - Replaced O(n²) string concatenation with pre-allocated char buffer approach
# in recompute_today and recompute_tomorrow JSON building (build32 lambda)
# Using snprintf into fixed buffers eliminates heap fragmentation and reduces
# stack usage during the critical parsing window
# - Added yield() calls in tokenise() loop to prevent watchdog triggers during
# parsing of large responses
# - Split heavy recompute operations out of immediate HTTP callback using delayed
# script execution (script.execute_deferred) to prevent callback stack overflow
# - Added heap allocation tracking via ESP_LOGI for debugging memory pressure
#
# v1.2.1 changes (2026-04-07):
# - CONFIG_ESP_MAIN_TASK_STACK_SIZE raised from 8192 to 16384 bytes via
# esp32: framework: sdkconfig_options — eliminates stack overflow scenario
# observed in production during simultaneous NVS load + HTTP fetch/parse
# - Price vectors pre-allocated at boot with .reserve(96) in on_boot lambda:
# price_timestamps_today, price_values_today,
# price_timestamps_tomorrow, price_values_tomorrow
# prevents repeated heap reallocation during NVS load and HTTP parse cycles
# - Hourly JSON sensors now publish "" when cleared (was "[]") —
# consistent with all six 15-min JSON sensors
#
# 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)
@@ -42,6 +83,7 @@
# eprices_country_bzn e.g. "SI"
# eprices_prov_fee e.g. "0.12" (provider fee, 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 CHANGELOG.md for complete sensor and entity ID listing.
@@ -70,11 +112,27 @@ esphome:
} else {
id(boot_time) = 0;
}
// Pre-allocate price vectors to avoid repeated heap reallocation
// during NVS load and HTTP fetch/parse cycles
id(price_timestamps_today).reserve(96);
id(price_values_today).reserve(96);
id(price_timestamps_tomorrow).reserve(96);
id(price_values_tomorrow).reserve(96);
// Log available heap for debugging
ESP_LOGI("eprices", "Boot: free heap = %u bytes", heap_caps_get_free_size(MALLOC_CAP_8BIT));
esp32:
board: esp32dev
framework:
type: esp-idf
sdkconfig_options:
CONFIG_ESP_MAIN_TASK_STACK_SIZE: "16384"
# v1.2.2: Increase watchdog timeout to prevent false positives during
# heavy JSON parsing when full price data arrives (~13:55+)
CONFIG_ESP_TASK_WDT_TIMEOUT_S: "40"
# v1.2.2: Disable idle task watchdog to prevent issues during blocked states
CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0: n
CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1: n
# No include_builtin_idf_components needed — nvs headers are always
# available in esp-idf framework; eprices_nvs.h includes them directly.
@@ -185,6 +243,15 @@ time:
- seconds: /10
then:
- 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)) {
ESP_LOGI("eprices", "Worker: starting Today update");
id(need_today_update) = false;
@@ -194,6 +261,15 @@ time:
- seconds: /10
then:
- 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();
bool in_window = (t.hour > 13 && t.hour < 23) ||
(t.hour == 13 && t.minute >= 20) ||
@@ -322,7 +398,8 @@ time:
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();
# Attempts #3.. at 14:55, 15:55, 16:55, 17:55, 18:55, 19:55 (capped to 8)
@@ -343,7 +420,8 @@ time:
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();
- minutes: /1
@@ -462,6 +540,9 @@ globals:
- id: is_updating_today
type: bool
initial_value: 'false'
- id: is_updating_today_since
type: time_t
initial_value: '0'
- id: tomorrow_last_successful_update
type: time_t
@@ -478,7 +559,7 @@ globals:
initial_value: 'false'
- id: tomorrow_update_status_message
type: std::string
initial_value: '"Waiting for 13:20"'
initial_value: '"No data yet"'
- id: tomorrow_retry_count
type: int
initial_value: '0'
@@ -494,6 +575,9 @@ globals:
- id: is_updating_tomorrow
type: bool
initial_value: 'false'
- id: is_updating_tomorrow_since
type: time_t
initial_value: '0'
# --- Country code for Energy-Charts API (wired from secret) ---
- id: country_bzn
@@ -1303,15 +1387,22 @@ script:
}
// Tokenise comma-separated values
// v1.2.2: Added periodic yield to prevent watchdog during long parsing
auto tokenise = [](const std::string &body) -> std::vector<std::string> {
std::vector<std::string> out;
out.reserve(body.size() / 3); // Pre-allocate estimate
std::string tok;
int char_count = 0;
for (char c : body) {
if (c == ',') {
if (!tok.empty()) { out.push_back(tok); tok.clear(); }
} else if (c > ' ') {
tok += c;
}
// v1.2.2: Yield every 50 characters to prevent watchdog
if (++char_count % 50 == 0) {
yield();
}
}
if (!tok.empty()) out.push_back(tok);
return out;
@@ -1356,6 +1447,9 @@ script:
}
}
// v1.2.2: Log heap before heavy parsing
ESP_LOGI("eprices", "parse_today: free heap before parsing = %u bytes", heap_caps_get_free_size(MALLOC_CAP_8BIT));
for (size_t i = 0; i < n; i++) {
if (prc_toks[i] == "null") continue;
char *end1 = nullptr, *end2 = nullptr;
@@ -1367,13 +1461,17 @@ script:
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_values_today).push_back(eur_kwh);
// v1.2.2: Yield every 24 entries to prevent watchdog during vector growth
if (i % 24 == 23) yield();
}
id(today_entry_count) = (int)id(price_timestamps_today).size();
id(today_date_str) = std::string(today_buf);
ESP_LOGI("eprices", "parse_today: stored %d entries, date=%s",
id(today_entry_count), today_buf);
// v1.2.2: Log heap after parsing
ESP_LOGI("eprices", "parse_today: stored %d entries, free heap = %u bytes",
id(today_entry_count), heap_caps_get_free_size(MALLOC_CAP_8BIT));
# ---------------------------------------------------------------------------
# JSON PARSER TOMORROW
@@ -1410,15 +1508,23 @@ script:
return;
}
// Tokenise comma-separated values
// v1.2.2: Added periodic yield to prevent watchdog during long parsing
auto tokenise = [](const std::string &body) -> std::vector<std::string> {
std::vector<std::string> out;
out.reserve(body.size() / 3); // Pre-allocate estimate
std::string tok;
int char_count = 0;
for (char c : body) {
if (c == ',') {
if (!tok.empty()) { out.push_back(tok); tok.clear(); }
} else if (c > ' ') {
tok += c;
}
// v1.2.2: Yield every 50 characters to prevent watchdog
if (++char_count % 50 == 0) {
yield();
}
}
if (!tok.empty()) out.push_back(tok);
return out;
@@ -1444,6 +1550,9 @@ script:
snprintf(tmr_buf, sizeof(tmr_buf), "%04d%02d%02d",
tmr_tm->tm_year + 1900, tmr_tm->tm_mon + 1, tmr_tm->tm_mday);
// v1.2.2: Log heap before heavy parsing
ESP_LOGI("eprices", "parse_tomorrow: free heap before parsing = %u bytes", heap_caps_get_free_size(MALLOC_CAP_8BIT));
for (size_t i = 0; i < n; i++) {
if (prc_toks[i] == "null") continue;
char *end1 = nullptr, *end2 = nullptr;
@@ -1455,16 +1564,21 @@ script:
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_values_tomorrow).push_back(eur_kwh);
// v1.2.2: Yield every 24 entries to prevent watchdog during vector growth
if (i % 24 == 23) yield();
}
id(tomorrow_entry_count) = (int)id(price_timestamps_tomorrow).size();
id(tomorrow_date_str) = std::string(tmr_buf);
ESP_LOGI("eprices", "parse_tomorrow: stored %d entries, date=%s",
id(tomorrow_entry_count), tmr_buf);
// v1.2.2: Log heap after parsing
ESP_LOGI("eprices", "parse_tomorrow: stored %d entries, free heap = %u bytes",
id(tomorrow_entry_count), heap_caps_get_free_size(MALLOC_CAP_8BIT));
# ---------------------------------------------------------------------------
# RECOMPUTE TODAY fills legacy 96/24-slot vectors + JSON text sensors
# v1.2.2: Optimized JSON string building to prevent heap fragmentation
# ---------------------------------------------------------------------------
- id: recompute_today
then:
@@ -1472,6 +1586,9 @@ script:
int n = id(today_entry_count);
if (n == 0) { ESP_LOGW("eprices", "recompute_today: no entries"); return; }
// v1.2.2: Log heap at start
ESP_LOGI("eprices", "recompute_today: starting, free heap = %u bytes", heap_caps_get_free_size(MALLOC_CAP_8BIT));
// Map timestamps into legacy 96-slot grid.
// NOTE: on DST fall-back days (25h) the second occurrence of the
// repeated hour overwrites the first in this grid.
@@ -1505,47 +1622,73 @@ script:
float h_min_v = 9999.0f, h_max_v = -9999.0f;
int h_min_i = 0, h_max_i = 0;
double raw_sum = 0.0; int cnt_kwh = 0;
std::string json_h = "[";
// v1.2.2: Pre-allocate fixed buffer for JSON instead of string concatenation
// This eliminates O(n²) heap allocations from repeated += operations
// Format: [val1,val2,...,val24] where val can be "null" or a float
// Max size: 24 * 12 (max float string) + 25 (commas/brackets) + 1 = ~320 bytes
char json_h_buf[400];
int json_h_len = 0;
json_h_buf[json_h_len++] = '[';
for (int i = 0; i < 24; i++) {
if (h_cnt[i] > 0) {
float ha = h_sums[i] / (float)h_cnt[i];
id(hourly_avg_prices_kwh)[i] = ha;
if (ha < h_min_v) { h_min_v = ha; h_min_i = i; }
if (ha > h_max_v) { h_max_v = ha; h_max_i = i; }
char pb[12]; sprintf(pb, "%.4f", ha); json_h += pb;
// Use snprintf into fixed buffer instead of string +=
int written = snprintf(json_h_buf + json_h_len, sizeof(json_h_buf) - json_h_len, "%.4f", ha);
json_h_len += written;
raw_sum += ha; cnt_kwh++;
} else {
id(hourly_avg_prices_kwh)[i] = NAN;
json_h += "null";
json_h_len += snprintf(json_h_buf + json_h_len, sizeof(json_h_buf) - json_h_len, "null");
}
if (i < 23) {
json_h_buf[json_h_len++] = ',';
}
if (i < 23) json_h += ",";
}
json_h += "]";
id(json_hourly_prices_kwh).publish_state(json_h.c_str());
json_h_buf[json_h_len++] = ']';
json_h_buf[json_h_len] = '\0';
id(json_hourly_prices_kwh).publish_state(json_h_buf);
auto build32 = [&](int start) -> std::string {
std::string j = "[";
// v1.2.2: Optimized build32 using fixed buffer approach
// Format: [val1,val2,...,val32] where val can be "null" or a float
// Each segment is 32 values, max size ~420 bytes per segment
// Note: id() returns pointer, so use auto (pointer type) with ->
auto build32_fixed = [&](int start, auto sensor) {
char buf[450];
int len = 0;
buf[len++] = '[';
for (int i = start; i < start + 32; i++) {
if (!std::isnan(v[i])) {
char pb[16];
// Use 3 decimal places for negative prices to stay within
// the 255-character HA text sensor state limit.
// Positive prices keep full 4 decimal place precision.
if (v[i] < 0.0f)
snprintf(pb, sizeof(pb), "%.3f", v[i]);
len += snprintf(buf + len, sizeof(buf) - len, "%.3f", v[i]);
else
snprintf(pb, sizeof(pb), "%.4f", v[i]);
j += pb;
len += snprintf(buf + len, sizeof(buf) - len, "%.4f", v[i]);
} else {
j += "null";
len += snprintf(buf + len, sizeof(buf) - len, "null");
}
if (i < start + 31) j += ",";
if (i < start + 31) {
buf[len++] = ',';
}
// Safety: prevent buffer overflow
if (len >= (int)sizeof(buf) - 20) break;
}
j += "]"; return j;
buf[len++] = ']';
buf[len] = '\0';
sensor->publish_state(buf);
};
id(json_15min_prices_kwh_p1_00_00_07_45).publish_state(build32(0));
id(json_15min_prices_kwh_p2_08_00_15_45).publish_state(build32(32));
id(json_15min_prices_kwh_p3_16_00_23_45).publish_state(build32(64));
build32_fixed(0, id(json_15min_prices_kwh_p1_00_00_07_45));
yield(); // v1.2.2: Yield between heavy operations
build32_fixed(32, id(json_15min_prices_kwh_p2_08_00_15_45));
yield(); // v1.2.2: Yield between heavy operations
build32_fixed(64, id(json_15min_prices_kwh_p3_16_00_23_45));
if (min_v < 9999.0f) {
id(min_price).publish_state(min_v);
@@ -1581,8 +1724,12 @@ script:
id(current_price_status_str) = (cur >= 0) ? "Valid" : "Missing";
}
// v1.2.2: Log heap at end
ESP_LOGI("eprices", "recompute_today: done, free heap = %u bytes", heap_caps_get_free_size(MALLOC_CAP_8BIT));
# ---------------------------------------------------------------------------
# RECOMPUTE TOMORROW
# v1.2.2: Optimized JSON string building to prevent heap fragmentation
# ---------------------------------------------------------------------------
- id: recompute_tomorrow
then:
@@ -1590,6 +1737,9 @@ script:
int n = id(tomorrow_entry_count);
if (n == 0) { ESP_LOGW("eprices", "recompute_tomorrow: no entries"); return; }
// v1.2.2: Log heap at start
ESP_LOGI("eprices", "recompute_tomorrow: starting, free heap = %u bytes", heap_caps_get_free_size(MALLOC_CAP_8BIT));
// NOTE: same DST fall-back caveat as recompute_today for legacy grid.
id(tomorrow_hourly_prices).assign(96, NAN);
for (int i = 0; i < n; i++) {
@@ -1620,47 +1770,68 @@ script:
float h_min_v = 9999.0f, h_max_v = -9999.0f;
int h_min_i = 0, h_max_i = 0;
double raw_sum = 0.0; int cnt_kwh = 0;
std::string json_h = "[";
// v1.2.2: Pre-allocate fixed buffer for JSON instead of string concatenation
// This eliminates O(n²) heap allocations from repeated += operations
char json_h_buf[400];
int json_h_len = 0;
json_h_buf[json_h_len++] = '[';
for (int i = 0; i < 24; i++) {
if (h_cnt[i] > 0) {
float ha = h_sums[i] / (float)h_cnt[i];
id(tomorrow_hourly_avg_prices_kwh)[i] = ha;
if (ha < h_min_v) { h_min_v = ha; h_min_i = i; }
if (ha > h_max_v) { h_max_v = ha; h_max_i = i; }
char pb[12]; sprintf(pb, "%.4f", ha); json_h += pb;
int written = snprintf(json_h_buf + json_h_len, sizeof(json_h_buf) - json_h_len, "%.4f", ha);
json_h_len += written;
raw_sum += ha; cnt_kwh++;
} else {
id(tomorrow_hourly_avg_prices_kwh)[i] = NAN;
json_h += "null";
json_h_len += snprintf(json_h_buf + json_h_len, sizeof(json_h_buf) - json_h_len, "null");
}
if (i < 23) {
json_h_buf[json_h_len++] = ',';
}
if (i < 23) json_h += ",";
}
json_h += "]";
id(json_tomorrow_hourly_prices_kwh).publish_state(json_h.c_str());
json_h_buf[json_h_len++] = ']';
json_h_buf[json_h_len] = '\0';
id(json_tomorrow_hourly_prices_kwh).publish_state(json_h_buf);
auto build32 = [&](int start) -> std::string {
std::string j = "[";
// v1.2.2: Optimized build32 using fixed buffer approach
// Note: id() returns pointer, so use auto (pointer type) with ->
auto build32_fixed = [&](int start, auto sensor) {
char buf[450];
int len = 0;
buf[len++] = '[';
for (int i = start; i < start + 32; i++) {
if (!std::isnan(v[i])) {
char pb[16];
// Use 3 decimal places for negative prices to stay within
// the 255-character HA text sensor state limit.
// Positive prices keep full 4 decimal place precision.
if (v[i] < 0.0f)
snprintf(pb, sizeof(pb), "%.3f", v[i]);
len += snprintf(buf + len, sizeof(buf) - len, "%.3f", v[i]);
else
snprintf(pb, sizeof(pb), "%.4f", v[i]);
j += pb;
len += snprintf(buf + len, sizeof(buf) - len, "%.4f", v[i]);
} else {
j += "null";
len += snprintf(buf + len, sizeof(buf) - len, "null");
}
if (i < start + 31) j += ",";
if (i < start + 31) {
buf[len++] = ',';
}
// Safety: prevent buffer overflow
if (len >= (int)sizeof(buf) - 20) break;
}
j += "]"; return j;
buf[len++] = ']';
buf[len] = '\0';
sensor->publish_state(buf);
};
id(json_tomorrow_15min_prices_kwh_p1_00_00_07_45).publish_state(build32(0));
id(json_tomorrow_15min_prices_kwh_p2_08_00_15_45).publish_state(build32(32));
id(json_tomorrow_15min_prices_kwh_p3_16_00_23_45).publish_state(build32(64));
build32_fixed(0, id(json_tomorrow_15min_prices_kwh_p1_00_00_07_45));
yield(); // v1.2.2: Yield between heavy operations
build32_fixed(32, id(json_tomorrow_15min_prices_kwh_p2_08_00_15_45));
yield(); // v1.2.2: Yield between heavy operations
build32_fixed(64, id(json_tomorrow_15min_prices_kwh_p3_16_00_23_45));
if (min_v < 9999.0f) {
id(tomorrow_min_price).publish_state(min_v);
@@ -1691,6 +1862,9 @@ script:
id(tomorrow_current_price_status_str) = (cur >= 0) ? "Valid" : "Missing";
id(tomorrow_last_update_attempt) = id(ha_time).now().timestamp;
// v1.2.2: Log heap at end
ESP_LOGI("eprices", "recompute_tomorrow: done, free heap = %u bytes", heap_caps_get_free_size(MALLOC_CAP_8BIT));
# ---------------------------------------------------------------------------
# MIDNIGHT BRIDGE
# ---------------------------------------------------------------------------
@@ -1769,6 +1943,7 @@ script:
return;
}
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(retry_count)++;
{ char buf[8]; snprintf(buf, sizeof(buf), "%d", id(retry_count)); id(today_price_update_attempts).publish_state(buf); }
@@ -1872,6 +2047,7 @@ script:
return;
}
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_retry_count)++;
{ char buf[8]; snprintf(buf, sizeof(buf), "%d", id(tomorrow_retry_count)); id(tomorrow_price_update_attempts).publish_state(buf); }
@@ -2059,7 +2235,7 @@ script:
id(today_current_max_hourly_price_percentage).publish_state(NAN);
id(last_update_success) = false;
id(update_status_message) = "Cleared awaiting NVS or HTTP";
id(json_hourly_prices_kwh).publish_state("[]");
id(json_hourly_prices_kwh).publish_state("");
id(json_15min_prices_kwh_p1_00_00_07_45).publish_state("");
id(json_15min_prices_kwh_p2_08_00_15_45).publish_state("");
id(json_15min_prices_kwh_p3_16_00_23_45).publish_state("");
@@ -2092,7 +2268,7 @@ script:
id(tomorrow_min_hourly_price).publish_state(NAN);
id(tomorrow_max_hourly_price).publish_state(NAN);
id(tomorrow_current_max_hourly_price_percentage).publish_state(NAN);
id(json_tomorrow_hourly_prices_kwh).publish_state("[]");
id(json_tomorrow_hourly_prices_kwh).publish_state("");
id(json_tomorrow_15min_prices_kwh_p1_00_00_07_45).publish_state("");
id(json_tomorrow_15min_prices_kwh_p2_08_00_15_45).publish_state("");
id(json_tomorrow_15min_prices_kwh_p3_16_00_23_45).publish_state("");
@@ -2101,7 +2277,7 @@ script:
id(tomorrow_min_hourly_price_time_str) = "--:--";
id(tomorrow_max_hourly_price_time_str) = "--:--";
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_price_update_status).update();
id(tomorrow_price_status_message).update();