pluginbattery/docs/quick-index.md
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

8.2 KiB
Raw Blame History

Quick index

Where each concept lives in the codebase. Update as code is added.

Code map

Concept Location
InfluxDB → CSV export scripts/export_from_influx.py
Hourly data loader pluginbattery.sim.load_hourly
Battery spec + presets (ECOFLOW_STREAM_AC) pluginbattery.sim.Battery
State engine (walks SoC, clamps to limits) pluginbattery.sim.simulate
24h-foresight oracle (4-var LP per UTC day) pluginbattery.sim.oracle_daily_schedule
NL consumer-price tariff helper pluginbattery.sim.apply_nl_tariff
PV synthesis from horizontal irradiance pluginbattery.sim.synthesize_pv
Driver: compare multiple batteries side-by-side scripts/compare_batteries.py
Driver: run oracle for one battery (default EcoFlow) scripts/run_oracle.py
Driver: honest LP vs cracked store calc, single scenario scripts/compare_with_store.py
Driver: capacity-vs-savings sweep scripts/capacity_sweep.py
Driver: rank every catalog battery by honest payback scripts/battery_leaderboard.py
Catalog scraper (thuisbatterijgids.net REST API) scripts/scrape_batteries.py
Reverse-engineered store calculator (thuisbatterijgids.nl) pluginbattery.store_calc
Web app (Flask): live leaderboard + scenario inputs pluginbattery.web
LP-side smoke tests tests/test_sim.py
Store-formula regression tests (locked to 3 quotes) tests/test_store_calc.py

Web app

PORT=8765 uv run python -m pluginbattery.web    # local dev
gunicorn pluginbattery.web:app                  # production (Procfile is set up for h4a)

The page renders the default scenario server-side at first load (Marstek Venus B / €124/yr / 3.84 yr) so there's no blank flash. Inputs trigger a POST to /api/calculate which returns JSON. Results are cached per scenario hash — the first cold scenario takes ~20 s for 37 unique (cap, power) LP runs; identical re-requests are instant.

Cracked store formula (thuisbatterijgids.nl)

Verified against three observed quotes within €0.5 each. Source: src/pluginbattery/store_calc.py.

hours_to_fill   = capacity_kwh / max_charge_kw
cycles_per_day  = min(1.0, 4 hours / hours_to_fill)
arbitrage       = 365 × cycles_per_day × capacity × 0.85 × (avg_retail  avg_EPEX)
self_consume    = 195 × capacity × 0.85 × avg_retail        (if PV and no saldering)
                = 0                                          (if full saldering)
year1           = arbitrage + self_consume
payback @ 3% i  = log(1 + cost·i/year1) / log(1+i)

Where avg_EPEX ≈ €0.0824/kWh (their apparent assumed wholesale baseline).

The deception is in the (avg_retail avg_EPEX) term: charged kWh are valued at avg wholesale (no VAT, no energy tax), discharged kWh at full retail. No NL supplier will sell you wholesale-priced kWh, so that spread doesn't exist for any real customer. Inflates dynamic-mode savings ~2× for EcoFlow, up to ~3.5× for Marstek 2.5 kW. Fixed-rate PV self-consumption side is honest within ~5%.

Running comparisons

# No PV, no saldering (post-2027 future)
uv run python scripts/compare_batteries.py

# 3 kWp PV, no saldering (likely dad's situation 2027+)
uv run python scripts/compare_batteries.py --pv-kwp 3.0

# 3 kWp PV with saldering still active (current 2023-24 reality)
uv run python scripts/compare_batteries.py --pv-kwp 3.0 --saldering full

PV total is auto-calibrated to 900 kWh/kWp/year (NL norm). Override with --pv-target.

Run

uv sync
uv run python scripts/run_oracle.py        # writes data/processed/oracle_daily.csv
uv run pytest                               # smoke tests

Latest oracle results — payback in years

Window 2023-09-01 → 2024-09-01 (8745 h). Tariff: consumer = EPEX × 1.21 + 0.136 EUR/kWh. PV calibrated to 2700 kWh/yr at 3 kWp. 24h-foresight oracle, daily LP per UTC day.

The number depends strongly on what you assume about saldering and price level. Same hardware, same data, same dispatch — three very different paybacks:

