Skip to content

core: pass grid feed-in cap to optimizer when curtailment active - #31949

Merged
andig merged 2 commits into
masterfrom
feat/optimizer-feedin-cap
Jul 19, 2026
Merged

core: pass grid feed-in cap to optimizer when curtailment active#31949
andig merged 2 commits into
masterfrom
feat/optimizer-feedin-cap

Conversation

@andig

@andig andig commented Jul 19, 2026

Copy link
Copy Markdown
Member

part of #23042

Feed the active HEMS production/feed-in limit (e.g. the German 70% rule) into the optimizer as the grid export cap, so it can schedule battery charging into the PV peak and recover energy that would otherwise be curtailed.

  • optimizerUpdate now sets req.Grid.PMaxExp from site.hems.MaxProductionPower() when a limit is active (non-nil and > 0).
  • Restructured the existing PMaxImp assignment to set the field in place so the import and export caps can coexist.

The optimizer already treats p_max_exp as a soft cap: excess PV is curtailed rather than exported, so this never makes the schedule infeasible. The optimizer-side behavior is pinned by a test case in evcc-io/optimizer#93.

🤖 Generated with Claude Code

When a HEMS reports an active production/feed-in limit (e.g. German 70%
rule), send it as the optimizer grid export cap so battery charging is
scheduled into the PV peak to recover otherwise-curtailed energy.
@andig andig added enhancement New feature or request experimental Experimental feature labels Jul 19, 2026

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • Consider whether req.Grid should be initialized explicitly before setting PMaxImp/PMaxExp, to avoid unintentionally overwriting any existing optimizer grid configuration elsewhere and to make the intended merge semantics clear.
  • It may be worth clarifying in a comment or code guard what should happen when both site.circuit.GetMaxPower() and site.hems.MaxProductionPower() are present but conflicting, so future changes don't accidentally assume one cap always dominates the other.
  • If site.hems.MaxProductionPower() can change during runtime, consider whether caching or debouncing is needed to avoid frequent small updates to PMaxExp that could cause unnecessary rescheduling churn.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Consider whether `req.Grid` should be initialized explicitly before setting `PMaxImp`/`PMaxExp`, to avoid unintentionally overwriting any existing optimizer grid configuration elsewhere and to make the intended merge semantics clear.
- It may be worth clarifying in a comment or code guard what should happen when both `site.circuit.GetMaxPower()` and `site.hems.MaxProductionPower()` are present but conflicting, so future changes don't accidentally assume one cap always dominates the other.
- If `site.hems.MaxProductionPower()` can change during runtime, consider whether caching or debouncing is needed to avoid frequent small updates to `PMaxExp` that could cause unnecessary rescheduling churn.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@jeremiahpslewis

Copy link
Copy Markdown

Question here: is there a generic way to communicate fixed feed-in curtailment which is implemented on inverter side so that optimizer can use it? This hems piece seems more relevant for steuerbox cases, can't find anything in documentation.

@andig

andig commented Jul 19, 2026

Copy link
Copy Markdown
Member Author

Question here

That's a different topic, lets not hide it here.

@andig
andig enabled auto-merge (squash) July 19, 2026 16:35
@andig andig mentioned this pull request Jul 19, 2026
17 tasks
@andig
andig merged commit 1018393 into master Jul 19, 2026
8 of 9 checks passed
@andig
andig deleted the feat/optimizer-feedin-cap branch July 19, 2026 16:38
@github-actions

Copy link
Copy Markdown
Contributor

Code review

Bug: missing nil-check on site.hems will break the optimizer for any site without a HEMS configured

evcc/core/site_optimizer.go

Lines 333 to 341 in 426b3b8

// soft grid feed-in cap from active HEMS curtailment (e.g. German 70% rule):
// export is capped at this power, excess PV is curtailed instead of exported
if curtailed := hems.Curtailed(site.hems); curtailed != nil && *curtailed {
if pMaxExp := site.hems.MaxProductionPower(); pMaxExp != nil {
req.Grid.PMaxExp = float32(*pMaxExp)
}
}

// soft grid feed-in cap from active HEMS curtailment (e.g. German 70% rule):
// export is capped at this power, excess PV is curtailed instead of exported
if curtailed := hems.Curtailed(site.hems); curtailed != nil && *curtailed {
    if pMaxExp := site.hems.MaxProductionPower(); pMaxExp != nil {
        req.Grid.PMaxExp = float32(*pMaxExp)
    }
}

site.hems is a nil api.HEMS interface whenever no HEMS device is configured (it's only ever set via site.SetHEMS, which cmd/setup.go only calls when a HEMS device actually exists). hems.Curtailed calls hems.CurtailedPercent() on the argument unconditionally with no nil guard:

func Curtailed(hems api.HEMS) *bool {
	percent := hems.CurtailedPercent() // panics on a nil interface
	...
}

Calling a method on a nil interface value panics. Since optimizerUpdate runs on every optimizer cycle regardless of whether HEMS is configured, this panics every cycle for any installation that uses the optimizer without a HEMS device — the panic is caught by the recover() in optimizerUpdateAsync, so it doesn't crash the process, but it leaves the optimizer feature permanently non-functional for that large segment of users (logged as an error every cycle instead).

The existing sibling code just above (if site.circuit != nil { ... }) and the established pattern elsewhere in the codebase (e.g. core/site.go guards every use of site.hems with if site.hems != nil) both show the intended guard was simply omitted here.

Suggested fix:

if site.hems != nil {
    if curtailed := hems.Curtailed(site.hems); curtailed != nil && *curtailed {
        if pMaxExp := site.hems.MaxProductionPower(); pMaxExp != nil {
            req.Grid.PMaxExp = float32(*pMaxExp)
        }
    }
}

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

Labels

enhancement New feature or request experimental Experimental feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants