Commit Graph
288 Commits
Author SHA1 Message Date
tliu93 c851bad829 M8-R07: integrate WarmteLink device access into Compose 2026-08-24 06:45:31 +02:00
tliu93 09abe05f66 M8-R06: refresh Tibber prices after startup and configuration 2026-08-24 06:45:31 +02:00
tliu93 0924e8df52 M8-R05: recover WarmteLink ingestion and discovery automatically 2026-08-24 06:45:31 +02:00
tliu93 72ac7e7300 M8-R04: interpret WarmteLink timestamps as Amsterdam wall time 2026-08-24 06:45:31 +02:00
tliu93 18083822ea M8-R04A: fix fake discovery receive clock 2026-08-24 06:45:31 +02:00
tliu93 3beeb5a461 M8-R03B: keep meter swap recompute in caller transaction 2026-08-24 06:45:31 +02:00
tliu93 631b14e2ec M8-R03: hand off channel binding during meter swap 2026-08-24 06:45:31 +02:00
tliu93 231c340ea6 M8-R03A: stabilize thermal recompute time-range tests 2026-08-24 06:45:30 +02:00
tliu93 c24b6684cc M8-R02A: reconcile DSMR runtime after source CRUD 2026-08-24 06:45:30 +02:00
tliu93 d9e82038dc M8-R02: report DSMR MQTT source health 2026-08-24 06:45:30 +02:00
tliu93 e59c192097 M8-R01: add configurable MQTT client identity 2026-08-24 00:44:28 +02:00
tliu93 2a47dab272 M8-R01A: align lifespan DSMR reconcile mock contract 2026-08-24 00:35:24 +02:00
tliu93 9db7f63274 M8-R00: align tariff expose tests with source bindings 2026-08-24 00:15:48 +02:00
tliu93 ebf96de4f1 M8-T20: document and validate WarmteLink deployment 2026-08-23 21:28:26 +02:00
tliu93 5b9d60e80a M8-T19: add scope-aware energy contract and cost UI 2026-08-23 21:22:06 +02:00
tliu93 963e43e3e4 M8-T18: add source and multi-commodity meter UI 2026-08-23 21:22:06 +02:00
tliu93 39c11ae606 M8-T17: add Home Assistant thermal entities 2026-08-23 21:22:06 +02:00
tliu93 3eec701448 M8-T16: add thermal meter cost APIs 2026-08-23 21:22:06 +02:00
tliu93 489e5b596a M8-T15: add thermal meter cost engine 2026-08-23 21:22:06 +02:00
tliu93 567ddb9779 M8-T14: add generic meter cost periods 2026-08-23 21:22:06 +02:00
tliu93 0fb51d338c M8-T13: add district heating pricing profile 2026-08-23 21:22:06 +02:00
tliu93 b812d5ac46 M8-T12: scope energy contracts 2026-08-23 21:22:06 +02:00
tliu93 a9458394f2 M8-T11: add WarmteLink discovery and history API 2026-08-23 21:22:06 +02:00
tliu93 4884a19e3d M8-T10: add WarmteLink serial worker manager 2026-08-23 21:22:06 +02:00
tliu93 afe653bafa M8-T09: add WarmteLink ingest state machine 2026-08-23 21:22:06 +02:00
tliu93 25a08c47a4 M8-T08: add WarmteLink reading schema 2026-08-23 21:22:05 +02:00
tliu93 ffc693e995 M8-T07: extract reusable P1 parser 2026-08-23 21:22:05 +02:00
tliu93 5855fff451 M8-T06: add meter source management API 2026-08-23 21:22:05 +02:00
tliu93 1ea2f659e0 M8-T05: bind electricity costs to source bindings 2026-08-23 21:22:05 +02:00
tliu93 2e125dbd53 M8-T04: reconcile DSMR ingest from meter sources 2026-08-23 21:22:05 +02:00
tliu93 28486a83c7 M8-T03: adopt DSMR history into meter sources 2026-08-23 21:22:05 +02:00
tliu93 a78401c2ef M8-T02: add source profiles and binding services 2026-08-23 21:22:05 +02:00
tliu93 009856a50d M8-T01: add meter source identity schema 2026-08-23 21:22:05 +02:00
tliu93 43c2ddce1a M8: clarify Alembic schema migration plan 2026-08-22 22:02:14 +02:00
tliu93 d7f04aee8c M8: finalize WarmteLink implementation plan
pytest / test (push) Successful in 2m22s
frontend / frontend (push) Successful in 29s
2026-08-22 21:23:19 +02:00
tliu93 8dbb59a3b7 PRE-M8-T03: finalize WarmteLink hardware validation
frontend / frontend (push) Successful in 30s
pytest / test (push) Successful in 2m23s
2026-08-22 19:29:54 +02:00
tliu93 2992bbb0ef PRE-M8-T02: add read-only WarmteLink serial probe 2026-08-22 18:06:15 +02:00
tliu93 22faeb45bb PRE-M8-T01: add WarmteLink P1 telegram parser 2026-08-22 17:47:30 +02:00
tliu93 c37dfacfc7 PRE-M8: record WarmteLink findings and implementation plan 2026-08-22 17:23:41 +02:00
tliu93 16b050d821 PRE-M8: add staged WarmteLink bring-up reference
frontend / frontend (push) Successful in 29s
pytest / test (push) Successful in 2m23s
2026-08-20 12:11:05 +02:00
tliu93 d3d914b117 PRE-M8: add WarmteLink P1 planning placeholders
frontend / frontend (push) Successful in 27s
pytest / test (push) Successful in 2m20s
2026-08-18 17:20:31 +02:00
tliu93 fc4af857e3 AGENTS: make repository guidance harness-neutral
frontend / frontend (push) Successful in 46s
pytest / test (push) Successful in 2m33s
2026-08-18 13:28:23 +02:00
tliu93 0958d9a2e9 fix(energy): report real kWh in the cost Summary instead of mislabelled money
docker-image / build-and-push (push) Successful in 1m37s
frontend / frontend (push) Successful in 10m24s
pytest / test (push) Successful in 11m57s
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.
v1.5.1
2026-08-06 22:20:56 +02:00
tliu93 b405aea88b docs: require frontend codegen alongside the OpenAPI export in the gates
frontend / frontend (push) Successful in 9m59s
pytest / test (push) Successful in 11m51s
docker-image / build-and-push (push) Successful in 12m49s
The local gate list only covered `scripts/export_openapi.py` + a clean
`openapi/` diff, but `frontend/src/api/schema.d.ts` is generated from that JSON
and CI re-runs `npm run codegen` with `git diff --exit-code`. d07a083 changed a
route docstring, refreshed openapi.json, and skipped codegen — local green,
remote red on a one-line comment diff. Spell out both steps and note that route
docstrings feed the OpenAPI description too.

