Skip to content

fix(plan): let PV surplus charge the battery during an export freeze - #4425

Closed
chalfontchubby wants to merge 3 commits into
mainfrom
fix/fox-export-freeze-pv-charge
Closed

fix(plan): let PV surplus charge the battery during an export freeze#4425
chalfontchubby wants to merge 3 commits into
mainfrom
fix/fox-export-freeze-pv-charge

Conversation

@chalfontchubby

Copy link
Copy Markdown
Collaborator

Summary

  • Fixes FoxESS Freeze Export mode predicts no battery charge, but the battery does charge #4207: during an export freeze, prediction.py unconditionally zeroed the battery charge rate, ignoring inverter_can_charge_during_export. On inverters that route PV surplus to the battery during a freeze (e.g. FoxESS "Feed-in First"), Predbat's plan predicted no charging while the inverter actually charged the battery from surplus PV.
  • Only force the charge rate to zero when the inverter genuinely can't route surplus PV to the battery during export freeze (inverter_can_charge_during_export == False), matching the behaviour already used for the partial export-limit-exceeded case elsewhere in the same file.
  • Mirrored the fix in prediction_kernel.cpp and bumped PK_PARITY_REVISION/KERNEL_PARITY_REVISION per the kernel's parity contract.

Test plan

  • Added battery_discharge_freeze_pv_charge / _hybrid regression scenarios in tests/test_model.py asserting SoC rises from PV surplus during a freeze with the default inverter_can_charge_during_export=True
  • Updated the 3 existing freeze scenarios to set inverter_can_charge_during_export=False, preserving their original "inverter genuinely can't charge" intent
  • ./run_all --quick passes
  • ./run_all --test kernel_parity passes (150-seed Python/C++ fuzz parity suite)
  • ./run_pre_commit passes

🤖 Generated with Claude Code

…4207)

