Skip to content

fix(config): FoxESS support_discharge_freeze should be False, matching FoxCloud - #4538

Merged
springfall2008 merged 6 commits into
mainfrom
fix/gate-freeze-on-inverter-capability
Aug 17, 2026
Merged

springfall2008 merged 6 commits into
mainfrom
fix/gate-freeze-on-inverter-capability

Conversation

@chalfontchubby

@chalfontchubby chalfontchubby commented Aug 15, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Fixes #4207. Supersedes #4425 (closed separately - see comment there).

Some inverters (e.g. FoxESS "Feed-in First") can't be commanded into a state where battery charging is disabled at all - they keep charging from any available surplus PV regardless of the mode Predbat has requested. That means Freeze Export (hold the battery flat, export the rest) can never actually be achieved on that hardware. Predbat's plan previously assumed freeze always holds SoC flat, so on this class of inverter it predicted "no charging" while the inverter actually kept charging from the surplus - a plan/reality mismatch.

#4425 tried to fix this by teaching the prediction model to simulate PV-charging during freeze too. That correctly modelled the physical behaviour, but as raised there: once applied, freeze (99%) and idle (100%) run through identical code for these inverters, so "Freeze Export" becomes a pure relabelling with zero distinguishing effect from doing nothing - the plan would keep showing a FrzExp slot that isn't actually achieving a freeze.

This PR takes the alternative raised in that discussion instead: rather than patch 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.

Revision history on this PR, for anyone following the thread: this went through two earlier approaches before landing here.

  1. Originally gated on the existing inverter_can_charge_during_export flag. @springfall2008 correctly pointed out that's a different question - PV exceeding the inverter/export limit during active Force Export, not whether Freeze Export can hold the battery flat at all.
  2. That was replaced with a new, dedicated inverter_can_freeze_export apps.yaml switch (default true). @springfall2008 then pointed out this duplicated an existing mechanism: INVERTER_DEF[type]["support_discharge_freeze"] already models exactly this per-inverter-type capability, and execute.py's fetch_inverter_data() already force-disables set_export_freeze/set_export_freeze_only together, early enough in the cycle to stop the optimiser ever offering freeze, whenever that flag is False.

What's actually in this PR now: no new switch, no plan.py/fetch.py/predbat.py changes at all. Just a one-line data fix: INVERTER_DEF["FoxESS"]["support_discharge_freeze"] was True, INVERTER_DEF["FoxCloud"]["support_discharge_freeze"] (the same hardware via a different connection method) was already correctly False. Only the modbus FoxESS entry was wrong, so #4207's reporter (connected via modbus) never got the protection the existing mechanism was already meant to provide.

This also sidesteps a real bug the second approach had: forcing allow_freeze = False via the new switch without also clearing set_export_freeze_only could fall through to offering forced export (0.0) in freeze-only mode, violating that mode's own semantics. The existing support_discharge_freeze path never has this problem, since it already clears both flags together.

Test plan

  • New test in test_teslemetry.py (test_foxess_support_discharge_freeze_matches_foxcloud) confirming FoxESS's value now matches FoxCloud's
  • ./run_all --quick passes
  • ./run_pre_commit passes

🤖 Generated with Claude Code

… 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>
@daviessm

Copy link
Copy Markdown
Contributor

Does this mean that for inverters with inverter_can_charge_during_export=true, "feed-in first" mode won't be used at all and the only way to get the system to export before the battery is full would be to set force discharge?

@springfall2008

Copy link
Copy Markdown
Owner

I'm not sure this seems right, freeze export disables battery charging allowing all solar to be exported.

Can charge during export just means can the battery charge if the solar exceeds the capability of the inverter to export, this is different.

@chalfontchubby

chalfontchubby commented Aug 16, 2026 via email

Copy link
Copy Markdown
Collaborator Author

#4207/#4538)

springfall2008 pointed out on #4538 that gating on
inverter_can_charge_during_export was wrong: that flag is specifically
about PV exceeding the inverter/export limit during *active* Force Export
(see its own docstring), not about whether Freeze Export can hold the
battery flat at all - two genuinely different questions this PR had
conflated.

Add inverter_can_freeze_export as its own setting (apps.yaml only, defaults
to true) and gate optimise_export()/optimise_solar()'s freeze offering on
it instead. Reusing inverter_can_charge_during_export's default of true
would have been wrong here too - most inverters genuinely can freeze, so
"assume it works, let affected users opt out" is the right default for
this flag as well, it just needed to be a different flag with a different
meaning, not a shared one.

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

chalfontchubby commented Aug 16, 2026 •

Copy link
Copy Markdown
Collaborator Author

@springfall2008 You're right, thanks for catching that - inverter_can_charge_during_export is specifically about PV exceeding the inverter/export limit during active Force Export, not about whether Freeze Export can hold the battery flat at all. Conflating those was a mistake.

Pushed a fix using a new, dedicated inverter_can_freeze_export flag instead (default true, so nothing changes for anyone unless they explicitly set it false) - PR description updated to match.

@chalfontchubby

Copy link
Copy Markdown
Collaborator Author

