Single algorithm with two passes:
Pass 1 — greedy self-consumption (real-firmware default):
For each hour, charge any surplus, discharge into any demand.
This nails sunny days: battery fills from morning surplus, exports
only after capacity is reached, drains during evening peak.
Pass 2 — daily price-spread arbitrage on the residual capacity:
For each UTC day, repeatedly find the most profitable
cheap-charge → expensive-discharge pair (positive after round-trip
efficiency), execute it, recompute SoC trajectory, repeat until no
profitable cycle remains. Discharge must fit within hourly demand
for plug-in batteries (no grid push). Sees only that day's prices,
matching what a Tibber/Frank-style smart-charging controller does
with day-ahead price visibility.
Effect:
- Sunny weeks (June 17-19): unchanged — greedy fill + evening discharge.
- No-sun weeks (Feb 7+): now generates arbitrage savings instead of €0.
- Mixed weeks: greedy handles surplus, arbitrage handles the rest.
Updated: test_grid_arbitrage_kicks_in_on_no_sun_days replaces the prior
"greedy doesn't arbitrage" test; now verifies the dispatcher charges
cheap hours and discharges expensive ones when no PV is available.
The LP was producing visually unintuitive schedules: when several surplus
hours had identical cost benefit (true with constant export_rate), the
solver picked an arbitrary subset, which the user couldn't read as 'this
is the battery doing its job'. Worse, the daily-LP variant drained to 0
every midnight because it placed zero value on next-day SoC.
Replaced oracle_daily_schedule() with a one-pass greedy dispatcher that
matches what every plug-in battery in the catalog (Marstek, Zendure,
EcoFlow, HomeWizard) actually does in 'self-consumption mode':
for each hour:
if exporting at meter: charge as fast as power+capacity allows
if importing at meter: discharge to cover net demand
Trade-offs:
- Greedy doesn't do grid arbitrage (charge cheap → discharge expensive
without a surplus source). The LP would; real plug-in firmware
doesn't, so greedy is more honest about what dad's battery would do.
- Charts now show 'fill in the morning, overflow at midday' which is
what users expect to see.
- Updated tests: dropped LP-arbitrage assertion, added a greedy-fills-
from-surplus-then-overflows test that locks the new behaviour in.
- 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.