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.
This commit is contained in:
Michiel Berger 2026-04-30 13:46:27 +02:00
commit 60e0706736
29 changed files with 30358 additions and 0 deletions

66
scripts/run_oracle.py Normal file
View file

@ -0,0 +1,66 @@
#!/usr/bin/env python3
"""Run the daily 24h-foresight oracle over the full window and write per-hour output."""
from __future__ import annotations
import argparse
from pathlib import Path
from pluginbattery.sim import (
ECOFLOW_STREAM_AC,
Battery,
apply_nl_tariff,
load_hourly,
oracle_daily_schedule,
simulate,
)
# EcoFlow Stream AC retail price (incl. VAT) for payback math.
ECOFLOW_STREAM_AC_PRICE_EUR = 699.0
def run(battery: Battery, price_eur: float, data_dir: Path, out_path: Path) -> None:
df = load_hourly(data_dir)
df = apply_nl_tariff(df) # eur_per_kwh now = consumer price (incl. VAT + tax)
print(f"Loaded {len(df)} hours: {df.index[0]}{df.index[-1]}")
print(f"Battery: cap={battery.capacity_kwh} kWh, "
f"P_in={battery.max_charge_kw} kW, P_out={battery.max_discharge_kw} kW, "
f"η_rt={battery.round_trip_eff:.2f}, allows_export={battery.allows_export}")
print(f"Price (incl. VAT) : €{price_eur:.0f}")
print(f"Tariff: consumer = EPEX × 1.21 + 0.136 EUR/kWh")
print("Solving daily 24h LPs...")
schedule = oracle_daily_schedule(df, battery)
print("Walking SoC...")
out = simulate(df, battery, schedule)
out_path.parent.mkdir(parents=True, exist_ok=True)
out.to_csv(out_path)
cost_no = out["cost_no_battery"].sum()
cost_yes = out["cost_with_battery"].sum()
savings = out["savings"].sum()
cycles = out["discharge_kwh"].sum() / battery.capacity_kwh
payback_years = price_eur / savings if savings > 0 else float("inf")
print()
print(f"Year cost without battery : €{cost_no:8.2f}")
print(f"Year cost with battery : €{cost_yes:8.2f}")
print(f"Annual savings (consumer) : €{savings:8.2f}")
print(f"Equivalent full cycles : {cycles:7.1f}")
print(f"Hours simulated : {len(out)}")
print(f"Simple payback : {payback_years:6.2f} years")
print(f"Output -> {out_path}")
def main() -> None:
p = argparse.ArgumentParser(description=__doc__)
p.add_argument("--data", default="data/raw")
p.add_argument("--out", default="data/processed/oracle_daily.csv")
p.add_argument("--price", type=float, default=ECOFLOW_STREAM_AC_PRICE_EUR)
args = p.parse_args()
run(ECOFLOW_STREAM_AC, args.price, Path(args.data), Path(args.out))
if __name__ == "__main__":
main()