mirror of
https://github.com/Legolas-2025/EPrices.git
synced 2026-08-18 12:44:51 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d22713eb4 | ||
|
|
f7b2513b40 | ||
|
|
344b1e8294 | ||
|
|
275f7cebbf | ||
|
|
8ded49eb80 | ||
|
|
420a745129 | ||
|
|
70c0abcf9e | ||
|
|
8c68afdc80 | ||
|
|
951d99bca1 |
+173
-8
@@ -1,5 +1,169 @@
|
|||||||
# EPrices – Changelog
|
# 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 EUR⁄kWh`
|
||||||
|
- `Tomorrow JSON Hourly Prices EUR⁄kWh`
|
||||||
|
|
||||||
|
**Changed locations in `eprices.yaml`:**
|
||||||
|
- `clear_today_prices` script — `"[]"` → `""`
|
||||||
|
- `clear_tomorrow_prices` script — `"[]"` → `""`
|
||||||
|
- `midnight_bridge_promotion` script — `"[]"` → `""` (where applicable)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v1.2 — 2026-04-06
|
## v1.2 — 2026-04-06
|
||||||
|
|
||||||
### HTTP fetch stuck-flag watchdog
|
### HTTP fetch stuck-flag watchdog
|
||||||
@@ -179,10 +343,10 @@ eprices_vat_rate # e.g. "0.22" (VAT rate as decimal multiplier)
|
|||||||
|
|
||||||
| Name | Entity ID | Notes |
|
| Name | Entity ID | Notes |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Today JSON Hourly Prices EUR⁄kWh | `sensor.eprices_today_json_hourly_prices_eur_kwh` | JSON array, 24 values |
|
| Today JSON Hourly Prices EUR⁄kWh | `sensor.eprices_today_json_hourly_prices_eur_kwh` | JSON array, 24 values; `""` when no data |
|
||||||
| Today JSON 15-Min Prices EUR⁄kWh (P1 00:00-07:45) | `sensor.eprices_today_json_15_min_prices_eur_kwh_p1_00_00_07_45` | JSON array, 32 values |
|
| Today JSON 15-Min Prices EUR⁄kWh (P1 00:00-07:45) | `sensor.eprices_today_json_15_min_prices_eur_kwh_p1_00_00_07_45` | JSON array, 32 values; `""` when no data |
|
||||||
| Today JSON 15-Min Prices EUR⁄kWh (P2 08:00-15:45) | `sensor.eprices_today_json_15_min_prices_eur_kwh_p2_08_00_15_45` | JSON array, 32 values |
|
| Today JSON 15-Min Prices EUR⁄kWh (P2 08:00-15:45) | `sensor.eprices_today_json_15_min_prices_eur_kwh_p2_08_00_15_45` | JSON array, 32 values; `""` when no data |
|
||||||
| Today JSON 15-Min Prices EUR⁄kWh (P3 16:00-23:45) | `sensor.eprices_today_json_15_min_prices_eur_kwh_p3_16_00_23_45` | JSON array, 32 values |
|
| Today JSON 15-Min Prices EUR⁄kWh (P3 16:00-23:45) | `sensor.eprices_today_json_15_min_prices_eur_kwh_p3_16_00_23_45` | JSON array, 32 values; `""` when no data |
|
||||||
| Today Highest Price Time | `sensor.eprices_today_highest_price_time` | HH:MM |
|
| 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 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 |
|
| Today Highest Hourly Price Time | `sensor.eprices_today_highest_hourly_price_time` | HH:00 |
|
||||||
@@ -194,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 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 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 |
|
| Today Entry Count | `sensor.eprices_today_entry_count` | Number of stored price points; diagnostic |
|
||||||
| Tomorrow JSON Hourly Prices EUR⁄kWh | `sensor.eprices_tomorrow_json_hourly_prices_eur_kwh` | JSON array, 24 values |
|
| Tomorrow JSON Hourly Prices EUR⁄kWh | `sensor.eprices_tomorrow_json_hourly_prices_eur_kwh` | JSON array, 24 values; `""` when no data |
|
||||||
| Tomorrow JSON 15-Min Prices EUR⁄kWh (P1 00:00-07:45) | `sensor.eprices_tomorrow_json_15_min_prices_eur_kwh_p1_00_00_07_45` | JSON array, 32 values |
|
| Tomorrow JSON 15-Min Prices EUR⁄kWh (P1 00:00-07:45) | `sensor.eprices_tomorrow_json_15_min_prices_eur_kwh_p1_00_00_07_45` | JSON array, 32 values; `""` when no data |
|
||||||
| Tomorrow JSON 15-Min Prices EUR⁄kWh (P2 08:00-15:45) | `sensor.eprices_tomorrow_json_15_min_prices_eur_kwh_p2_08_00_15_45` | JSON array, 32 values |
|
| Tomorrow JSON 15-Min Prices EUR⁄kWh (P2 08:00-15:45) | `sensor.eprices_tomorrow_json_15_min_prices_eur_kwh_p2_08_00_15_45` | JSON array, 32 values; `""` when no data |
|
||||||
| Tomorrow JSON 15-Min Prices EUR⁄kWh (P3 16:00-23:45) | `sensor.eprices_tomorrow_json_15_min_prices_eur_kwh_p3_16_00_23_45` | JSON array, 32 values |
|
| Tomorrow JSON 15-Min Prices EUR⁄kWh (P3 16:00-23:45) | `sensor.eprices_tomorrow_json_15_min_prices_eur_kwh_p3_16_00_23_45` | JSON array, 32 values; `""` when no data |
|
||||||
| Tomorrow Highest Price Time | `sensor.eprices_tomorrow_highest_price_time` | HH:MM |
|
| 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 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 |
|
| Tomorrow Highest Hourly Price Time | `sensor.eprices_tomorrow_highest_hourly_price_time` | HH:00 |
|
||||||
@@ -238,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 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
|
- **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`
|
- **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
|
||||||
|
|||||||
+102
-76
@@ -85,96 +85,114 @@ The goal of EPrices was to:
|
|||||||
| Manual force update | Button | Button (unchanged) |
|
| Manual force update | Button | Button (unchanged) |
|
||||||
| Midnight bridge | External HA automation at 00:00 | On-device `on_time: 00:00:00` |
|
| Midnight bridge | External HA automation at 00:00 | On-device `on_time: 00:00:00` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Sensor naming
|
### Sensor naming
|
||||||
|
|
||||||
All sensors were renamed with a systematic **Today / Tomorrow** prefix.
|
All sensors were renamed with a systematic **Today / Tomorrow** prefix.
|
||||||
The words "electricity" and "energy" were removed from all sensor names.
|
The words "electricity" and "energy" were removed from all sensor names.
|
||||||
"Next Day" was replaced with "Tomorrow" throughout.
|
"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
|
#### Numeric sensors
|
||||||
|
|
||||||
| entso-e-prices v4.3.1 name | EPrices v1.0 name |
|
| entso-e-prices v4.3.1 name | v4.3.1 entity ID | EPrices v1.2 name | EPrices v1.2 entity ID |
|
||||||
|---|---|
|
|---|---|---|---|
|
||||||
| `Current Electricity Price` | `Today Current Price` |
|
| `Current Electricity Price` | `sensor.entso_e_prices_current_electricity_price` | `Today Current Price` | `sensor.eprices_today_current_price` |
|
||||||
| `Next Electricity Price` | `Today Next Price` |
|
| `Next Electricity Price` | `sensor.entso_e_prices_next_electricity_price` | `Today Next Price` | `sensor.eprices_today_next_price` |
|
||||||
| `Average Electricity Price Today` | `Today Average 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` | `Today Highest 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` | `Today Lowest 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` | `Today Current Hourly 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` | `Today Next 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` | `Today Highest 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` | `Today Lowest 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` | `Today Current Max Hourly Price Percentage` |
|
| `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` | `Today API Fetch Attempts` *(moved to text_sensor)* |
|
| `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` | `Today Entry Count` *(moved to text_sensor)* |
|
| `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` | `Tomorrow Current Price` |
|
| `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` | `Tomorrow Next 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` | `Tomorrow Average 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` | `Tomorrow Highest 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` | `Tomorrow Lowest 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` | `Tomorrow Current Hourly 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` | `Tomorrow Next 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` | `Tomorrow Highest 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` | `Tomorrow Lowest 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` | `Tomorrow Current Max Hourly Price Percentage` |
|
| `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` | `Tomorrow API Fetch Attempts` *(moved to text_sensor)* |
|
| `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` | `Tomorrow Entry Count` *(moved to text_sensor)* |
|
| `Tomorrow Entry Count` | `sensor.entso_e_prices_tomorrow_entry_count` | `Tomorrow Entry Count` *(moved to text_sensor)* | `sensor.eprices_tomorrow_entry_count` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
#### Text sensors
|
#### Text sensors
|
||||||
|
|
||||||
| entso-e-prices v4.3.1 name | EPrices v1.0 name |
|
| 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 EUR⁄kWh JSON` | `Today JSON Hourly Prices EUR⁄kWh` |
|
| `ENTSO-E Hourly Prices EUR⁄kWh JSON` | `sensor.entso_e_prices_entso_e_hourly_prices_eur_kwh_json` | `Today JSON Hourly Prices EUR⁄kWh` | `sensor.eprices_today_json_hourly_prices_eur_kwh` |
|
||||||
| `ENTSO-E 15-Min Prices EUR⁄kWh JSON (P1 00:00-07:45)` | `Today JSON 15-Min Prices EUR⁄kWh (P1 00:00-07:45)` |
|
| `ENTSO-E 15-Min Prices EUR⁄kWh 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 EUR⁄kWh (P1 00:00-07:45)` | `sensor.eprices_today_json_15_min_prices_eur_kwh_p1_00_00_07_45` |
|
||||||
| `ENTSO-E 15-Min Prices EUR⁄kWh JSON (P2 08:00-15:45)` | `Today JSON 15-Min Prices EUR⁄kWh (P2 08:00-15:45)` |
|
| `ENTSO-E 15-Min Prices EUR⁄kWh 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 EUR⁄kWh (P2 08:00-15:45)` | `sensor.eprices_today_json_15_min_prices_eur_kwh_p2_08_00_15_45` |
|
||||||
| `ENTSO-E 15-Min Prices EUR⁄kWh JSON (P3 16:00-23:45)` | `Today JSON 15-Min Prices EUR⁄kWh (P3 16:00-23:45)` |
|
| `ENTSO-E 15-Min Prices EUR⁄kWh 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 EUR⁄kWh (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` | `Today Highest Price Time` |
|
| `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` | `Today Lowest 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` | `Today Highest Hourly 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` | `Today Lowest 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` | `Today Price Update Status` |
|
| `Price Update Status` | `sensor.entso_e_prices_price_update_status` | `Today Price Update Status` | `sensor.eprices_today_price_update_status` |
|
||||||
| `Last Price Update Time` | `Today Data Loaded Time` |
|
| `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` | `Today Price Update Status Message` |
|
| `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` | `Today Current Price Status` |
|
| `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 EUR⁄kWh JSON` | `Tomorrow JSON Hourly Prices EUR⁄kWh` |
|
| `ENTSO-E Next Day Hourly Prices EUR⁄kWh JSON` | `sensor.entso_e_prices_entso_e_next_day_hourly_prices_eur_kwh_json` | `Tomorrow JSON Hourly Prices EUR⁄kWh` | `sensor.eprices_tomorrow_json_hourly_prices_eur_kwh` |
|
||||||
| `ENTSO-E Next Day 15-Min Prices EUR⁄kWh JSON (P1 00:00-07:45)` | `Tomorrow JSON 15-Min Prices EUR⁄kWh (P1 00:00-07:45)` |
|
| `ENTSO-E Next Day 15-Min Prices EUR⁄kWh 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 EUR⁄kWh (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 EUR⁄kWh JSON (P2 08:00-15:45)` | `Tomorrow JSON 15-Min Prices EUR⁄kWh (P2 08:00-15:45)` |
|
| `ENTSO-E Next Day 15-Min Prices EUR⁄kWh 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 EUR⁄kWh (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 EUR⁄kWh JSON (P3 16:00-23:45)` | `Tomorrow JSON 15-Min Prices EUR⁄kWh (P3 16:00-23:45)` |
|
| `ENTSO-E Next Day 15-Min Prices EUR⁄kWh 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 EUR⁄kWh (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` | `Tomorrow Highest Price Time` |
|
| `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` | `Tomorrow Lowest 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` | `Tomorrow Highest Hourly 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` | `Tomorrow Lowest 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` | `Tomorrow Price Update Status` |
|
| `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` | `Tomorrow Data Loaded Time` |
|
| `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` | `Tomorrow Price Update Status Message` |
|
| `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` | `Tomorrow Current Price Status` |
|
| `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` | `Last Reboot` |
|
| `ENTSO-E Last Reboot` | `sensor.entso_e_prices_entso_e_last_reboot` | `Last Reboot` | `sensor.eprices_last_reboot` |
|
||||||
| `Entso-E Today NVS Status` | `Today NVS Status` |
|
| `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` | `Tomorrow 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` | `Last Update Source` |
|
| `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` | `Today Data Date` *(unchanged)* |
|
| `Today Data Date` | `sensor.entso_e_prices_today_data_date` | `Today Data Date` *(unchanged)* | `sensor.eprices_today_data_date` |
|
||||||
| `Tomorrow Data Date` | `Tomorrow Data Date` *(unchanged)* |
|
| `Tomorrow Data Date` | `sensor.entso_e_prices_tomorrow_data_date` | `Tomorrow Data Date` *(unchanged)* | `sensor.eprices_tomorrow_data_date` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
#### Buttons
|
#### Buttons
|
||||||
|
|
||||||
| entso-e-prices v4.3.1 name | EPrices v1.0 name |
|
| 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` | `Force Today's Update` |
|
| `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` | `Force Tomorrow'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` | `Reboot Device` |
|
| `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)
|
### New sensors in EPrices v1.0 (no equivalent in v4.3.1)
|
||||||
|
|
||||||
| Sensor | Purpose |
|
| EPrices v1.2 name | EPrices v1.2 entity ID | Purpose |
|
||||||
|---|---|
|
|---|---|---|
|
||||||
| `Today Last API Fetch Time` | Timestamp of last successful HTTP fetch for today |
|
| `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` | Timestamp of last successful HTTP fetch for tomorrow |
|
| `Tomorrow Last API Fetch Time` | `sensor.eprices_tomorrow_last_api_fetch_time` | Timestamp of last successful HTTP fetch for tomorrow |
|
||||||
| `WiFi Signal` | RSSI in dBm |
|
| `WiFi Signal` | `sensor.eprices_wifi_signal` | RSSI in dBm |
|
||||||
| `Uptime` | Human-readable uptime string |
|
| `Uptime` | `sensor.eprices_uptime` | Human-readable uptime string |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Sensor output changes
|
### 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` |
|
| `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` |
|
| `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 Current Price Status` | `Valid` / `Missing` | `Valid` / `Missing` / `Stale` |
|
||||||
| `Today Data Loaded Time` | Stamped on HTTP fetch only | Stamped on NVS load and HTTP fetch |
|
| `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 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
|
### HA entity ID changes
|
||||||
|
|
||||||
All entity IDs changed due to the node name change from `entso-e-prices` to `eprices`.
|
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_
|
sensor.entso_e_prices_ → sensor.eprices_
|
||||||
button.entso_e_prices_ → button.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
|
### NVS data
|
||||||
|
|
||||||
The NVS namespace changed from `entsoe2` to `eprices`. On first boot after
|
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.
|
trigger a fresh HTTP fetch automatically. This is expected and safe.
|
||||||
No manual NVS erase is required.
|
No manual NVS erase is required.
|
||||||
|
|
||||||
@@ -211,9 +236,10 @@ No manual NVS erase is required.
|
|||||||
## Migration checklist
|
## Migration checklist
|
||||||
|
|
||||||
- [ ] Flash `eprices.yaml` to the device
|
- [ ] Flash `eprices.yaml` to the device
|
||||||
- [ ] Update `secrets.yaml` — rename all `entsoe_` keys to `eprices_`; add `eprices_prov_fee` and `eprices_vat_rate`
|
- [ ] 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: replace `entso_e_prices_` with `eprices_` in all entity IDs
|
- [ ] In all HA automations: bulk replace `sensor.entso_e_prices_` → `sensor.eprices_` and `button.entso_e_prices_` → `button.eprices_`
|
||||||
- [ ] Apply individual sensor name slug renames from the tables above
|
- [ ] 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
|
- [ ] 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`)
|
- [ ] 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
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ Assistant automations are required for any core functionality.
|
|||||||
- **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
|
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
|
- **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
|
||||||
@@ -39,6 +42,12 @@ Assistant automations are required for any core functionality.
|
|||||||
- **ESP32** development board (tested on `esp32dev`)
|
- **ESP32** development board (tested on `esp32dev`)
|
||||||
- ESPHome with **`esp-idf` framework** (required for NVS flash support)
|
- 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
|
## Files
|
||||||
@@ -210,10 +219,10 @@ safe on DST transition days (23-hour and 25-hour days).
|
|||||||
|
|
||||||
| Sensor | Entity ID | Description |
|
| Sensor | Entity ID | Description |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Today JSON Hourly Prices EUR⁄kWh | `sensor.eprices_today_json_hourly_prices_eur_kwh` | JSON array of 24 hourly averages |
|
| Today JSON Hourly Prices EUR⁄kWh | `sensor.eprices_today_json_hourly_prices_eur_kwh` | JSON array of 24 hourly averages; `""` when no data |
|
||||||
| Today JSON 15-Min Prices EUR⁄kWh (P1 00:00-07:45) | `sensor.eprices_today_json_15_min_prices_eur_kwh_p1_00_00_07_45` | JSON array of 32 prices |
|
| Today JSON 15-Min Prices EUR⁄kWh (P1 00:00-07:45) | `sensor.eprices_today_json_15_min_prices_eur_kwh_p1_00_00_07_45` | JSON array of 32 prices; `""` when no data |
|
||||||
| Today JSON 15-Min Prices EUR⁄kWh (P2 08:00-15:45) | `sensor.eprices_today_json_15_min_prices_eur_kwh_p2_08_00_15_45` | JSON array of 32 prices |
|
| Today JSON 15-Min Prices EUR⁄kWh (P2 08:00-15:45) | `sensor.eprices_today_json_15_min_prices_eur_kwh_p2_08_00_15_45` | JSON array of 32 prices; `""` when no data |
|
||||||
| Today JSON 15-Min Prices EUR⁄kWh (P3 16:00-23:45) | `sensor.eprices_today_json_15_min_prices_eur_kwh_p3_16_00_23_45` | JSON array of 32 prices |
|
| Today JSON 15-Min Prices EUR⁄kWh (P3 16:00-23:45) | `sensor.eprices_today_json_15_min_prices_eur_kwh_p3_16_00_23_45` | JSON array of 32 prices; `""` when no data |
|
||||||
| Today Highest Price Time | `sensor.eprices_today_highest_price_time` | Time of highest 15-min price (HH:MM) |
|
| Today 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 Lowest Price Time | `sensor.eprices_today_lowest_price_time` | Time of lowest 15-min price (HH:MM) |
|
||||||
| Today Highest Hourly Price Time | `sensor.eprices_today_highest_hourly_price_time` | Time of highest hourly average (HH:00) |
|
| Today Highest Hourly Price Time | `sensor.eprices_today_highest_hourly_price_time` | Time of highest hourly average (HH:00) |
|
||||||
@@ -240,10 +249,10 @@ safe on DST transition days (23-hour and 25-hour days).
|
|||||||
|
|
||||||
| Sensor | Entity ID | Description |
|
| Sensor | Entity ID | Description |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Tomorrow JSON Hourly Prices EUR⁄kWh | `sensor.eprices_tomorrow_json_hourly_prices_eur_kwh` | JSON array of 24 hourly averages |
|
| Tomorrow JSON Hourly Prices EUR⁄kWh | `sensor.eprices_tomorrow_json_hourly_prices_eur_kwh` | JSON array of 24 hourly averages; `""` when no data |
|
||||||
| Tomorrow JSON 15-Min Prices EUR⁄kWh (P1 00:00-07:45) | `sensor.eprices_tomorrow_json_15_min_prices_eur_kwh_p1_00_00_07_45` | JSON array of 32 prices |
|
| Tomorrow JSON 15-Min Prices EUR⁄kWh (P1 00:00-07:45) | `sensor.eprices_tomorrow_json_15_min_prices_eur_kwh_p1_00_00_07_45` | JSON array of 32 prices; `""` when no data |
|
||||||
| Tomorrow JSON 15-Min Prices EUR⁄kWh (P2 08:00-15:45) | `sensor.eprices_tomorrow_json_15_min_prices_eur_kwh_p2_08_00_15_45` | JSON array of 32 prices |
|
| Tomorrow JSON 15-Min Prices EUR⁄kWh (P2 08:00-15:45) | `sensor.eprices_tomorrow_json_15_min_prices_eur_kwh_p2_08_00_15_45` | JSON array of 32 prices; `""` when no data |
|
||||||
| Tomorrow JSON 15-Min Prices EUR⁄kWh (P3 16:00-23:45) | `sensor.eprices_tomorrow_json_15_min_prices_eur_kwh_p3_16_00_23_45` | JSON array of 32 prices |
|
| Tomorrow JSON 15-Min Prices EUR⁄kWh (P3 16:00-23:45) | `sensor.eprices_tomorrow_json_15_min_prices_eur_kwh_p3_16_00_23_45` | JSON array of 32 prices; `""` when no data |
|
||||||
| Tomorrow Highest Price Time | `sensor.eprices_tomorrow_highest_price_time` | Time of highest 15-min price (HH:MM) |
|
| Tomorrow 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 Lowest Price Time | `sensor.eprices_tomorrow_lowest_price_time` | Time of lowest 15-min price (HH:MM) |
|
||||||
| Tomorrow Highest Hourly Price Time | `sensor.eprices_tomorrow_highest_hourly_price_time` | Time of highest hourly average (HH:00) |
|
| Tomorrow Highest Hourly Price Time | `sensor.eprices_tomorrow_highest_hourly_price_time` | Time of highest hourly average (HH:00) |
|
||||||
|
|||||||
+70
-2
@@ -1,5 +1,74 @@
|
|||||||
# EPrices – Version History
|
# 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 EUR⁄kWh` and `Tomorrow JSON Hourly Prices EUR⁄kWh`
|
||||||
|
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
|
## v1.2 — 2026-04-06
|
||||||
|
|
||||||
### HTTP fetch stuck-flag watchdog
|
### HTTP fetch stuck-flag watchdog
|
||||||
@@ -25,8 +94,7 @@ sensor to undercount after the first scheduled attempt at 13:25.
|
|||||||
### Status message improvements
|
### Status message improvements
|
||||||
|
|
||||||
- `tomorrow_update_status_message` initial value changed from
|
- `tomorrow_update_status_message` initial value changed from
|
||||||
`"Waiting for 13:20"` to `"No data yet"` — removes misleading time
|
`"Waiting for 13:20"` to `"No data yet"`
|
||||||
reference shown after afternoon reboots
|
|
||||||
- `clear_tomorrow_prices` end message changed from
|
- `clear_tomorrow_prices` end message changed from
|
||||||
`"Cleared – awaiting next 13:20 window"` to
|
`"Cleared – awaiting next 13:20 window"` to
|
||||||
`"Cleared – awaiting fetch window"`
|
`"Cleared – awaiting fetch window"`
|
||||||
|
|||||||
+175
-43
@@ -1,9 +1,35 @@
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
# ESPHome: EPrices v1.2
|
# ESPHome: EPrices v1.2.2
|
||||||
#
|
#
|
||||||
# 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.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):
|
# v1.2 changes (2026-04-06):
|
||||||
# - HTTP fetch stuck-flag watchdog (120 s) added to today and tomorrow workers:
|
# - 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
|
# if is_updating_today / is_updating_tomorrow stays true for >120 s (TCP
|
||||||
@@ -86,11 +112,27 @@ esphome:
|
|||||||
} else {
|
} else {
|
||||||
id(boot_time) = 0;
|
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:
|
esp32:
|
||||||
board: esp32dev
|
board: esp32dev
|
||||||
framework:
|
framework:
|
||||||
type: esp-idf
|
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
|
# No include_builtin_idf_components needed — nvs headers are always
|
||||||
# available in esp-idf framework; eprices_nvs.h includes them directly.
|
# available in esp-idf framework; eprices_nvs.h includes them directly.
|
||||||
|
|
||||||
@@ -1345,15 +1387,22 @@ script:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Tokenise comma-separated values
|
// 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> {
|
auto tokenise = [](const std::string &body) -> std::vector<std::string> {
|
||||||
std::vector<std::string> out;
|
std::vector<std::string> out;
|
||||||
|
out.reserve(body.size() / 3); // Pre-allocate estimate
|
||||||
std::string tok;
|
std::string tok;
|
||||||
|
int char_count = 0;
|
||||||
for (char c : body) {
|
for (char c : body) {
|
||||||
if (c == ',') {
|
if (c == ',') {
|
||||||
if (!tok.empty()) { out.push_back(tok); tok.clear(); }
|
if (!tok.empty()) { out.push_back(tok); tok.clear(); }
|
||||||
} else if (c > ' ') {
|
} else if (c > ' ') {
|
||||||
tok += 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);
|
if (!tok.empty()) out.push_back(tok);
|
||||||
return out;
|
return out;
|
||||||
@@ -1398,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++) {
|
for (size_t i = 0; i < n; i++) {
|
||||||
if (prc_toks[i] == "null") continue;
|
if (prc_toks[i] == "null") continue;
|
||||||
char *end1 = nullptr, *end2 = nullptr;
|
char *end1 = nullptr, *end2 = nullptr;
|
||||||
@@ -1409,13 +1461,17 @@ script:
|
|||||||
float eur_kwh = (float)((double)raw_mwh * (raw_mwh >= 0 ? MULT_POS : MULT_NEG) / 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);
|
||||||
|
|
||||||
|
// 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_entry_count) = (int)id(price_timestamps_today).size();
|
||||||
id(today_date_str) = std::string(today_buf);
|
id(today_date_str) = std::string(today_buf);
|
||||||
|
|
||||||
ESP_LOGI("eprices", "parse_today: stored %d entries, date=%s",
|
// v1.2.2: Log heap after parsing
|
||||||
id(today_entry_count), today_buf);
|
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
|
# JSON PARSER – TOMORROW
|
||||||
@@ -1452,15 +1508,23 @@ script:
|
|||||||
return;
|
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> {
|
auto tokenise = [](const std::string &body) -> std::vector<std::string> {
|
||||||
std::vector<std::string> out;
|
std::vector<std::string> out;
|
||||||
|
out.reserve(body.size() / 3); // Pre-allocate estimate
|
||||||
std::string tok;
|
std::string tok;
|
||||||
|
int char_count = 0;
|
||||||
for (char c : body) {
|
for (char c : body) {
|
||||||
if (c == ',') {
|
if (c == ',') {
|
||||||
if (!tok.empty()) { out.push_back(tok); tok.clear(); }
|
if (!tok.empty()) { out.push_back(tok); tok.clear(); }
|
||||||
} else if (c > ' ') {
|
} else if (c > ' ') {
|
||||||
tok += 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);
|
if (!tok.empty()) out.push_back(tok);
|
||||||
return out;
|
return out;
|
||||||
@@ -1486,6 +1550,9 @@ script:
|
|||||||
snprintf(tmr_buf, sizeof(tmr_buf), "%04d%02d%02d",
|
snprintf(tmr_buf, sizeof(tmr_buf), "%04d%02d%02d",
|
||||||
tmr_tm->tm_year + 1900, tmr_tm->tm_mon + 1, tmr_tm->tm_mday);
|
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++) {
|
for (size_t i = 0; i < n; i++) {
|
||||||
if (prc_toks[i] == "null") continue;
|
if (prc_toks[i] == "null") continue;
|
||||||
char *end1 = nullptr, *end2 = nullptr;
|
char *end1 = nullptr, *end2 = nullptr;
|
||||||
@@ -1497,16 +1564,21 @@ script:
|
|||||||
float eur_kwh = (float)((double)raw_mwh * (raw_mwh >= 0 ? MULT_POS : MULT_NEG) / 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);
|
||||||
|
|
||||||
|
// 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_entry_count) = (int)id(price_timestamps_tomorrow).size();
|
||||||
id(tomorrow_date_str) = std::string(tmr_buf);
|
id(tomorrow_date_str) = std::string(tmr_buf);
|
||||||
|
|
||||||
ESP_LOGI("eprices", "parse_tomorrow: stored %d entries, date=%s",
|
// v1.2.2: Log heap after parsing
|
||||||
id(tomorrow_entry_count), tmr_buf);
|
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
|
# 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
|
- id: recompute_today
|
||||||
then:
|
then:
|
||||||
@@ -1514,6 +1586,9 @@ script:
|
|||||||
int n = id(today_entry_count);
|
int n = id(today_entry_count);
|
||||||
if (n == 0) { ESP_LOGW("eprices", "recompute_today: no entries"); return; }
|
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.
|
// Map timestamps into legacy 96-slot grid.
|
||||||
// NOTE: on DST fall-back days (25h) the second occurrence of the
|
// NOTE: on DST fall-back days (25h) the second occurrence of the
|
||||||
// repeated hour overwrites the first in this grid.
|
// repeated hour overwrites the first in this grid.
|
||||||
@@ -1547,47 +1622,73 @@ script:
|
|||||||
float h_min_v = 9999.0f, h_max_v = -9999.0f;
|
float h_min_v = 9999.0f, h_max_v = -9999.0f;
|
||||||
int h_min_i = 0, h_max_i = 0;
|
int h_min_i = 0, h_max_i = 0;
|
||||||
double raw_sum = 0.0; int cnt_kwh = 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++) {
|
for (int i = 0; i < 24; i++) {
|
||||||
if (h_cnt[i] > 0) {
|
if (h_cnt[i] > 0) {
|
||||||
float ha = h_sums[i] / (float)h_cnt[i];
|
float ha = h_sums[i] / (float)h_cnt[i];
|
||||||
id(hourly_avg_prices_kwh)[i] = ha;
|
id(hourly_avg_prices_kwh)[i] = ha;
|
||||||
if (ha < h_min_v) { h_min_v = ha; h_min_i = i; }
|
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; }
|
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++;
|
raw_sum += ha; cnt_kwh++;
|
||||||
} else {
|
} else {
|
||||||
id(hourly_avg_prices_kwh)[i] = NAN;
|
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 += ",";
|
if (i < 23) {
|
||||||
|
json_h_buf[json_h_len++] = ',';
|
||||||
}
|
}
|
||||||
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 {
|
// v1.2.2: Optimized build32 using fixed buffer approach
|
||||||
std::string j = "[";
|
// 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++) {
|
for (int i = start; i < start + 32; i++) {
|
||||||
if (!std::isnan(v[i])) {
|
if (!std::isnan(v[i])) {
|
||||||
char pb[16];
|
|
||||||
// Use 3 decimal places for negative prices to stay within
|
// Use 3 decimal places for negative prices to stay within
|
||||||
// the 255-character HA text sensor state limit.
|
// the 255-character HA text sensor state limit.
|
||||||
// Positive prices keep full 4 decimal place precision.
|
// Positive prices keep full 4 decimal place precision.
|
||||||
if (v[i] < 0.0f)
|
if (v[i] < 0.0f)
|
||||||
snprintf(pb, sizeof(pb), "%.3f", v[i]);
|
len += snprintf(buf + len, sizeof(buf) - len, "%.3f", v[i]);
|
||||||
else
|
else
|
||||||
snprintf(pb, sizeof(pb), "%.4f", v[i]);
|
len += snprintf(buf + len, sizeof(buf) - len, "%.4f", v[i]);
|
||||||
j += pb;
|
|
||||||
} else {
|
} else {
|
||||||
j += "null";
|
len += snprintf(buf + len, sizeof(buf) - len, "null");
|
||||||
}
|
}
|
||||||
if (i < start + 31) j += ",";
|
if (i < start + 31) {
|
||||||
|
buf[len++] = ',';
|
||||||
}
|
}
|
||||||
j += "]"; return j;
|
// Safety: prevent buffer overflow
|
||||||
|
if (len >= (int)sizeof(buf) - 20) break;
|
||||||
|
}
|
||||||
|
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));
|
build32_fixed(0, id(json_15min_prices_kwh_p1_00_00_07_45));
|
||||||
id(json_15min_prices_kwh_p3_16_00_23_45).publish_state(build32(64));
|
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) {
|
if (min_v < 9999.0f) {
|
||||||
id(min_price).publish_state(min_v);
|
id(min_price).publish_state(min_v);
|
||||||
@@ -1623,8 +1724,12 @@ script:
|
|||||||
id(current_price_status_str) = (cur >= 0) ? "Valid" : "Missing";
|
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
|
# RECOMPUTE TOMORROW
|
||||||
|
# v1.2.2: Optimized JSON string building to prevent heap fragmentation
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
- id: recompute_tomorrow
|
- id: recompute_tomorrow
|
||||||
then:
|
then:
|
||||||
@@ -1632,6 +1737,9 @@ script:
|
|||||||
int n = id(tomorrow_entry_count);
|
int n = id(tomorrow_entry_count);
|
||||||
if (n == 0) { ESP_LOGW("eprices", "recompute_tomorrow: no entries"); return; }
|
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.
|
// NOTE: same DST fall-back caveat as recompute_today for legacy grid.
|
||||||
id(tomorrow_hourly_prices).assign(96, NAN);
|
id(tomorrow_hourly_prices).assign(96, NAN);
|
||||||
for (int i = 0; i < n; i++) {
|
for (int i = 0; i < n; i++) {
|
||||||
@@ -1662,47 +1770,68 @@ script:
|
|||||||
float h_min_v = 9999.0f, h_max_v = -9999.0f;
|
float h_min_v = 9999.0f, h_max_v = -9999.0f;
|
||||||
int h_min_i = 0, h_max_i = 0;
|
int h_min_i = 0, h_max_i = 0;
|
||||||
double raw_sum = 0.0; int cnt_kwh = 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++) {
|
for (int i = 0; i < 24; i++) {
|
||||||
if (h_cnt[i] > 0) {
|
if (h_cnt[i] > 0) {
|
||||||
float ha = h_sums[i] / (float)h_cnt[i];
|
float ha = h_sums[i] / (float)h_cnt[i];
|
||||||
id(tomorrow_hourly_avg_prices_kwh)[i] = ha;
|
id(tomorrow_hourly_avg_prices_kwh)[i] = ha;
|
||||||
if (ha < h_min_v) { h_min_v = ha; h_min_i = i; }
|
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; }
|
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++;
|
raw_sum += ha; cnt_kwh++;
|
||||||
} else {
|
} else {
|
||||||
id(tomorrow_hourly_avg_prices_kwh)[i] = NAN;
|
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 += ",";
|
if (i < 23) {
|
||||||
|
json_h_buf[json_h_len++] = ',';
|
||||||
}
|
}
|
||||||
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 {
|
// v1.2.2: Optimized build32 using fixed buffer approach
|
||||||
std::string j = "[";
|
// 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++) {
|
for (int i = start; i < start + 32; i++) {
|
||||||
if (!std::isnan(v[i])) {
|
if (!std::isnan(v[i])) {
|
||||||
char pb[16];
|
|
||||||
// Use 3 decimal places for negative prices to stay within
|
// Use 3 decimal places for negative prices to stay within
|
||||||
// the 255-character HA text sensor state limit.
|
// the 255-character HA text sensor state limit.
|
||||||
// Positive prices keep full 4 decimal place precision.
|
// Positive prices keep full 4 decimal place precision.
|
||||||
if (v[i] < 0.0f)
|
if (v[i] < 0.0f)
|
||||||
snprintf(pb, sizeof(pb), "%.3f", v[i]);
|
len += snprintf(buf + len, sizeof(buf) - len, "%.3f", v[i]);
|
||||||
else
|
else
|
||||||
snprintf(pb, sizeof(pb), "%.4f", v[i]);
|
len += snprintf(buf + len, sizeof(buf) - len, "%.4f", v[i]);
|
||||||
j += pb;
|
|
||||||
} else {
|
} else {
|
||||||
j += "null";
|
len += snprintf(buf + len, sizeof(buf) - len, "null");
|
||||||
}
|
}
|
||||||
if (i < start + 31) j += ",";
|
if (i < start + 31) {
|
||||||
|
buf[len++] = ',';
|
||||||
}
|
}
|
||||||
j += "]"; return j;
|
// Safety: prevent buffer overflow
|
||||||
|
if (len >= (int)sizeof(buf) - 20) break;
|
||||||
|
}
|
||||||
|
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));
|
build32_fixed(0, id(json_tomorrow_15min_prices_kwh_p1_00_00_07_45));
|
||||||
id(json_tomorrow_15min_prices_kwh_p3_16_00_23_45).publish_state(build32(64));
|
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) {
|
if (min_v < 9999.0f) {
|
||||||
id(tomorrow_min_price).publish_state(min_v);
|
id(tomorrow_min_price).publish_state(min_v);
|
||||||
@@ -1733,6 +1862,9 @@ script:
|
|||||||
id(tomorrow_current_price_status_str) = (cur >= 0) ? "Valid" : "Missing";
|
id(tomorrow_current_price_status_str) = (cur >= 0) ? "Valid" : "Missing";
|
||||||
id(tomorrow_last_update_attempt) = id(ha_time).now().timestamp;
|
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
|
# MIDNIGHT BRIDGE
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -2103,7 +2235,7 @@ script:
|
|||||||
id(today_current_max_hourly_price_percentage).publish_state(NAN);
|
id(today_current_max_hourly_price_percentage).publish_state(NAN);
|
||||||
id(last_update_success) = false;
|
id(last_update_success) = false;
|
||||||
id(update_status_message) = "Cleared – awaiting NVS or HTTP";
|
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_p1_00_00_07_45).publish_state("");
|
||||||
id(json_15min_prices_kwh_p2_08_00_15_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("");
|
id(json_15min_prices_kwh_p3_16_00_23_45).publish_state("");
|
||||||
@@ -2136,7 +2268,7 @@ script:
|
|||||||
id(tomorrow_min_hourly_price).publish_state(NAN);
|
id(tomorrow_min_hourly_price).publish_state(NAN);
|
||||||
id(tomorrow_max_hourly_price).publish_state(NAN);
|
id(tomorrow_max_hourly_price).publish_state(NAN);
|
||||||
id(tomorrow_current_max_hourly_price_percentage).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_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_p2_08_00_15_45).publish_state("");
|
||||||
id(json_tomorrow_15min_prices_kwh_p3_16_00_23_45).publish_state("");
|
id(json_tomorrow_15min_prices_kwh_p3_16_00_23_45).publish_state("");
|
||||||
|
|||||||
Reference in New Issue
Block a user