The Summary cards labelled `metered_import` / `metered_export` as "(kWh)", but
both fields are monetary totals (Σ import_cost / Σ export_revenue). Today's
page therefore showed "Import 1.339 kWh" when the meter had actually imported
4.188 kWh — the 1.339 was EUR. Cross-checked against the DSMR cumulative
registers and Home Assistant: our energy figures were correct all along, only
the label was wrong.
summarize() now also aggregates the metered energy, reusing the already-fetched
non-degraded rows so no extra query is issued:
metered_import_kwh = Σ (d1_kwh + d2_kwh)
metered_export_kwh = Σ (r1_kwh + r2_kwh)
The Import/Export cards show kWh as the headline figure and keep the monetary
equivalent as a sub-line, so the split between energy cost and standing
charges/credits behind total_payable stays visible.
The `_kwh` suffix is now the only thing separating energy from money in this
payload, so the docstrings on both summarize() and SummaryResponse call that
out explicitly.
app/integrations/expose.py reads only the money keys, so the HA outbound
sensors are unaffected by the additive fields.
The price chart keyed its X axis on formatLocalTime() "HH:mm" labels, which
repeat across a today+tomorrow range. Recharts resolves axis tooltips by value
(combineTooltipPayload -> findEntryInArray), so hovering a slot after midnight
matched today's identically labelled point: the tooltip showed today's prices
and the active dot jumped back to today's position instead of following the
cursor.
Key the axis on the ISO instant instead (buildChartRows, sorted by slot start)
and format down to HH:mm in the tick formatter; the tooltip label now carries
the date so today and tomorrow are distinguishable.
Also mark the price slot currently in effect by default: findActiveSlotIndex()
locates the slot containing now, rendered as a ReferenceDot on the buy and sell
lines plus a caption, re-evaluated every 30s.
Regression test drives a real mousemove over a sized chart in jsdom and asserts
the resolved slot and active-dot position.
- MQTT import_cost_total / export_revenue_total now anchor fixed-fee / tax-credit
accrual at max(contract start, first recorded period), so standing charges for
untracked pre-recording days are no longer folded into the running total.
- Add energy.import_cost_today / energy.export_revenue_today (monetary,
total_increasing) that reset at server-local midnight, for per-day cost
aggregation in Home Assistant alongside the running totals.
- ContractForm add-version mode prefills the contract's open version values, so
only the fields that actually change need editing.
- Store UTC, compute day boundaries in server local timezone (new app/services/timezone.py).
- summarize: fixed fee / tax credit accrue only for elapsed whole local days,
integrated across contract versions — no future-day inflation, no version-switch collapse.
- MQTT import_cost_total / export_revenue_total getters delegate to summarize (no inline
apportionment), anchored at the active contract's earliest version effective_from so the
total sensors stay monotonic and never jump backward.
- effective_from: naive datetimes interpreted as server-local wall-clock, then stored UTC.
- frontend ContractForm sends local-midnight naive datetime (was UTC midnight).
- get_costs_summary default window uses server-local "today".
A disabled device with historical readings still cannot be deleted by default
(409 guard stays, to prevent accidental data loss). A new explicit opt-in lets
the user remove it for real:
- DELETE /api/modbus/devices/{uuid}?cascade=true removes the device's readings
and exposed_entity_toggle rows, then the device, in one transaction (FK order:
readings before device). Best-effort clears the HA discovery configs first
(MQTT not connected -> no-op, never blocks the delete). Default (no cascade)
unchanged. Returns 200 with deletion counts.
- Frontend: a 'Force Delete (all data)' button appears only after the 409, with
a red irreversible warning; cascade is sent only on that explicit action.
- Also corrected a stale comment: FK RESTRICT IS enforced at runtime now.
Backend serializes SQLite-read (naive) UTC datetimes without a Z, so the browser
was parsing them as local time -> the UI effectively showed UTC values.
- New src/utils/datetime.ts: treats a tz-less timestamp as UTC, then formats in
the browser's local timezone with hour12:false (no AM/PM).
- Applied to Energy tabs (contracts/prices/costs/DSMR), per-device readings &
trends, and the meter readings table on the Records page.
- CostView Today/This month now use local day boundaries (queries stay UTC ISO).
- Untouched: location/poo (raw Zulu strings), the map, and query-window params.
- Tests are timezone-independent (verified under TZ=America/Los_Angeles).
- New 'DSMR' tab showing the latest parsed telegram (GET /api/energy/dsmr/latest)
as a key/value table — the source-of-truth view so you can see ingest is
landing data without reading the DB.
- Auto-refresh toggle (~10s); loading/error/empty states (empty explains the
likely cause); null phase values shown as a dash.
- useDsmrLatest extended with optional auto-refresh. Tests added.