Commit graph

23 commits

Author SHA1 Message Date
Michiel Berger
e39f78fcc5 Fix two timeline bugs
1. /api/timeline silently defaulted missing export_rate to None → 0,
   which broke the LP for dad's terugleveringskosten scenario. With
   export=0, charging during surplus has no benefit, so the battery
   stayed half-empty while solar exported. Fix: when the param is
   absent, fall back to DEFAULTS['export_rate'] (matches the form's
   initial value). Explicit blank still means 'use saldering toggle'.

2. uPlot charts had legend disabled. Re-enabled with per-series value
   formatters so hovering shows '14:00 NL · −1.70 kWh/h' style values.
2026-05-01 09:54:08 +02:00
Michiel Berger
af55fb33dd Default demand_kwh = 1518 (annualised) so dad's hourly P1 passes through unscaled 2026-05-01 09:37:21 +02:00
Michiel Berger
093605be2e Modal markup must come before script tag — fix silent click handler 2026-05-01 09:27:25 +02:00
Michiel Berger
be6012c1ea Make whole battery row open the timeline modal; drop the shop link from the row 2026-05-01 09:23:54 +02:00
Michiel Berger
b39501ca21 Battery timeline modal + switch to real 2025 data
Modal:
- Click any battery row to open a modal with three uPlot charts:
  irradiance (W/m²), battery SoC (kWh), net meter flow (kWh/h).
- Date picker + prev/next buttons + 1/3/7-day window selector.
- 'Battery on' toggle flips the SoC and grid charts to the
  no-battery baseline; summary numbers update accordingly.
- New /api/timeline endpoint returns the full per-hour series for
  any (battery, scenario, window).

Data:
- Re-exported prices + solar from data-vm for 2025-01-01 → 2025-09-01
  (8 months — solar coverage limit). All three signals now share
  real 2025 timestamps; no more month/day alignment fudge.
- Re-processed dad's CSV against the new window so power_w is real
  net consumption per actual hour.
- Compute_leaderboard now applies an 8766/window-hours annualisation
  factor to year-1 savings, capt kWh, throughput, and total surplus
  so payback / 10-yr net stay correct on the shorter window.