Also add AGENTS.md as a symlink to CLAUDE.md so other agent tooling picks up
the same contract.
v1.5.0
2026-07-27 19:03:03 +02:00
tliu93 bfc7aa3031 chore(frontend): regenerate API schema after sell_fee docstring change
frontend / frontend (push) Successful in 9m59s
pytest / test (push) Successful in 12m27s
d07a083 changed the /api/energy/prices docstring (sell now deducts sell_fee)
and refreshed openapi/openapi.json, but frontend/src/api/schema.d.ts was not
regenerated, so CI's "check codegen is in sync" step failed. Comment-only diff.
2026-07-27 18:29:54 +02:00
tliu93 2f63b9630c fix(prices): keep hover marker on the hovered slot past midnight
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.
2026-07-27 18:29:08 +02:00
tliu93 d07a083e03 fix(tibber): deduct verkoopvergoeding (sell_fee) from feed-in sell price
frontend / frontend (push) Failing after 5m49s
pytest / test (push) Successful in 20m30s
docker-image / build-and-push (push) Successful in 13m26s
Tibber's API `total` already includes the buy-side inkoopvergoeding
(verified from production data: total = spot×1.21 + energy_tax 0.11085 +
inkoopvergoeding 0.0248). Under net metering Tibber pays back
`total − verkoopvergoeding` per returned kWh (NL: EUR 0.28 -> 0.2552), so the
two EUR 0.0248 fees do NOT cancel — the feed-in price sits 0.0248 below buy.

