Commit graph

15 commits

Author SHA1 Message Date
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