Scenario EcoFlow €699 Marstek 0.8 kW €1339 Marstek 2.5 kW €1339
No PV, saldering on/off (same — no PV means no exports either way) 12.0 16.3 12.6
3 kWp PV, full saldering (current 2024 reality) 13.3 18.9 14.6
3 kWp PV, no saldering, export at raw EPEX (~€0.075/kWh) 6.8 9.7 8.2
3 kWp PV, no saldering, export = €0 5.9 7.7 6.7
3 kWp PV, export = €0, prices × 1.5 (matches stroomstoring.nl style sales calc) 3.96 5.81 4.51

The × 1.5 factor lifts our 2023-24 average consumer price (€0.226) to ≈€0.34, the typical Dutch retail level in 2025. At that price level + zero export compensation, the EcoFlow at €699 pays back in 4.0 years — exactly matching the stroomstoring.nl quote of €176/yr / 3.9 years.

The simulator gives a different answer than the store only because of two assumption changes:

  1. Export rate: the store assumes €0 (no compensation at all), our default uses raw EPEX (~€0.075/kWh).
  2. Price level: the store uses current retail (€0.34/kWh), our backtest uses what actually happened in 2023-24 (€0.23/kWh).

Both positions are defensible — the store's is forward-looking ("buy this today and save under post-saldering 2025+ pricing"), ours is historically grounded ("had we owned this in 2023-24"). Use --price-mult 1.5 --export-rate 0 on compare_batteries.py to reproduce the store quote.

Annual electricity bill (no battery), our house, our 2023-24 prices:

Setup Imports Exports Bill
No PV 7277 kWh 0 €1497.38
3 kWp PV, full saldering 5708 1125 €995.86
3 kWp PV, no saldering, export = raw EPEX 5708 1125 €1158.18
3 kWp PV, no saldering, export = 0, prices × 1.5 5708 1125 €1803.75

Validation against dad's actual bill (different house, similar 3 kWp PV): dad reported 1976 kWh exported. With ~2700 kWh PV that implies 2530% self-consumption — typical NL. Our simulation lands at 60% self-consumption because this house has higher base demand to soak up more PV directly.

Raw data files (data/raw/)

Generated by scripts/export_from_influx.py. All hourly, UTC, ISO-8601 timestamps.

File Column Unit Source field on data-vm
prices_hourly.csv eur_per_kwh EUR/kWh, raw EPEX (no taxes, no markup) epex-price (market='nl') / 1000
p1_hourly.csv power_w watts, hour-mean, household import only (no PV in this house) power-current
solar_hourly.csv irradiance_w_m2 W/m², horizontal (zenith-facing) ws1-solarradiation

Tariff additions (BTW, energy tax, supplier markup, network charges, terugleverkosten, saldering) are layered on downstream (UI / cost calc), not in the export.

Working window

2023-09-01T00:00:00Z → 2024-09-01T00:00:00Z (8784 hours, 366 days incl. 2024-02-29).

Picked over 2024-09→2025-09 because the latter contains a 19-day P1 outage (2025-03-05→23).

Known gaps in the working window

File Coverage Largest gap
prices_hourly.csv 8782/8784 (100.0%) 1h at each DST transition — ENTSO-E artefact
p1_hourly.csv 8764/8784 (99.8%) 12h on 2024-05-09 18:00→2024-05-10 05:00
solar_hourly.csv 8765/8784 (99.8%) 8h overnight 2024-01-04→05

Drop hours where any signal is missing rather than interpolating, unless an interpolation model is explicitly chosen.

Sign / sense conventions

  • power_w: this house has no PV today, so the field is pure household consumption. Always positive. For "with solar" scenarios, synthesize PV output from irradiance_w_m2 × assumed kWp/tilt/azimuth and subtract from power_w to get net.
  • eur_per_kwh: pre-tax EPEX. Genuinely negative in some hours (mean 0.075, min 0.200, max 0.464 over the window). Adding 21% BTW on a negative wholesale price is a tariff modelling decision, not a data one.

Value-range sanity checks (current window)

  • Prices (EPEX raw): mean 0.075 EUR/kWh, min 0.200, max 0.464.
  • Solar: mean 92.8 W/m², max 762 W/m². Plausible for NL horizontal.
  • P1: mean 833 W, median ~335 W, max 6135 W. Reasonable household consumption.