Skip to content

Feat: add primary_goal to prefer self-consumption over selling - #156

Draft
djfanatix wants to merge 1 commit into
evcc-io:mainfrom
djfanatix:feat/self-consumption-goal
Draft

djfanatix wants to merge 1 commit into
evcc-io:mainfrom
djfanatix:feat/self-consumption-goal

Conversation

@djfanatix

Copy link
Copy Markdown

This (Claude generated) PR adds a new mode to Primary Goal: Max. Self Consumption.
The idea: Maximize the use of solar, independent of the prices. This can be useful if you are driving during the day, and don't want the optimizer to sell all your solar and come home to a house with empty batteries and an empty car.
If you use the planner, it will also calculate solar + grid prices.

This is claude generated, and if you agree to this, I can make also the PR in EVCC repo.
I am running this now live, and works fine


charging_strategy/discharging_strategy only break ties between schedules the cost stage already finds equally cheap - they never change what counts as optimal. On a flat feed-in rate (or any slot where p_a for a battery's stored energy undercuts p_E/eta_c after efficiency loss), the cost stage itself decides exporting beats storing, and no tiebreaker can undo that.

primary_goal: maximize_self_consumption changes the cost stage instead: export is now weighted like import - a cost, at the same p_E it used to earn as revenue - rather than money the solver collects. A battery with headroom is then preferred over exporting even when exporting would strictly earn more, without touching the feasibility constraints, the preference stage, or (with primary_goal left at its minimize_cost default) any existing request's behavior.

  • optimizer.py: OptimizationStrategy gains primary_goal (default 'minimize_cost'); PRIMARY_GOALS lists the valid values next to CHARGING_STRATEGIES/DISCHARGING_STRATEGIES. _setup_target_function flips the sign on the export term for maximize_self_consumption; the import term, the p_a final-value term and penalty_base's price-based scaling are untouched, so constraint penalties stay correctly calibrated in both modes.
  • app.py / openapi.yaml: primary_goal on OptimizationStrategy, validated against PRIMARY_GOALS the same way the other two strategy fields are.
  • client: regenerated (go generate ./...).
  • tests/test_primary_goal.py: a single-step case where p_a is set below p_E/eta_c on purpose - minimize_cost exports the full surplus, maximize_self_consumption charges the battery with it instead; plus a default-value regression test.

charging_strategy/discharging_strategy only break ties between schedules
the cost stage already finds equally cheap - they never change what counts
as optimal. On a flat feed-in rate (or any slot where p_a for a battery's
stored energy undercuts p_E/eta_c after efficiency loss), the cost stage
itself decides exporting beats storing, and no tiebreaker can undo that.

primary_goal: maximize_self_consumption changes the cost stage instead:
export is now weighted like import - a cost, at the same p_E it used to
earn as revenue - rather than money the solver collects. A battery with
headroom is then preferred over exporting even when exporting would
strictly earn more, without touching the feasibility constraints, the
preference stage, or (with primary_goal left at its minimize_cost default)
any existing request's behavior.

- optimizer.py: OptimizationStrategy gains primary_goal (default
  'minimize_cost'); PRIMARY_GOALS lists the valid values next to
  CHARGING_STRATEGIES/DISCHARGING_STRATEGIES. _setup_target_function flips
  the sign on the export term for maximize_self_consumption; the import
  term, the p_a final-value term and penalty_base's price-based scaling
  are untouched, so constraint penalties stay correctly calibrated in
  both modes.
- app.py / openapi.yaml: primary_goal on OptimizationStrategy, validated
  against PRIMARY_GOALS the same way the other two strategy fields are.
- client: regenerated (go generate ./...).
- tests/test_primary_goal.py: a single-step case where p_a is set below
  p_E/eta_c on purpose - minimize_cost exports the full surplus,
  maximize_self_consumption charges the battery with it instead; plus a
  default-value regression test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@andig

andig commented Sep 15, 2026

Copy link
Copy Markdown
Member

Why do we need this?

A battery with headroom is then preferred over exporting even when exporting would strictly earn more

Why would you do this?

@andig
andig marked this pull request as draft September 15, 2026 14:28
@djfanatix

djfanatix commented Sep 15, 2026

Copy link
Copy Markdown
Author

Why do we need this?

A battery with headroom is then preferred over exporting even when exporting would strictly earn more

Why would you do this?

To store more solar then needed, in case the EV returns home, so EVCC doesn't need to charge the home battery anymore (or the day after), and can focus on the EV

@andig

andig commented Sep 15, 2026

Copy link
Copy Markdown
Member

I understand. But- given this is a manual intervention anyway- why does this need to be in the optimizer? The whole point is automatic processing for a given goal:

An HTTP service that plans the cheapest way to run a home energy system over the next hours or days.

If that's not the desired goal, why not just disable auto mode?

@djfanatix

Copy link
Copy Markdown
Author

Because with disabled auto-mode, the planner still uses tariff only approach to charge the car, it doesn’t take into account any forecasted solar.

And when EV and batteries are full, it behaves as a normal optimizer.

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