Skip to content

Commit b5b079b

Browse files
fix(plan): gate Freeze Export on a new inverter_can_freeze_export flag (#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>
1 parent d6770e5 commit b5b079b

5 files changed

Lines changed: 34 additions & 14 deletions

File tree

apps/predbat/fetch.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2396,6 +2396,7 @@ def fetch_config_options(self):
23962396
self.inverter_clock_skew_discharge_start = self.get_arg("inverter_clock_skew_discharge_start", 0)
23972397
self.inverter_clock_skew_discharge_end = self.get_arg("inverter_clock_skew_discharge_end", 0)
23982398
self.inverter_can_charge_during_export = self.get_arg("inverter_can_charge_during_export", True)
2399+
self.inverter_can_freeze_export = self.get_arg("inverter_can_freeze_export", True)
23992400

24002401
# Log clock skew
24012402
if self.inverter_clock_skew_start != 0 or self.inverter_clock_skew_end != 0:

apps/predbat/plan.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2126,11 +2126,15 @@ def optimise_export(self, window_n, record_charge_windows, try_charge_limit, cha
21262126
export_step = 5
21272127
export_step_large = 15
21282128

2129-
if not self.set_export_freeze or self.inverter_can_charge_during_export:
2130-
# An inverter that charges the battery from surplus PV regardless of what it's been told
2131-
# (inverter_can_charge_during_export) can't actually achieve a frozen SoC - it behaves
2132-
# identically to idle in that case, so offering freeze as a distinct option is pointless
2133-
# and would mislabel the plan (#4207/#4425).
2129+
if not self.set_export_freeze or not self.inverter_can_freeze_export:
2130+
# inverter_can_freeze_export is a different question to inverter_can_charge_during_export
2131+
# (that one is specifically about PV exceeding the export/inverter limit during *active*
2132+
# force export - see its own docstring). Some inverters simply cannot be commanded into a
2133+
# state where battery charging is disabled at all - they charge from any available surplus
2134+
# PV regardless of mode, so Freeze Export never achieves anything distinct from idle on
2135+
# them. Offering it as a distinct plan option there is pointless and mislabels the plan
2136+
# (#4207/#4538 - originally gated on inverter_can_charge_during_export, corrected after
2137+
# springfall2008 pointed out that conflated two genuinely different things).
21342138
allow_freeze = False
21352139

21362140
# loop on each export option
@@ -3054,8 +3058,8 @@ def optimise_solar(self, best_metric, best_cost, best_keep, best_cycle, best_car
30543058

30553059
# Freeze export slots only have an effect when export freeze is enabled, and only mean anything
30563060
# distinct from idle when the inverter can actually hold SoC flat - see optimise_export()'s
3057-
# matching gate (#4207/#4425) for why inverter_can_charge_during_export rules it out too.
3058-
if not self.calculate_best_export or not self.set_export_freeze or not self.export_window_best or self.inverter_can_charge_during_export:
3061+
# matching gate (#4207/#4538) for why inverter_can_freeze_export rules it out too.
3062+
if not self.calculate_best_export or not self.set_export_freeze or not self.export_window_best or not self.inverter_can_freeze_export:
30593063
return best_metric, best_cost, best_keep, best_cycle, best_carbon, best_import
30603064

30613065
pv_forecast_minute_step = self.prediction.pv_forecast_minute_step

apps/predbat/predbat.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,7 @@ def reset(self):
604604
self.set_export_low_power = False
605605
self.config_root = "./"
606606
self.inverter_can_charge_during_export = True
607+
self.inverter_can_freeze_export = True
607608
self.octopus_last_joined_try = None
608609
self.calculate_savings_max_charge_slots = 1
609610
self.inverter_data_last_fetch = None

apps/predbat/tests/test_optimise_solar.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def run_optimise_solar(
3232
expect_export_start=None,
3333
car_charging_slots=None,
3434
car_charging_from_battery=False,
35-
inverter_can_charge_during_export=False,
35+
inverter_can_freeze_export=True,
3636
):
3737
print("Starting optimise solar test {}".format(name))
3838
failed = False
@@ -42,7 +42,7 @@ def run_optimise_solar(
4242
my_predbat.calculate_best_export = calculate_best_export
4343
my_predbat.set_export_freeze = set_export_freeze
4444
my_predbat.set_charge_freeze = True
45-
my_predbat.inverter_can_charge_during_export = inverter_can_charge_during_export
45+
my_predbat.inverter_can_freeze_export = inverter_can_freeze_export
4646
my_predbat.export_more_solar_threshold = threshold
4747
my_predbat.soc_max = battery_size
4848
my_predbat.soc_kw = battery_soc
@@ -163,18 +163,19 @@ def run_optimise_solar_tests(my_predbat):
163163
set_export_freeze=False,
164164
)
165165

166-
# An inverter that charges the battery from surplus PV regardless of command can't actually hold
167-
# SoC flat, so freeze is no different from idle for it (#4207/#4425) - feature is a no-op here too,
168-
# same outcome as freeze_disabled above but via the capability gate instead of the switch
166+
# An inverter that can't be commanded into a state where battery charging is disabled at all
167+
# can't actually hold SoC flat, so freeze is no different from idle for it (#4207/#4538) - feature
168+
# is a no-op here too, same outcome as freeze_disabled above but via the capability gate instead
169+
# of the switch
169170
failed |= run_optimise_solar(
170-
"freeze_pointless_when_inverter_always_charges",
171+
"freeze_pointless_when_inverter_cannot_freeze",
171172
my_predbat,
172173
export_window_best=export_window_best,
173174
export_limits_best=[100.0, 100.0, 100.0],
174175
expect_export_limit=[100.0, 100.0, 100.0],
175176
pv_amount=3.0,
176177
threshold=100.0,
177-
inverter_can_charge_during_export=True,
178+
inverter_can_freeze_export=False,
178179
)
179180

180181
# An already active export window (limit 0) is left untouched, only idle ones are converted

docs/apps-yaml.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1149,6 +1149,19 @@ During a force export period if the generated solar exceeds the inverter limit o
11491149
If this setting is `true` then the inverter is able to charge the battery from excess PV while still in Force Export mode.
11501150
If this setting is `false` then the inverter will not charge the battery and the excess PV will be lost.
11511151

1152+
This is a different question to **inverter_can_freeze_export** below - it's specifically about PV exceeding the inverter/export limit during *active* Force Export, not about whether Freeze Export can hold the battery flat at all.
1153+
1154+
### **inverter_can_freeze_export**
1155+
1156+
Global setting, defaults to `true`.
1157+
1158+
Controls the way Predbat models your inverter, this does not change the way it is controlled.
1159+
1160+
Freeze Export is meant to disable battery charging entirely so all solar is exported. Some inverters (e.g. certain "Feed-in First" work modes) cannot be commanded into a state where charging is disabled at all - they keep charging the battery from any available surplus PV regardless of the mode requested.
1161+
1162+
If this setting is `true` (the default) Predbat assumes Freeze Export genuinely holds the battery flat, and will offer it as a distinct option in the plan.
1163+
If this setting is `false`, Predbat assumes your inverter can't actually achieve that - Freeze Export would behave identically to Idle - so it stops offering Freeze Export in the plan at all, rather than showing a `FrzExp` slot that wouldn't achieve anything different from doing nothing.
1164+
11521165
## Controlling the Inverter
11531166

11541167
There are a few different ways to control your inverter:

0 commit comments

Comments
 (0)