@daviessm To answer directly: no, this shouldn't take away "feed-in first"/pre-emptive export for you. The gate now checks a new inverter_can_freeze_export setting (defaults to true) rather than inverter_can_charge_during_export - since you've presumably never set the new one (it didn't exist until this PR), Freeze Export is offered exactly as before. It would only stop being offered if you explicitly set inverter_can_freeze_export: false because you'd confirmed your specific inverter can't actually hold SoC flat - not something this change does on its own.

@daviessm

Copy link
Copy Markdown
Contributor

@daviessm To answer directly: no, this shouldn't take away "feed-in first"/pre-emptive export for you. The gate now checks a new inverter_can_freeze_export setting (defaults to true) rather than inverter_can_charge_during_export - since you've presumably never set the new one (it didn't exist until this PR), Freeze Export is offered exactly as before. It would only stop being offered if you explicitly set inverter_can_freeze_export: false because you'd confirmed your specific inverter can't actually hold SoC flat - not something this change does on its own.

My FoxESS inverter doesn't have a mode to hold SoC flat so I should be setting the new flag; it should probably be a default for FoxESS as I believe they all work like this. If this then stops "feed-in first" from ever being planned, the only way for me to prioritise export over charging is by using "force discharge", right?

I have a low export limit (5kW) compared to array size (13kWp) so in the summer I would expect the plan to feed-in first until there'll be enough solar to top the battery up without clipping.

I still don't quite understand why "freeze export" exists - it expects the inverter to throw away/clip excess PV generation above the export limit, I think?

@chalfontchubby

Copy link
Copy Markdown
Collaborator Author

I suspect the combination of export limit vs solar power is a corner that isn't well covered for most people.

Holding the battery level flat and exporting the rest is a useful mode for planning I think - up to the point that you are hitting at which point charging seems the better plan.

I'm going to ask that @springfall2008 come in at this point.

@springfall2008

Copy link
Copy Markdown
Owner

but hold on isn't this just 'support_discharge_freeze' in the inverter config?

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 adds a new inverter capability flag (inverter_can_freeze_export) to prevent the optimiser from offering/using Freeze Export on inverters where Freeze Export cannot actually hold SoC flat (e.g., some “Feed-in First” modes). This keeps the generated plan aligned with what the hardware can meaningfully do, avoiding “FrzExp” plan slots that are effectively a no-op on such inverters.

Changes:

  • Introduces inverter_can_freeze_export (apps.yaml-only, default true) and threads it through config loading and defaults.
  • Gates Freeze Export selection in optimise_export() and “Export more solar” logic in optimise_solar().
  • Adds documentation and a regression test ensuring Freeze Export is not offered when the capability is disabled.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
docs/apps-yaml.md Documents new inverter_can_freeze_export flag and clarifies distinction from inverter_can_charge_during_export.
apps/predbat/tests/test_optimise_solar.py Adds regression coverage for “Export more solar” when Freeze Export is unsupported.
apps/predbat/predbat.py Adds default initialisation for inverter_can_freeze_export.
apps/predbat/plan.py Gates Freeze Export search-space in optimise_export() and optimise_solar().
apps/predbat/fetch.py Loads inverter_can_freeze_export from apps.yaml config.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread apps/predbat/plan.py Outdated
Comment on lines +2129 to +2133
if not self.set_export_freeze or not self.inverter_can_freeze_export:
# inverter_can_freeze_export is a different question to inverter_can_charge_during_export
# (that one is specifically about PV exceeding the export/inverter limit during *active*
# force export - see its own docstring). Some inverters simply cannot be commanded into a
# state where battery charging is disabled at all - they charge from any available surplus
chalfontchubby and others added 3 commits August 17, 2026 13:33
…g FoxCloud (#4207/#4538)

Reworks #4538 after springfall2008 pointed out the earlier inverter_can_freeze_export
switch duplicated an existing mechanism: INVERTER_DEF[type]["support_discharge_freeze"]
already gates this exact capability, and execute.py's fetch_inverter_data() already
force-disables set_export_freeze/set_export_freeze_only together, before planning runs,
whenever it's False.

FoxCloud's entry was already correctly False - only the modbus "FoxESS" entry was left
True, so #4207's reporter (connected via modbus) never got the protection. Using the
existing per-inverter-type mechanism is simpler than a new global switch and avoids a
review-caught semantic bug in the switch approach (forcing allow_freeze off without also
clearing set_export_freeze_only could fall through to offering forced export in
freeze-only mode) - that class of bug can't occur here since the two flags are already
cleared together upstream.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@chalfontchubby chalfontchubby changed the title fix(plan): don't offer Freeze Export for inverters that always charge from PV surplus fix(config): FoxESS support_discharge_freeze should be False, matching FoxCloud Aug 17, 2026
@chalfontchubby

Copy link
Copy Markdown
Collaborator Author

@springfall2008 You're right - support_discharge_freeze already covers this exactly, and execute.py's fetch_inverter_data() already forces set_export_freeze/set_export_freeze_only off together before planning runs whenever it's False. No need for a new switch at all.

Turned out FoxCloud's entry already had support_discharge_freeze: False correctly set - only the modbus FoxESS entry (what #4207's reporter uses) was left True. Reworked this PR down to that one-line fix, removed inverter_can_freeze_export entirely, PR description updated to match.

@daviessm this also answers your question more simply than before: nothing changes for you unless FoxESS's own support_discharge_freeze value is wrong for your model too - worth checking your dashboard's plan for a FrzExp slot that doesn't actually seem to hold SoC flat in practice, if you want to confirm one way or the other.

@springfall2008

Copy link
Copy Markdown
Owner

Okay great.

Why is the test for this in Teslemetry?

@chalfontchubby

Copy link
Copy Markdown
Collaborator Author

Okay great.

Why is the test for this in Teslemetry?

That is a very reasonable question. Shifting it. I looked more at the test than the filename and trusted Claude too much

…nverter.py

Wasn't Teslemetry-specific - springfall2008 flagged it on PR review.

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

Copy link
Copy Markdown
Collaborator Author

Fair catch - no reason for it to live there. Moved to test_inverter.py (called from run_inverter_tests), pushed.

@springfall2008
springfall2008 merged commit c779a20 into main Aug 17, 2026
2 checks passed
springfall2008 pushed a commit that referenced this pull request Aug 17, 2026
#4207/#4538)

springfall2008 pointed out on #4538 that gating on
inverter_can_charge_during_export was wrong: that flag is specifically
about PV exceeding the inverter/export limit during *active* Force Export
(see its own docstring), not about whether Freeze Export can hold the
battery flat at all - two genuinely different questions this PR had
conflated.

Add inverter_can_freeze_export as its own setting (apps.yaml only, defaults
to true) and gate optimise_export()/optimise_solar()'s freeze offering on
it instead. Reusing inverter_can_charge_during_export's default of true
would have been wrong here too - most inverters genuinely can freeze, so
"assume it works, let affected users opt out" is the right default for
this flag as well, it just needed to be a different flag with a different
meaning, not a shared one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
springfall2008 pushed a commit that referenced this pull request Aug 17, 2026
…g FoxCloud (#4207/#4538)

Reworks #4538 after springfall2008 pointed out the earlier inverter_can_freeze_export
switch duplicated an existing mechanism: INVERTER_DEF[type]["support_discharge_freeze"]
already gates this exact capability, and execute.py's fetch_inverter_data() already
force-disables set_export_freeze/set_export_freeze_only together, before planning runs,
whenever it's False.

FoxCloud's entry was already correctly False - only the modbus "FoxESS" entry was left
True, so #4207's reporter (connected via modbus) never got the protection. Using the
existing per-inverter-type mechanism is simpler than a new global switch and avoids a
review-caught semantic bug in the switch approach (forcing allow_freeze off without also
clearing set_export_freeze_only could fall through to offering forced export in
freeze-only mode) - that class of bug can't occur here since the two flags are already
cleared together upstream.

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

Copy link
Copy Markdown
Contributor

This change means that FoxESS (modbus or cloud) will never use "feed-in first" mode by design, right?

@chalfontchubby

Copy link
Copy Markdown
Collaborator Author

@daviessm I'm continuing to dig into this and am I think revising my opinions - I'll gather thoughts and propose the right move going forwards.
I think I'd focused too much on "fox doing things differently" overlooking the main issue here which is your pv much greater than export limit. I currently think sigenergy "freeze_export" does the same as your "feed in first" - it's perhaps just missing something in the modelling of the "what happens when we exceed the export limit".

@WyndStryke

WyndStryke commented Aug 18, 2026 •

Copy link
Copy Markdown

Feed-in first (household load first, then exporting surplus solar to the grid, then overflowing into the battery if grid export is maxed out) is an important mode that I use all the time. I do not want the battery to be charged from solar unless it is needed (either to support a later predicted load, or to store clipping), since that causes unnecessary round-trip losses, and unnecessary battery cycling.

If it doesn't map onto 'freeze export', then what does it map onto?

@chalfontchubby

Copy link
Copy Markdown
Collaborator Author

Current thinking is it is "freeze_export" - but predbat currently perhaps doesn't model well what happens when pv surplus exceeds the export limit. I will put in a new patch reinstating FIF as freeze_export but modelling it better

@WyndStryke

WyndStryke commented Aug 18, 2026 •

Copy link
Copy Markdown

I see from the earlier conversation that the FoxCloud integration doesn't implement it? Perhaps that's a problem too, since Feed-in-first is one of the 5 basic modes that the mode scheduler uses (feed-in-first, self-use/demand, backup, force charge, force discharge).

There's also peak-shaving mode but TBH I'm not entirely sure how that works.

@chalfontchubby

Copy link
Copy Markdown
Collaborator Author

Added #4579 to track fixing the mess I've created. Could I ask @WyndStryke and @daviessm to drop some debug.yaml in there (ideally with pv which both exceeds and does not exceed your export limit) and I'll repair the damage.

@chalfontchubby
chalfontchubby deleted the fix/gate-freeze-on-inverter-capability branch August 18, 2026 21:12
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

5 participants