WIP: Real-time energy via trapezoidal ∫W·dt over the live power stream (streaming 4/5) #59

Closed
mat wants to merge 1 commit from feat/stream-energy into feat/stream-power
Owner

Slice 4 of 5 (stacked on #58). The energy total now rises in real time with the power stream, instead of jumping once per hour with a 1h offset.

What

  • _EnergyState extended with last_power_w / last_power_t / live_total_wh / live_by_hour (UTC-hour buckets, populated for Slice 5).
  • New ComwattCoordinator.integrate_live_energy(device_powers): trapezoidal delta_Wh = (W_now + W_prev) / 2 × dt_hours, seeded from the poll's total_wh on the first stream burst, bucketed by UTC calendar hour into live_by_hour.
  • _fetch_device_metrics early-returns energy = live_total_wh (no QUANTITY fetch) once the stream has taken over → prevents double-counting between the poll's bucket accumulation and the stream's ∫W·dt.
  • stream.py: extracted _compute_device_powers pure helper; _apply_power_updates signature refactored to (device_powers, devices_data); _process_batch integrates live energy on every batch then notifies via async_update_listeners().

Why trapezoidal + separate live_total_wh

The stream pushes power (~15s); energy = ∫W·dt. A separate live_total_wh (seeded from the poll, then stream-owned) avoids double-counting the server's hourly QUANTITY buckets against the live accumulation. The 1h offset (#5/#42) dissolves because the energy is computed at the actual moment of consumption — last_updated is correct by construction, no backdating needed.

Scope / merge-safety

  • UPDATE_INTERVAL unchanged (2 min). QUANTITY fetch NOT removed (Slice 5 re-uses it for reconciliation). No reconciliation in this slice — live_by_hour is populated but not consumed.
  • Touches only coordinator.py, stream.py, test_coordinator.py, test_stream.py.

Tests

6 new tests (T7-T12) + 4 updated. T8/T9/T12 assert exact trapezoidal values with patched clocks (not sleeps). T10 verifies the double-count prevention (QUANTITY skipped when stream active). 63/63 passing, ruff + mypy clean.

Drift note

If the stream permanently dies, the energy freezes at the last live value until HA restart. Slice 5's reconciliation (vs server QUANTITY/HOUR for completed UTC hours) bounds this drift — that's the final slice.

Stacking

Base: feat/stream-power (PR #58). Rebase once #58 merges.

Slice 4 of 5 (stacked on #58). The energy total now rises in real time with the power stream, instead of jumping once per hour with a 1h offset. ## What - `_EnergyState` extended with `last_power_w` / `last_power_t` / `live_total_wh` / `live_by_hour` (UTC-hour buckets, populated for Slice 5). - New `ComwattCoordinator.integrate_live_energy(device_powers)`: trapezoidal `delta_Wh = (W_now + W_prev) / 2 × dt_hours`, seeded from the poll's `total_wh` on the first stream burst, bucketed by UTC calendar hour into `live_by_hour`. - `_fetch_device_metrics` early-returns `energy = live_total_wh` (no QUANTITY fetch) once the stream has taken over → **prevents double-counting** between the poll's bucket accumulation and the stream's ∫W·dt. - `stream.py`: extracted `_compute_device_powers` pure helper; `_apply_power_updates` signature refactored to `(device_powers, devices_data)`; `_process_batch` integrates live energy on every batch then notifies via `async_update_listeners()`. ## Why trapezoidal + separate live_total_wh The stream pushes power (~15s); energy = ∫W·dt. A separate `live_total_wh` (seeded from the poll, then stream-owned) avoids double-counting the server's hourly QUANTITY buckets against the live accumulation. The 1h offset (#5/#42) dissolves because the energy is computed at the actual moment of consumption — `last_updated` is correct by construction, no backdating needed. ## Scope / merge-safety - `UPDATE_INTERVAL` unchanged (2 min). QUANTITY fetch NOT removed (Slice 5 re-uses it for reconciliation). No reconciliation in this slice — `live_by_hour` is populated but not consumed. - Touches only `coordinator.py`, `stream.py`, `test_coordinator.py`, `test_stream.py`. ## Tests 6 new tests (T7-T12) + 4 updated. T8/T9/T12 assert exact trapezoidal values with patched clocks (not sleeps). T10 verifies the double-count prevention (QUANTITY skipped when stream active). 63/63 passing, ruff + mypy clean. ## Drift note If the stream permanently dies, the energy freezes at the last live value until HA restart. Slice 5's reconciliation (vs server QUANTITY/HOUR for completed UTC hours) bounds this drift — that's the final slice. ## Stacking Base: `feat/stream-power` (PR #58). Rebase once #58 merges.
Real-time energy via trapezoidal ∫W·dt over the live power stream (streaming 4/5)
All checks were successful
Validate / lint-ruff (pull_request) Successful in 7s
Validate / test-pytest (pull_request) Successful in 3m2s
Validate / type-check-mypy (pull_request) Successful in 3m7s
Validate / lint-ruff (push) Successful in 7s
Validate / test-pytest (push) Successful in 3m1s
Validate / type-check-mypy (push) Successful in 3m8s
dec6371201
The stream now accumulates trapezoidal delta_Wh = (W_now + W_prev)/2 × dt
into a separate live_total_wh (seeded from the poll's total_wh on the first
burst), so sensor.<device>_total_energy rises in real time instead of
jumping hourly. _fetch_device_metrics early-returns the live total once the
stream has taken over, preventing the poll's QUANTITY/HOUR buckets and the
stream's ∫W·dt from double-counting (Slice 5 re-introduces QUANTITY as
reconciliation). _compute_device_powers is extracted from _apply_power_updates
and _process_batch feeds every burst to integrate_live_energy.
Author
Owner

Landed in main via the linear stack fast-forward (commit 9d016f7). This slice is part of the streaming feature merged as #56…#62 — closing (its commits are already in main).

Landed in `main` via the linear stack fast-forward (commit `9d016f7`). This slice is part of the streaming feature merged as #56…#62 — closing (its commits are already in `main`).
mat closed this pull request 2026-07-14 17:12:47 +00:00
All checks were successful
Validate / lint-ruff (pull_request) Successful in 7s
Validate / test-pytest (pull_request) Successful in 3m2s
Validate / type-check-mypy (pull_request) Successful in 3m7s
Validate / lint-ruff (push) Successful in 7s
Validate / test-pytest (push) Successful in 3m1s
Validate / type-check-mypy (push) Successful in 3m8s

Pull request closed

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!59
No description provided.