Repository navigation
fix(config): FoxESS support_discharge_freeze should be False, matching FoxCloud - #4538
Conversation
… 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>
|
Does this mean that for inverters with |
|
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. |
|
I wondered about a new switch - battery can’t not charge while exporting,Freeze_export_supported, something like that
Sent from Yahoo Mail for iPhone
On Sunday, August 16, 2026, 6:37 pm, Trefor Southwell ***@***.***> wrote:
springfall2008 left a comment (springfall2008/batpred#4538)
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.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you authored the thread.Message ID: ***@***.***>
|
#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 You're right, thanks for catching that - Pushed a fix using a new, dedicated |
|
@daviessm To answer directly: no, this shouldn't take away "feed-in first"/pre-emptive export for you. The gate now checks a new |
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? |
|
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. |
|
but hold on isn't this just 'support_discharge_freeze' in the inverter config? |
There was a problem hiding this comment.
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, defaulttrue) and threads it through config loading and defaults. - Gates Freeze Export selection in
optimise_export()and “Export more solar” logic inoptimise_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.
| 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 |
… (WIP, pre-merge)
…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>
|
@springfall2008 You're right - Turned out @daviessm this also answers your question more simply than before: nothing changes for you unless FoxESS's own |
|
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>
|
Fair catch - no reason for it to live there. Moved to |
#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>
…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>
|
This change means that FoxESS (modbus or cloud) will never use "feed-in first" mode by design, right? |
|
@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. |
|
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? |
|
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 |
|
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. |
|
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. |
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
FrzExpslot 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.
inverter_can_charge_during_exportflag. @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.inverter_can_freeze_exportapps.yaml switch (defaulttrue). @springfall2008 then pointed out this duplicated an existing mechanism:INVERTER_DEF[type]["support_discharge_freeze"]already models exactly this per-inverter-type capability, andexecute.py'sfetch_inverter_data()already force-disablesset_export_freeze/set_export_freeze_onlytogether, early enough in the cycle to stop the optimiser ever offering freeze, whenever that flag isFalse.What's actually in this PR now: no new switch, no
plan.py/fetch.py/predbat.pychanges at all. Just a one-line data fix:INVERTER_DEF["FoxESS"]["support_discharge_freeze"]wasTrue,INVERTER_DEF["FoxCloud"]["support_discharge_freeze"](the same hardware via a different connection method) was already correctlyFalse. Only the modbusFoxESSentry 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 = Falsevia the new switch without also clearingset_export_freeze_onlycould fall through to offering forced export (0.0) in freeze-only mode, violating that mode's own semantics. The existingsupport_discharge_freezepath never has this problem, since it already clears both flags together.Test plan
test_teslemetry.py(test_foxess_support_discharge_freeze_matches_foxcloud) confirmingFoxESS's value now matchesFoxCloud's./run_all --quickpasses./run_pre_commitpasses🤖 Generated with Claude Code