Skip to content

feat: add /optimize/level-schedule endpoint for solar-first strategy - #98

Draft
1va13n7in wants to merge 4 commits into
evcc-io:mainfrom
1va13n7in:feature/solar-first
Draft

feat: add /optimize/level-schedule endpoint for solar-first strategy#98
1va13n7in wants to merge 4 commits into
evcc-io:mainfrom
1va13n7in:feature/solar-first

Conversation

@1va13n7in

Copy link
Copy Markdown
Contributor

Add /optimize/level-schedule endpoint for solar-first strategy.

Finds the optimal flat export ceiling that charges a battery from solar
surplus while minimizing peak grid export. Response uses the same format
as /optimize/charge-schedule for identical downstream processing.

Depends on #97 (charge_profile).

Part of evcc-io/evcc#31963

Lars Valentin added 2 commits July 22, 2026 08:02
Add CC-CV taper modeling to the optimizer. Batteries can now specify
a charge_profile (e.g. lifepo4_conservative) that limits charge power
as a function of SoC, matching real-world BMS behavior.

The MILP solver uses iterative constraint tightening: solve, extract
SoC trajectory, compute per-slot P_max(SoC) from the profile, add
upper bound constraints, re-solve until convergence (max 5 iterations).

New API fields in BatteryConfig:
- charge_profile: named profile (e.g. lifepo4_conservative)
- charge_knee: SoC% where taper begins (override)
- charge_k: exponential decay constant (override)
- charge_c_rate_float: minimum C-rate at float (override)

New module: charging_profiles.py with profile definitions and
max_charge_power() calculation.
Lars Valentin added 2 commits July 22, 2026 14:17
Replace the iterative constraint tightening (3-6 re-solves) with
piecewise-linear upper bounds added at model build time. The CC-CV
taper curve is convex, so tangent lines at 4 breakpoints (knee to
100% SoC) provide valid linear constraints on charge power as a
function of s[i][t-1]. One solve, no stale bounds, bounded wall clock.
Add a new optimization endpoint that finds the optimal flat export
ceiling for charging a battery from solar surplus. Instead of
minimizing cost (MILP), this maximizes self-use by flattening the
grid export curve.

The algorithm finds the export level where all active charging slots
reduce the forecasted feed-in power by the same amount. Charge power
per slot is variable and respects the CC-CV taper profile.

The response uses the same format as /optimize/charge-schedule
(batteries[].charging_power/discharging_power/state_of_charge),
so consumers can process both endpoints with identical code.

New endpoint: POST /optimize/level-schedule
New function: find_optimal_level() in charging_profiles.py
New function: _level_to_milp_format() in app.py
@1va13n7in
1va13n7in force-pushed the feature/solar-first branch from ddf87de to ac5f77e Compare July 22, 2026 15:19
@1va13n7in
1va13n7in marked this pull request as ready for review July 22, 2026 16:32
@andig
andig marked this pull request as draft July 23, 2026 10:18
@1va13n7in

Copy link
Copy Markdown
Contributor Author

Production results from a 34 kWp / 42.58 kWh LiFePO4 system.

Solar first active -- flat export ceiling during charging:
2026-07-29_08-52-25-optimize-SOC-33

The grey line (grid power) stays flat while the battery charges.
CC-CV taper visible as charge power decreases toward end of window.

Actual power flows (Home Assistant):
2026-07-29-charging

Real-world result from the same day. Battery charged from solar
surplus, export stayed nearly flat during charging.

The evcc-side PR (strategy routing, UI) is ready and waiting
on this to be merged and tagged.

@andig

andig commented Jul 30, 2026

Copy link
Copy Markdown
Member

Its still unclear to me (can't find in which related issue I've asked but didn't get an answer):

@1va13n7in

1va13n7in commented Aug 1, 2026

Copy link
Copy Markdown
Contributor Author

The dependency on #97 is because find_optimal_level() calls
max_charge_power() from charging_profiles.py which #97 introduces.
Without the taper, the level algorithm would assume constant c_max
up to 100% SoC, producing unrealistic schedules. That said, it could
work without taper (just less accurate) if you prefer to decouple them.

The difference to attenuate_grid_peaks: that is a cost-neutral
tiebreaker inside the MILP. It nudges toward charging during high
solar but the primary objective is still cost minimization.

Solar-first is a different optimization objective entirely. It
directly computes the flat export ceiling that fills the battery
to target SoC. The export curve is mathematically flat by
construction, not an approximation through incentive weighting.

For flat-rate tariffs both point in the same direction, but
solar-first is purpose-built for the self-consumption goal.

CC-CVtaper_Solar-first

@andig

andig commented Aug 1, 2026

Copy link
Copy Markdown
Member

For flat-rate tariffs both point in the same direction, but
solar-first is purpose-built for the self-consumption goal.

I still don't get why we should be doing this. Why would self-consumption be a relevant goal and preferable over economic value plus peak shaving?

/cc @premultiply

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants