🐛 persist live energy state across HA restarts #65

Merged
mat merged 2 commits from persist-energy-state into main 2026-07-25 18:08:32 +00:00
Owner

Fix the *_total_energy sensors jumping backwards by thousands of Wh at every HA restart/reload.

Problem

The legacy seed path (state.total_wh += val, used when live_total_wh is None at restart/first-install) summed raw QUANTITY/HOUR buckets WITHOUT the kWh→Wh unit conversion PR #64 added to the reconciliation path, AND only knew the last 24h. Measured live at one restart: solar −24636 Wh, injection −3881 Wh, soutirage −8482 Wh. Sensors are state_class: total_increasing, so each restart corrupted the HA Energy dashboard.

Fix

  • Persist live_total_wh + live_by_hour + last_bucket_ts + total_wh per device via hass.storage.Store (key comwatt.energy_state, version 1).
  • Restore in async_setup_entry BEFORE async_config_entry_first_refresh.
  • Save at the end of each ~2-min poll cycle (not on every stream burst).
  • Apply _server_bucket_to_wh(val, live_wh) to the legacy seed path too (unit-safe first-install seed: skips buckets with no live reference → 0.0 until the stream produces one, ≤ ~15s of energy lost; hourly reconciliation self-heals).

After this, counters are forward-only across restarts; the server endpoint keeps its role as the hourly drift-correction source.

Tests

6 new (RED→GREEN): restore round-trip, poll-cycle persistence, schema versioning, ISO hour round-trip, unit-safe first-install, legacy-seed-skip. 85 total passing, ruff + mypy clean.

Scope

coordinator.py, __init__.py, tests/test_coordinator.py + tests/test_sensor.py (test that directly asserted the buggy raw-accumulation behavior). No changes to stream/sensor/switch/const/manifest.

Follow-ups (non-blocking): swallowed-warning save wrapper, store migrate_func, prune removed-device keys, stream-burst-after-restore test.

Fix the `*_total_energy` sensors jumping backwards by thousands of Wh at every HA restart/reload. ## Problem The legacy seed path (`state.total_wh += val`, used when `live_total_wh is None` at restart/first-install) summed raw `QUANTITY/HOUR` buckets WITHOUT the kWh→Wh unit conversion PR #64 added to the reconciliation path, AND only knew the last 24h. Measured live at one restart: solar −24636 Wh, injection −3881 Wh, soutirage −8482 Wh. Sensors are `state_class: total_increasing`, so each restart corrupted the HA Energy dashboard. ## Fix - Persist `live_total_wh` + `live_by_hour` + `last_bucket_ts` + `total_wh` per device via `hass.storage.Store` (key `comwatt.energy_state`, version 1). - Restore in `async_setup_entry` BEFORE `async_config_entry_first_refresh`. - Save at the end of each ~2-min poll cycle (not on every stream burst). - Apply `_server_bucket_to_wh(val, live_wh)` to the legacy seed path too (unit-safe first-install seed: skips buckets with no live reference → 0.0 until the stream produces one, ≤ ~15s of energy lost; hourly reconciliation self-heals). After this, counters are forward-only across restarts; the server endpoint keeps its role as the hourly drift-correction source. ## Tests 6 new (RED→GREEN): restore round-trip, poll-cycle persistence, schema versioning, ISO hour round-trip, unit-safe first-install, legacy-seed-skip. 85 total passing, ruff + mypy clean. ## Scope `coordinator.py`, `__init__.py`, `tests/test_coordinator.py` + `tests/test_sensor.py` (test that directly asserted the buggy raw-accumulation behavior). No changes to stream/sensor/switch/const/manifest. Follow-ups (non-blocking): swallowed-warning save wrapper, store `migrate_func`, prune removed-device keys, stream-burst-after-restore test.
- Add hass.storage.Store (key comwatt.energy_state, version 1) to
  ComwattCoordinator to persist per-device live_total_wh, live_by_hour,
  last_bucket_ts and total_wh.
- async_load_energy_state() restores state before the first poll so the
  integral accumulator continues from the persisted value rather than
  reseeding from a 24h bucket sum on every restart.
- async_save_energy_state() is called at the end of every _async_update_data
  poll cycle; monotonic-clock fields are intentionally omitted.
- async_setup_entry calls async_load_energy_state() before
  async_config_entry_first_refresh() so restored state is in place for
  the first QUANTITY/HOUR fetch.
- Store JSON keys are string device-ids; int keys converted at boundary only.
- Fix legacy seed path (live_total_wh is None branch): replace raw
  state.total_wh += val with _server_bucket_to_wh(val, live_wh). At first
  install live_wh=0 returns None so the bucket is skipped until the stream
  seeds a live reference (<=15s gap, acceptable per AC5).

Tests: 6 new RED->GREEN, updated test_accumulation_unchanged and
test_energy_sensor_accumulates_new_buckets for unit-safe legacy seed.
📝 fix: accurate _fetch_device_metrics docstring + rename misleading test
All checks were successful
Validate / lint-ruff (pull_request) Successful in 7s
Validate / test-pytest (pull_request) Successful in 3m3s
Validate / type-check-mypy (pull_request) Successful in 3m7s
Validate / lint-ruff (push) Successful in 7s
Validate / test-pytest (push) Successful in 3m0s
Validate / type-check-mypy (push) Successful in 3m7s
fe9b4e4b7c
mat changed title from WIP: fix: persist live energy state across HA restarts (Problem 1) to WIP: 🐛 fix: persist live energy state across HA restarts (Problem 1) 2026-07-25 18:05:09 +00:00
mat changed title from WIP: 🐛 fix: persist live energy state across HA restarts (Problem 1) to WIP: 🐛 persist live energy state across HA restarts (Problem 1) 2026-07-25 18:05:16 +00:00
mat changed title from WIP: 🐛 persist live energy state across HA restarts (Problem 1) to WIP: 🐛 persist live energy state across HA restarts 2026-07-25 18:08:08 +00:00
mat changed title from WIP: 🐛 persist live energy state across HA restarts to 🐛 persist live energy state across HA restarts 2026-07-25 18:08:27 +00:00
mat merged commit ee7670277b into main 2026-07-25 18:08:32 +00:00
mat deleted branch persist-energy-state 2026-07-25 18:08:32 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
mat/homeassistant-comwatt!65
No description provided.