mirror of
https://github.com/Legolas-2025/EPrices.git
synced 2026-08-18 12:44:51 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c954569125 | ||
|
|
fc48dd12ff | ||
|
|
e9c66c7d41 | ||
|
|
ea22c138d6 | ||
|
|
9d22713eb4 | ||
|
|
f7b2513b40 | ||
|
|
344b1e8294 | ||
|
|
275f7cebbf | ||
|
|
8ded49eb80 |
+153
@@ -1,5 +1,158 @@
|
||||
# EPrices – Changelog
|
||||
|
||||
## v1.2.3 — 2026-08-04
|
||||
|
||||
### Uptime sensor bucket display and logbook change-detection
|
||||
|
||||
The `Uptime` text sensor previously published a high-precision human-readable
|
||||
uptime string (e.g. `3 h 22 min`, `12 d 4 h 17 min`) on every internal
|
||||
`uptime` sensor update, which runs every 60 seconds. In the Home Assistant
|
||||
activity log, this produced one state change per minute during the first day
|
||||
after a reboot — a noisy stream redundant with the existing `Last Reboot`
|
||||
text sensor.
|
||||
|
||||
The uptime lambda now formats the value into coarse hourly / daily / monthly
|
||||
buckets and only calls `publish_state` when the bucket string actually
|
||||
changes. Result: **one logbook entry per hour** for the first day, **one per
|
||||
day** through the first month, **one per month** through the first year, and
|
||||
**one per month** after that.
|
||||
|
||||
**New display buckets:**
|
||||
|
||||
| Boot age | State | Window |
|
||||
|---|---|---|
|
||||
| 0 – 59 min | `< 1 hour` | 1 h |
|
||||
| 1 – 23 h | `> N hour(s)` | 1 h |
|
||||
| 1 – 30 d | `> N day(s)` | 1 d |
|
||||
| 1 – 11 mo | `> N month(s)` | 30 d |
|
||||
| 12+ mo | `> N year(s)` or `> N year(s) N month(s)` | 30 d |
|
||||
|
||||
Singular/plural is handled in C++ (`1 hour` vs `2 hours`, `1 day` vs
|
||||
`2 days`, etc.). Conventions: 1 month = 30 days, 1 year = 12 months —
|
||||
consistent with the existing `d / 30` month convention used elsewhere in
|
||||
the file.
|
||||
|
||||
**New global:**
|
||||
- `last_published_uptime` (type: `std::string`, `restore_value: false`,
|
||||
`initial_value: '""'`) — tracks the most recently published bucket value.
|
||||
The uptime lambda compares against it and only calls `publish_state` when
|
||||
the bucket string differs.
|
||||
|
||||
The `update_interval: 60s` on the internal `uptime` sensor is kept as-is so
|
||||
the lambda still runs every minute and reliably catches hour-boundary
|
||||
transitions even under transient load.
|
||||
|
||||
**Changed locations in `eprices.yaml`:**
|
||||
- `globals:` — added `last_published_uptime`
|
||||
- Internal `uptime` sensor `on_raw_value` lambda — replaced minute-precision
|
||||
format with bucket display + change-detection guard
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
@@ -24,12 +24,16 @@ Assistant automations are required for any core functionality.
|
||||
- **Midnight bridge** — tomorrow's data automatically becomes today at 00:00, fully on-device
|
||||
- **Auto-retry logic** — up to 8 HTTP fetch attempts for both today and tomorrow,
|
||||
with a **120-second stuck-fetch watchdog** that unblocks retries after TCP-level stalls
|
||||
- **Task watchdog stability** — watchdog timeout increased to 40s, idle task checking disabled;
|
||||
periodic `yield()` calls and optimised JSON building prevent spontaneous reboots during
|
||||
heavy parsing when full price data arrives (~13:55)
|
||||
- **DST-safe** — uses UNIX timestamps and binary search throughout, no hour-slot arithmetic
|
||||
- **Staleness detection** — `Today Current Price Status` shows `Stale` if stored date mismatches today
|
||||
- **Tomorrow live sensors** evaluate at `now + 86400s` — reflecting tomorrow at the same local time
|
||||
- Supports **any Energy-Charts bidding zone** (SI, DE-LU, AT, FR, HR, HU and more)
|
||||
- Full **diagnostic sensor suite** — NVS status, fetch attempts, API fetch times,
|
||||
data loaded times, WiFi signal, human-readable uptime
|
||||
data loaded times, WiFi signal, human-readable bucketed uptime (one HA logbook
|
||||
entry per hour / day / month — no per-minute churn)
|
||||
- All fee and VAT settings configurable via `secrets.yaml` — **no code changes needed**
|
||||
|
||||
---
|
||||
@@ -39,6 +43,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
|
||||
@@ -210,10 +220,10 @@ safe on DST transition days (23-hour and 25-hour days).
|
||||
|
||||
| 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 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 (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 (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 EUR⁄kWh | `sensor.eprices_today_json_hourly_prices_eur_kwh` | JSON array of 24 hourly averages; `""` when no data |
|
||||
| Today JSON 15-Min Prices EUR⁄kWh (P1 00:00-07:45) | `sensor.eprices_today_json_15_min_prices_eur_kwh_p1_00_00_07_45` | JSON array of 32 prices; `""` when no data |
|
||||
| Today JSON 15-Min Prices EUR⁄kWh (P2 08:00-15:45) | `sensor.eprices_today_json_15_min_prices_eur_kwh_p2_08_00_15_45` | JSON array of 32 prices; `""` when no data |
|
||||
| Today JSON 15-Min Prices EUR⁄kWh (P3 16:00-23:45) | `sensor.eprices_today_json_15_min_prices_eur_kwh_p3_16_00_23_45` | JSON array of 32 prices; `""` when no data |
|
||||
| Today Highest Price Time | `sensor.eprices_today_highest_price_time` | Time of highest 15-min price (HH:MM) |
|
||||
| Today Lowest Price Time | `sensor.eprices_today_lowest_price_time` | Time of lowest 15-min price (HH:MM) |
|
||||
| Today Highest Hourly Price Time | `sensor.eprices_today_highest_hourly_price_time` | Time of highest hourly average (HH:00) |
|
||||
@@ -240,10 +250,10 @@ safe on DST transition days (23-hour and 25-hour days).
|
||||
|
||||
| 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 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 (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 (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 EUR⁄kWh | `sensor.eprices_tomorrow_json_hourly_prices_eur_kwh` | JSON array of 24 hourly averages; `""` when no data |
|
||||
| Tomorrow JSON 15-Min Prices EUR⁄kWh (P1 00:00-07:45) | `sensor.eprices_tomorrow_json_15_min_prices_eur_kwh_p1_00_00_07_45` | JSON array of 32 prices; `""` when no data |
|
||||
| Tomorrow JSON 15-Min Prices EUR⁄kWh (P2 08:00-15:45) | `sensor.eprices_tomorrow_json_15_min_prices_eur_kwh_p2_08_00_15_45` | JSON array of 32 prices; `""` when no data |
|
||||
| Tomorrow JSON 15-Min Prices EUR⁄kWh (P3 16:00-23:45) | `sensor.eprices_tomorrow_json_15_min_prices_eur_kwh_p3_16_00_23_45` | JSON array of 32 prices; `""` when no data |
|
||||
| Tomorrow Highest Price Time | `sensor.eprices_tomorrow_highest_price_time` | Time of highest 15-min price (HH:MM) |
|
||||
| Tomorrow Lowest Price Time | `sensor.eprices_tomorrow_lowest_price_time` | Time of lowest 15-min price (HH:MM) |
|
||||
| Tomorrow Highest Hourly Price Time | `sensor.eprices_tomorrow_highest_hourly_price_time` | Time of highest hourly average (HH:00) |
|
||||
@@ -256,7 +266,7 @@ safe on DST transition days (23-hour and 25-hour days).
|
||||
| Sensor | Entity ID | Description |
|
||||
|---|---|---|
|
||||
| Last Reboot | `sensor.eprices_last_reboot` | Timestamp of last device boot |
|
||||
| Uptime | `sensor.eprices_uptime` | Human-readable: `45 s` / `5 min` / `3 h 22 min` / `12 d 4 h` / `4 months 12 d` |
|
||||
| Uptime | `sensor.eprices_uptime` | Human-readable bucketed: `< 1 hour` / `> N hours` / `> N days` / `> N months` / `> N year(s) N month(s)` — one logbook entry per hour, day, or month |
|
||||
| WiFi Signal | `sensor.eprices_wifi_signal` | RSSI in dBm, updated every 60s |
|
||||
| Last Update Source | `sensor.eprices_last_update_source` | `NVS_boot` / `HTTP_today` / `midnight_bridge` / `NVS_api` etc. |
|
||||
| Today Data Date | `sensor.eprices_today_data_date` | Date of currently stored today data |
|
||||
|
||||
+74
@@ -1,5 +1,79 @@
|
||||
# EPrices – Version History
|
||||
|
||||
## v1.2.3 — 2026-08-04
|
||||
|
||||
Quietness patch for the Home Assistant activity log. No new sensors, no
|
||||
secrets changes, no entity ID changes. Drop-in replacement for v1.2.2.
|
||||
|
||||
### Uptime display bucketed and change-detected
|
||||
|
||||
The `Uptime` text sensor previously published a new value every minute
|
||||
(e.g. `3 h 22 min`, `12 d 4 h 17 min`) because the underlying internal
|
||||
`uptime` sensor updates every 60 seconds and the lambda always called
|
||||
`publish_state`. In the HA activity log this produced a noisy stream of
|
||||
state changes redundant with the existing `Last Reboot` text sensor.
|
||||
|
||||
The lambda now formats uptime into coarse buckets and only publishes when
|
||||
the bucket string changes — **one logbook entry per hour** for the first
|
||||
day, then **one per day**, then **one per month**.
|
||||
|
||||
**Buckets:**
|
||||
|
||||
| Age | Display |
|
||||
|---|---|
|
||||
| 0 – 59 min | `< 1 hour` |
|
||||
| 1 – 23 h | `> 1 hour` ... `> 23 hours` |
|
||||
| 1 – 30 d | `> 1 day` ... `> 30 days` |
|
||||
| 1 – 11 mo | `> 1 month` ... `> 11 months` |
|
||||
| 12+ mo | `> 1 year` ... `> N year(s) N month(s)` |
|
||||
|
||||
**New global:** `last_published_uptime` (`std::string`) — tracks the last
|
||||
published bucket so the lambda can skip publishing when the value hasn't
|
||||
crossed a boundary.
|
||||
|
||||
See `CHANGELOG.md` for full implementation details.
|
||||
|
||||
---
|
||||
|
||||
## 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,
|
||||
|
||||
+207
-59
@@ -1,9 +1,38 @@
|
||||
# =============================================================================
|
||||
# ESPHome: EPrices v1.2.1
|
||||
# ESPHome: EPrices v1.2.3
|
||||
#
|
||||
# Electricity price data provided by Energy-Charts (https://energy-charts.info)
|
||||
# under CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/).
|
||||
#
|
||||
# v1.2.3 uptime log noise reduction (2026-08-04):
|
||||
# - Uptime text sensor now uses coarse bucketed display instead of minute
|
||||
# precision: "< 1 hour" / "> N hour(s)" / "> N day(s)" / "> N month(s)" /
|
||||
# "> N year(s)" / "> N year(s) N month(s)"
|
||||
# Buckets update once per hour (first day), once per day (first month),
|
||||
# once per month (first year) — eliminates per-minute state churn in
|
||||
# the HA activity log
|
||||
# - Added global last_published_uptime (std::string) to track the last
|
||||
# published bucket value; the uptime lambda only calls publish_state
|
||||
# when the bucket string actually changes
|
||||
# - Singular/plural handled in C++ (1 hour vs 2 hours, 1 day vs 2 days, etc.)
|
||||
# - Conventions: 1 month = 30 days, 1 year = 12 months — consistent with
|
||||
# the existing d / 30 month convention used elsewhere in the file
|
||||
#
|
||||
# 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
|
||||
@@ -103,6 +132,8 @@ esphome:
|
||||
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
|
||||
@@ -110,6 +141,12 @@ esp32:
|
||||
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.
|
||||
|
||||
@@ -486,6 +523,10 @@ globals:
|
||||
- id: boot_time
|
||||
type: time_t
|
||||
initial_value: '0'
|
||||
- id: last_published_uptime
|
||||
type: std::string
|
||||
restore_value: false
|
||||
initial_value: '""'
|
||||
- id: last_successful_update
|
||||
type: time_t
|
||||
initial_value: '0'
|
||||
@@ -841,27 +882,44 @@ sensor:
|
||||
then:
|
||||
- lambda: |-
|
||||
uint32_t s = (uint32_t)x;
|
||||
char buf[32];
|
||||
if (s < 120) {
|
||||
snprintf(buf, sizeof(buf), "%u s", s);
|
||||
} else if (s < 48 * 3600) {
|
||||
char buf[48];
|
||||
|
||||
if (s < 3600) {
|
||||
// < 1 hour: one state, no logbook churn
|
||||
snprintf(buf, sizeof(buf), "< 1 hour");
|
||||
} else if (s < 86400UL) {
|
||||
// 1..23 hours — bucket by full hour
|
||||
uint32_t h = s / 3600;
|
||||
uint32_t m = (s % 3600) / 60;
|
||||
if (h < 2)
|
||||
snprintf(buf, sizeof(buf), "%u min", s / 60);
|
||||
else
|
||||
snprintf(buf, sizeof(buf), "%u h %u min", h, m);
|
||||
} else if (s < 90UL * 86400UL) {
|
||||
uint32_t d = s / 86400;
|
||||
uint32_t h = (s % 86400) / 3600;
|
||||
snprintf(buf, sizeof(buf), "%u d %u h", d, h);
|
||||
snprintf(buf, sizeof(buf), "> %lu hour%s", h, h == 1 ? "" : "s");
|
||||
} else if (s < 31UL * 86400UL) {
|
||||
// 1..30 days — bucket by full day
|
||||
uint32_t d = s / 86400UL;
|
||||
snprintf(buf, sizeof(buf), "> %lu day%s", d, d == 1 ? "" : "s");
|
||||
} else if (s < 12UL * 30UL * 86400UL) {
|
||||
// 1..11 months — bucket by full month (1 month = 30 days)
|
||||
uint32_t mo = s / (30UL * 86400UL);
|
||||
snprintf(buf, sizeof(buf), "> %lu month%s", mo, mo == 1 ? "" : "s");
|
||||
} else {
|
||||
uint32_t d = s / 86400;
|
||||
uint32_t mo = d / 30;
|
||||
uint32_t rem = d % 30;
|
||||
snprintf(buf, sizeof(buf), "%u months %u d", mo, rem);
|
||||
// 1 year and up — year + optional months (1 year = 12 months)
|
||||
uint32_t mo_total = s / (30UL * 86400UL);
|
||||
uint32_t y = mo_total / 12;
|
||||
uint32_t mo = mo_total % 12;
|
||||
if (mo == 0) {
|
||||
snprintf(buf, sizeof(buf), "> %lu year%s", y, y == 1 ? "" : "s");
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf), "> %lu year%s %lu month%s",
|
||||
y, y == 1 ? "" : "s",
|
||||
mo, mo == 1 ? "" : "s");
|
||||
}
|
||||
}
|
||||
|
||||
// Only publish on actual bucket change — prevents
|
||||
// per-minute state churn in HA's activity log
|
||||
std::string new_state(buf);
|
||||
if (new_state != id(last_published_uptime)) {
|
||||
id(last_published_uptime) = new_state;
|
||||
id(uptime_text).publish_state(new_state);
|
||||
}
|
||||
id(uptime_text).publish_state(buf);
|
||||
|
||||
# =============================================================================
|
||||
# TEXT SENSORS – TODAY
|
||||
@@ -1364,15 +1422,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;
|
||||
@@ -1417,6 +1482,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;
|
||||
@@ -1428,13 +1496,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
|
||||
@@ -1471,15 +1543,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;
|
||||
@@ -1505,6 +1585,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;
|
||||
@@ -1516,16 +1599,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:
|
||||
@@ -1533,6 +1621,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.
|
||||
@@ -1566,47 +1657,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);
|
||||
@@ -1642,8 +1759,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:
|
||||
@@ -1651,6 +1772,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++) {
|
||||
@@ -1681,47 +1805,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);
|
||||
@@ -1752,6 +1897,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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user