pluginbattery/CLAUDE.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

6.7 KiB
Raw Permalink Blame History

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project intent

Backtest the ROI of a home battery against real historical hourly data. The core question:

"If I had owned battery X over period Y, what would it have actually saved me?"

First-class scenarios:

  • No solar, just price arbitrage — charge when day-ahead is cheap, discharge when expensive.
  • With solar — store surplus generation instead of exporting at low / negative prices.

Two battery archetypes to compare side-by-side, because they have very different economics:

  • Plug-in (AC-coupled, schuko-style) — small capacity (~2 kWh typical), low continuous output (~800 W), no grid export, simple time-shifting of own consumption only. Cheap, no installer.
  • Installed (hybrid inverter / DC-coupled) — 515 kWh, 35 kW continuous, can charge from solar DC, can export to grid. Expensive, needs an electrician.

Output should include: euro savings vs. counterfactual, simple payback period, and a sensitivity sweep across battery size / power rating / dispatch strategy.

Decisions still open — do not silently assume

These materially change the answer. Get the user to nail them down before writing simulation code, or state the assumption loudly in every result.

  • Battery model fidelity. Round-trip efficiency, depth-of-discharge limit, calendar + cycle degradation, idle/standby draw, inverter efficiency. Start with constants. Only add curves once results turn out sensitive to them.
  • Solar PV system parameters. Panel kWp, tilt, azimuth, inverter clipping. The horizontal irradiance from solar_hourly.csv is the input; PV output is computed downstream from these parameters. Default to a flat-roof horizontal panel until the user specifies otherwise.

Decisions made

  • Country / market: Netherlands.
  • Backtest window: 2023-09-01T00:00:00Z → 2024-09-01T00:00:00Z (8784 hours, 366 days; 99.5%+ coverage on all three signals). The next year forward had a 19-day P1 outage in March 2025 and was rejected.
  • Household load profile: real P1 meter log from user's house in InfluxDB. The relevant field (power-current) is import-only — this house has no PV today. For "with solar" scenarios we synthesize PV output from a horizontal irradiance signal × assumed system parameters.
  • Day-ahead prices: raw EPEX, hourly mean, in EUR/kWh, from user's InfluxDB. No taxes, BTW, supplier markup, or saldering math is baked into the data — those are tariff layers added downstream (UI / cost calc). Negative hours are real and preserved.
  • Tariff modelling: out of scope for this codebase. The simulator outputs energy flows (kWh imported, exported, charged/discharged) per hour. The UI / spreadsheet layered on top translates those into euros under whichever tariff structure is being explored.
  • Solar gap forward: irradiance sensor offline ~2025-09-09 → ~2026-04-29. Don't pick a window ending after Sep 2025 unless this is fixed.
  • Dispatch strategy:
    1. Build a 24h-perfect-foresight oracle as the ceiling — what's the max possible savings?
    2. Build rule-based dispatchers (e.g. price percentile thresholds, SoC bands) and measure how close they get to the oracle.
    3. Stretch goal: an LSTM (or simpler) policy trained on historical sequences to beat the rules. Don't pull in PyTorch until the rules have a measured, reproducible gap to close.

Data sources

The user's home InfluxDB is the origin — what actually happened in this house. But the simulator does not talk to InfluxDB. The user exports CSVs once and drops them in data/raw/; everything downstream reads CSV. That keeps runs reproducible, reviewable in git diffs (for small fixtures), and free of network/auth concerns.

What's known to exist in InfluxDB (verify before assuming):

  • Hourly day-ahead electricity prices (>2 years).
  • 15-minute electricity prices (~last 4 months).
  • P1 smart-meter log (>2 years), 1-min / 10-sec resolution.
  • Solar irradiance / PV output, with a gap from ~September 2025 to ~2026-04-29.

Expected raw CSVs (one per signal, UTC timestamps, hourly resampled at the source if possible):

  • data/raw/prices_hourly.csvtimestamp, eur_per_kwh (or eur_per_mwh, document which)
  • data/raw/p1_hourly.csvtimestamp, import_kwh, export_kwh (or net consumption_kwh)
  • data/raw/solar_hourly.csvtimestamp, pv_kwh (and/or irradiance_w_m2)

Document the exact columns and units in docs/quick-index.md once the export is in place. Anything resampled or derived from these files goes to data/processed/ and must be regenerable.

If the InfluxDB export turns out to be tedious to do by hand, write a single one-off script under scripts/export_from_influx.py that reads connection details from .env. It runs once per refresh, not every simulation run.

Stack (assumed; change if user objects)

  • Python 3.12+, managed with uv.
  • pandas for time series; drop to numpy only if the inner loop becomes the bottleneck.
  • pytest for tests.
  • influxdb-client only if/when we write the optional one-off export script — not a runtime dep of the simulator.
  • No web framework, no ORM, no plotting library yet. Add only when there is a concrete use case.
  • No PyTorch / TensorFlow yet. Only after rule-based dispatchers have a measured gap to the oracle that's worth closing.

Common commands

To be filled in once the project has code. For now:

uv sync                 # install deps once pyproject.toml exists
uv run pytest           # run tests
uv run pytest tests/test_dispatch.py::test_name   # run a single test

Repo layout (target)

  • src/ — simulation code
  • tests/ — pytest. Unit-test the dispatch logic and tariff calc; one integration test that runs a full year on small fixture data
  • data/raw/ — immutable downloads (gitignored except small fixtures)
  • data/processed/ — derived, regenerable
  • docs/quick-index.md — pointer to where each concept lives in the code
  • scripts/ — one-off data fetchers (ENTSO-E, KNMI, PVGIS)

Things to resist (anti-overengineering for this project)

  • Don't build a BatteryStrategy interface with one implementation. Write the function. Add the interface when the second strategy lands with a name.
  • Don't build a tariff DSL or YAML config. Hard-code the NL tariff structure in a function until a second country actually shows up.
  • Don't build a CLI framework. python -m batteryroi.run --year 2024 via argparse is enough.
  • Don't add caching / parquet / duckdb until a run takes >30s and the user is actively iterating on it.
  • Don't add a Battery dataclass with builders/factories. Plain kwargs into a function.
  • Don't pre-create empty modules "for structure". Files arrive when they have content.