- Defaults: demand 1010 (= dad's net over 8 months, scale 1.0),
  start 2025-06-15.

Tests pass. Local run: PORT=8765 uv run python -m pluginbattery.web,
click any row.
2026-05-01 09:21:16 +02:00
Michiel Berger
10cc32ddc0 timeline: label columns 'no-bat meter' / 'with-bat meter' to avoid confusion 2026-04-30 22:47:59 +02:00
Michiel Berger
2f05a40d40 Add timeline inspector — print hour-by-hour LP schedule for any week 2026-04-30 22:37:05 +02:00
Michiel Berger
1804606ff6 Add 'cycles/yr' column — total throughput including grid arbitrage 2026-04-30 22:02:33 +02:00
Michiel Berger
9d43e1802b Clarify what 'surplus' means in the leaderboard
The number was previously labelled 'total solar surplus' which read like
'total PV produced'. It's actually 'capturable surplus' = the kWh that
would be exported to the grid without a battery, which is less than gross
PV by the directly-self-consumed portion (PV used at the same hour it's
produced never crosses the meter).

For dad's 3 kWp setup: PV produced ≈ 2625 kWh, directly consumed ≈ 750
kWh, exported ≈ 1869 kWh. The battery can only redirect the 1869,
because the 750 doesn't cross the meter.

Renamed heading + extended legend with the explanation.
2026-04-30 21:57:11 +02:00
Michiel Berger
faecd9cb80 Default scenario = dad's actual: pv_kwp=0, demand=2350, export_rate=-0.106 2026-04-30 21:52:56 +02:00
Michiel Berger
a65ad1ba34 Add column-glossary legend below the leaderboard table 2026-04-30 21:44:09 +02:00
Michiel Berger
a3b9b2a0e9 Add solar-capture columns; drop Store columns from leaderboard
New columns:
- kWh capt:  absolute kWh of solar surplus the battery absorbed
- % capt:    that, as a % of total available surplus for the scenario
- % full:    of all surplus hours, the % when the battery was already full
- % sat:     of all surplus hours, the % when charging was at max kW
The leaderboard heading also shows total scenario surplus in kWh/yr so
the percentages have a denominator.

These make the limits visible: e.g. on dad's data, an 0.8 kW plug-in is
saturated 19% of surplus hours, while a 2.5 kW unit is never saturated
(0%) — but both end up bottlenecked by 'battery full' on multi-day sunny
stretches when demand can't drain it overnight.

Removed: Store €/yr, Store yr, ×over columns. The store_calc module is
still in the codebase and the API still returns the values; the columns
are just not displayed since dad isn't shopping based on the dishonest
quote.
2026-04-30 21:39:38 +02:00
Michiel Berger
91ea434912 Use dad's real P1 data; add export-rate input for terugleveringskosten
- scripts/import_dad_p1.py converts his 15-min cumulative meter dump
  (~/Downloads/P1e-2025-1-1-2026-1-1.csv) into our hourly format.
  Net of PV exports — power_w can be negative on surplus hours.
  Aligned to our 2023-09 → 2024-09 window by (month, day, hour) so
  prices and irradiance stay seasonally matched.
- data/raw/p1_hourly.csv is now dad's data; ours preserved as
  p1_hourly_michiel.csv for diff/comparison.
- LP gE upper bound was 'pv[t]'; now max(0, pv − demand). This was
  wrong before but invisible because demand was always positive; with
  dad's net data demand goes negative and the LP went infeasible.
- New 'Export rate' field in Advanced. Override saldering toggle with
  a literal €/kWh — set to −0.106 for terugleveringskosten.
2026-04-30 21:07:49 +02:00
Michiel Berger
df66a44d53 Drop Powerwall from extras — hybrid LP slowed cold scenarios 2026-04-30 17:45:34 +02:00
Michiel Berger
b0933c444b Dockerfile: gunicorn 2 workers x 4 threads (gthread)
Single sync worker means a slow request to / (compute_leaderboard, ~17s
on dev / longer on h4a's compact CPU slice) blocks every other request,
including the /healthz probe. With 2 worker processes and 4 threads
each, slow requests on / no longer wedge the gunicorn queue, and
/healthz responds promptly even while a /api/calculate is in flight.
2026-04-30 17:39:13 +02:00
Michiel Berger
c9dd23f078 PV system input: accept arbitrary decimals (step=any), max 100 kWp 2026-04-30 16:28:01 +02:00
Michiel Berger
ae994a7ded Move custom batteries from web.py into data/raw/extra_batteries.json 2026-04-30 15:52:16 +02:00
Michiel Berger
d6a3d53a04 Add 10-yr net profit column + clickable column sorting
- Backend: every row now carries ten_year_profit_eur =
  year1 × cumulative inflation factor − battery cost. Negative for
  batteries that don't pay back within 10 years (the Powerwall).
- New column shows green for positive net, red for negative.
- Every column header is now clickable: click to sort, click again to
  toggle direction. Sort indicator (↑/↓) on the active header.
- Sort + filter selections persist in localStorage independently.
- Default sort stays 'LP yr ascending' (best payback first), but a
  single click on '10-yr net' flips to absolute lifetime profit and
  larger batteries surface to the top.
2026-04-30 15:41:11 +02:00
Michiel Berger
386a758517 Add Powerwall to leaderboard + 3-way category filter
- Synthesised one hybrid entry (Tesla Powerwall 3, installed price €9500)
  appended to the catalog. Tagged installation: hybrid; LP runs with
  allows_export=True for those.
- Replaced single 'plug-in only' checkbox with three category filters:
  ≤0.8 kW plug-in, ≤2.5 kW plug-in, Hybrid/DC-coupled. Default: all on.
  Filter selection persists in localStorage.
- API now tags every row with a category so the JS can filter without
  re-fetching. SSR rows carry data-category for first-paint filtering.
- Store quote left blank for hybrids since thuisbatterijgids.net only
  models plug-in arithmetic.
2026-04-30 15:30:55 +02:00
Michiel Berger
1878b0a383 Speed up /api/calculate from 75s to 17s on h4a's small VM
- ProcessPoolExecutor with fork context: 4 workers each inherit the
  scenario df via copy-on-write. linprog/HiGHS doesn't release the GIL
  so threads were useless; processes get clean ~3.5× speedup.
- Cross-scenario LP cache keyed on everything the LP depends on
  (demand, retail, PV size, saldering, η_rt) but NOT inflation or cost.
  Result: tweaking just inflation re-uses every LP — sub-100ms.

Cold cold scenario went 75s → 17s; inflation tweak went 17s → 10ms.
2026-04-30 15:15:20 +02:00
Michiel Berger
fa7a205eb7 deploy.sh: auto-refresh SSH host key when h4a recreates the VM 2026-04-30 15:02:47 +02:00
Michiel Berger
1d7574e0b2 Add plug-in-only filter to web app + deploy script
- Checkbox 'plug-in only (≤ 0.8 kW, no electrician)' filters the leaderboard
  client-side and re-picks the top recommendation from visible rows.
  Filter state is persisted in localStorage.
- scripts/deploy.sh: idempotent rsync + systemd setup against the
  'thuisbatterij' SSH alias. Run after provisioning the h4a workload.
2026-04-30 14:58:52 +02:00
Michiel Berger
60e0706736 Initial import: home-battery ROI simulator + cracked thuisbatterijgids calc
- Hourly data exporter (InfluxDB → CSV) for prices, P1, irradiance.
- LP-based 24h-foresight oracle dispatch with SoC-consistent state engine.
- Reverse-engineered thuisbatterijgids.nl formula (matches their quotes
  to within €0.50 across three battery configs).
- Catalog scraper for the 52 batteries on thuisbatterijgids.net via their
  /wp-json REST endpoint.
- Web app (Flask) that ranks every catalog battery by honest payback and
  contrasts with the store's quote, deployable via the included Procfile.
2026-04-30 13:46:27 +02:00