Commit Graph
132 Commits
Author SHA1 Message Date
tliu93 5e1545efad M8-R17: allocate thermal standing costs by service 2026-08-28 14:24:57 +02:00
tliu93 6e197d7808 M8-R16: expose hot water to HA Energy
pytest / test (push) Successful in 4m2s
docker-image / build-and-push (push) Successful in 1m39s
frontend / frontend (push) Successful in 48s
2026-08-28 10:02:53 +02:00
tliu93 018f13d73d M8-R15: fix HA discovery identities and thermal totals
frontend / frontend (push) Successful in 47s
pytest / test (push) Successful in 4m1s
docker-image / build-and-push (push) Successful in 1m38s
2026-08-28 01:20:52 +02:00
tliu93 8180082f90 M8-R14: preserve zero DSMR sample interval
docker-image / build-and-push (push) Successful in 1m35s
frontend / frontend (push) Successful in 47s
pytest / test (push) Successful in 4m15s
2026-08-27 21:29:45 +02:00
tliu93 33ca3da593 M8-R11: make business timezone deterministic
frontend / frontend (push) Successful in 46s
pytest / test (push) Successful in 3m56s
2026-08-27 12:16:52 +02:00
tliu93 8dc3f71aaf M8-R08: add atomic meter close and binding transfer 2026-08-24 18:37:46 +02:00
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 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 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 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 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.
2026-08-06 22:20:56 +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.
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).
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.
2026-07-17 18:49:26 +02:00
tliu93 3e04b15656 feat(modbus): add DDSU666 profile and select read function code per profile
frontend / frontend (push) Successful in 2m21s
pytest / test (push) Successful in 9m50s
docker-image / build-and-push (push) Successful in 4m24s
- Add CHINT DDSU666 profile (ddsu666.yaml): FC03 holding registers, voltage/
  current/active power (kW)/reactive power (kvar)/PF/frequency, import+export
  active energy. Word/byte order left at big-endian as a documented best guess
  (manual has no float example) — to be confirmed on-device.
- Add DDSU666-Modbus-Protocol.md reference extracted from the official manual,
  plus the source PDF (parity with the SDM120 reference).
- Generalize driver.read_blocks to dispatch FC03 (holding) or FC04 (input)
  based on a function_code argument (default 4, SDM120 behaviour unchanged);
  the code is validated before any connection is attempted.
- Wire profile.function_code through the CLI read command, the background
  poller, and the device /test endpoint — previously the profile field was
  declared but never honoured (read path was hardcoded to FC04).
- Tests: default -> FC04, function_code=3 -> FC03 holding, invalid FC rejected
  before connecting.
2026-06-30 17:16:25 +02:00
tliu93 90a03e7fd6 FUE-T09: grace-shift *_today daily reset past local midnight + dedicated 00:00:10 publish
docker-image / build-and-push (push) Successful in 4m29s
frontend / frontend (push) Successful in 2m17s
pytest / test (push) Successful in 11m2s
2026-06-26 12:21:51 +02:00
tliu93 d3fc90b320 FUE-T08: defer daily fixed-fee/heffingskorting settlement to local 01:05 in summarize() 2026-06-26 11:44:34 +02:00
tliu93 d4acfc438a FUE-T07: use meter label directly as energy-cost HA device name (drop 'Energy Cost' prefix)
frontend / frontend (push) Successful in 2m14s
pytest / test (push) Successful in 10m49s
2026-06-25 21:28:13 +02:00
tliu93 c26160b10b FUE-T06: trigger HA discovery republish after meter declare/update
frontend / frontend (push) Successful in 2m11s
pytest / test (push) Successful in 10m36s
2026-06-25 20:54:42 +02:00
tliu93 efbe36d7c0 FUE-T05: anchor energy-cost HA device identity to active meter (uuid id, label name, empty when none) 2026-06-25 20:43:07 +02:00
tliu93 f663981cdb FUE-T04: add Meter.uuid (stable HA identity anchor) + backfill migration 2026-06-25 20:20:01 +02:00
tliu93 da05fd2f09 FUE-T03: correct meters POST docstring (no recomputed_periods field) and inaccurate test comments
frontend / frontend (push) Successful in 2m11s
pytest / test (push) Successful in 8m51s
2026-06-25 18:36:46 +02:00