The freeze-export guard in prediction.py unconditionally zeroed the charge
rate, ignoring inverter_can_charge_during_export - so on inverters that
route PV surplus to the battery during a freeze (e.g. FoxESS "Feed-in
First"), Predbat's plan wrongly predicted no charging while the inverter
actually charged. Mirrored the fix in prediction_kernel.cpp and bumped the
kernel parity revision.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
github-actions Bot and others added 2 commits August 2, 2026 22:19
Empty commit - the previous rerun replayed the original stale-binary commit
rather than HEAD (which already has the bot's cross-built kernel binaries),
so it failed the same way. This forces a fresh pull_request run against HEAD.

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

Copy link
Copy Markdown
Owner

I'm confused, why does this not just stop export freeze from doing anything at all?

Copilot AI 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.

Pull request overview

This PR fixes Predbat’s prediction logic during an export-freeze so that PV surplus can still charge the battery on inverters that support that behaviour (via inverter_can_charge_during_export, e.g. FoxESS “Feed-in First”), aligning the model with real inverter behaviour and preventing plan/SoC drift.

Changes:

  • Update Python prediction logic to only force charge rate to zero during export-freeze when inverter_can_charge_during_export is false.
  • Mirror the same logic change in the C++ prediction kernel and bump parity revisions to maintain Python/C++ parity contracts.
  • Add regression scenarios and update existing freeze scenarios to explicitly cover both “can charge during export” and “cannot charge” inverter behaviours.

Reviewed changes

Copilot reviewed 4 out of 10 changed files in this pull request and generated no comments.

File Description
apps/predbat/tests/test_model.py Adds/adjusts regression scenarios to validate SoC rises from PV surplus during export-freeze when supported.
apps/predbat/prediction.py Fixes export-freeze handling to avoid incorrectly zeroing charge rate when PV surplus can be routed to battery.
apps/predbat/prediction_kernel.py Bumps kernel parity revision to reflect the mirrored behaviour change in the C++ kernel.
apps/predbat/prediction_kernel.cpp Mirrors the Python fix in the C++ kernel and updates parity revision/comments for traceability.

@chalfontchubby

Copy link
Copy Markdown
Collaborator Author

I'm confused, why does this not just stop export freeze from doing anything at all?

That's a reasonable point that I maybe let Claude get carried away with. The alternative raised on the initial issue was perhaps to mark fox as not capable of a genuine freeze - since it does seem to charge I think.

Fair to say I'm more confused than you most of the time.

@chalfontchubby

Copy link
Copy Markdown
Collaborator Author

Good challenge - dug into it properly and you're right, not just semantically.

Once this fix is applied, export_limit_now == 99 (freeze) and == 100 (idle/off) run through identical code in prediction.py for an inverter with inverter_can_charge_during_export=True - both fail the same < 99.0 check that gates genuine active discharge-to-export, and both fall through to plain self-use behaviour (PV charges the battery, load draws from it normally as needed). There's no remaining code path that tells them apart. So for FoxESS-type inverters this fix makes "freeze export" a pure relabelling with zero distinguishing effect from doing nothing at all - which is exactly your point: it's not freeze export anymore.

Cleaner alternative, taking your suggestion: gate allow_freeze in optimise_export() (plan.py) - currently only checks self.set_export_freeze - on not inverter_can_charge_during_export too, so the optimiser never offers/selects freeze for these inverters in the first place. That lets me revert prediction.py and the C++ kernel entirely (no parity bump, no kernel-binary rebuild), and stops the plan from showing a "FrzExp" label on a slot that isn't really frozen.

One wrinkle before I go do that: optimise_solar() ("Export more solar") sets export_limits_best[window_n] = 99.0 directly on idle windows to avoid wasting PV, bypassing optimise_export's search entirely - so it would need the same capability gate, or it'd quietly reintroduce this bug through the back door once prediction.py is reverted. Digging into it, I'm also not convinced that pass does much for non-capable inverters either under the current model (freeze and idle already trace through the same discharge-side logic regardless of capability) - but that feels like a separate question, not something to fold into this fix as a side effect.

Want me to rework #4425 along those lines (gate in optimise_export + optimise_solar, revert the prediction-model changes), or is there a simpler angle you'd rather take?

@chalfontchubby

Copy link
Copy Markdown
Collaborator Author

Closing this in favour of #4538 - it's gotten stale (merge conflicts against main's kernel churn) and, per the discussion in this thread, doesn't actually resolve the underlying problem: once the prediction model is patched to simulate PV-charging during freeze on a capable inverter, freeze (99%) and idle (100%) become indistinguishable in the model, so "Freeze Export" stops meaning anything different from doing nothing at all - the plan would still show a FrzExp slot that isn't achieving a genuine freeze.

#4538 takes the alternative raised here instead: stop the optimiser offering/selecting freeze for inverters that can't actually achieve it (inverter_can_charge_during_export=true), rather than patch the model to simulate a distinction that doesn't exist in reality. No prediction.py/kernel changes needed as a result.

Originating issue: #4207.

springfall2008 pushed a commit that referenced this pull request Aug 17, 2026
… from PV surplus (#4207)

Alternative to the stale #4425 (which patched the prediction model instead
- reworking it here per that PR's own follow-up discussion): on an inverter
with inverter_can_charge_during_export=true (the default), PV surplus
charges the battery regardless of what mode Predbat has commanded, so
Freeze Export (hold SoC flat, export the rest) can never actually be
achieved - it collapses to identical behaviour as Idle. Rather than teach
the prediction model to simulate a distinction that doesn't exist in
reality, stop the optimiser offering/selecting freeze for these inverters
in the first place:

- optimise_export(): allow_freeze is now also forced off when the inverter
  can charge during export, alongside the existing set_export_freeze check.
- optimise_solar() ("Export more solar"): sets export limits to 99.0
  (freeze) directly, bypassing optimise_export()'s search entirely, so it
  needs the same gate independently or it reintroduces the bug through the
  back door.

No prediction.py or kernel changes needed - this is purely a search-space
restriction, not a modelling change, so no parity bump or binary rebuild.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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.

FoxESS Freeze Export mode predicts no battery charge, but the battery does charge

3 participants