Model the verkoopvergoeding as a first-class, always-subtracted contract
field `energy.sell_fee` (default 0.0248) instead of folding it into
`sell_adjust`. New sell formula:

    sell = total − energy_tax − sell_fee − sell_adjust

`sell_adjust` now carries only the net-metering energy-tax refund
(= −energy_tax). Applied in both the billing strategy and the /prices
endpoint; recorded in the pricing snapshot. Frontend renders the field
automatically (dynamic profile form). Docs (references, m6) corrected to
drop the wrong "fees cancel" premise.
v1.4.3
2026-07-20 13:50:13 +02:00
tliu93 b65f700d56 fix(tibber): fetch forward-looking today+tomorrow via priceInfo(QUARTER_HOURLY)
pytest / test (push) Successful in 20m2s
frontend / frontend (push) Failing after 6m5s
docker-image / build-and-push (push) Successful in 13m27s
priceInfoRange is a historical cursor connection whose range ends at "now": it
never returns upcoming slots. With it, the DB only ever held prices up to the
last hourly refresh, which caused two problems:

  1. The price chart could only show history up to now, never a forward curve.
  2. Worse, per-slot billing was subtly wrong. _tibber_strategy looks up the
     price via `starts_at <= t0` (nearest slot at or before the period). Because
     a period's exact 15-min slot was not fetched until the next hourly refresh
     (~1h later), intra-hour periods were billed with the PREVIOUS quarter's
     price and then locked in by the immutability guard — never corrected.

Switch to priceInfo(resolution: QUARTER_HOURLY) { today tomorrow }, which is
forward-looking AND quarter-hourly: today is always the full local day (96
slots) and tomorrow fills in once Tibber publishes day-ahead prices (picked up
by the next hourly refresh). Every 15-min slot's exact price is now in the DB
before the slot closes, so each period finds its own slot (accurate billing)
and the live current-price entity stays fresh.

Verified against the live Tibber API: fetch_price_range returns 192 points
(96 today + 96 tomorrow), spanning local today 00:00 → tomorrow 23:45, with
future slots present (previously 0).
v1.4.2
2026-07-18 20:20:33 +02:00
tliu93 f4cea3874b test(energy-cost): pin local_now in future-window summarize test
frontend / frontend (push) Failing after 6m38s
pytest / test (push) Successful in 20m16s
docker-image / build-and-push (push) Successful in 13m32s
test_future_window_counts_0_days relied on the real wall-clock date and assumed
7/1→8/1 2026 was entirely in the future; once that range started elapsing the
window counted fixed-fee days and the assertion failed. Pin local_now to
June 25 2026 (as the sibling window tests already do) so the case stays
deterministic.
v1.4.1
2026-07-17 18:49:26 +02:00
tliu93 134f0abb5f fix(tibber): fetch newest price slots via priceInfoRange last:192
priceInfoRange is a Relay-style cursor connection over the subscription's
entire price history. With no cursor, first:96 returned the OLDEST 96 slots,
anchored at the subscription start date — a fixed window that never advanced.
For a contract added mid-period this meant refresh_prices kept re-upserting the
same day-one slots forever, so GET /api/energy/prices found nothing in the
today+tomorrow window and the UI showed "No Tibber price points available".

Use last:192 to return the newest 192 quarter-hourly slots (2 days), which ends
at the latest published slot and advances daily, fully covering the prices
endpoint's today+tomorrow window.
2026-07-17 18:49:26 +02:00