@@ -871,12 +871,6 @@ def run_prediction(self, charge_limit, charge_window, export_window, export_limi
871871 # Count load
872872 load_kwh += load_yesterday
873873
874- # discharge freeze, reset charge rate by default
875- if set_export_freeze :
876- # Freeze mode
877- if (export_window_active ) and export_limit_now < 100.0 and (set_export_freeze and (export_limit_now == 99.0 or set_export_freeze_only )):
878- charge_rate_now = battery_rate_min # 0
879-
880874 # Set discharge during charge?
881875 if charge_window_active :
882876 if not set_discharge_during_charge :
@@ -1066,6 +1060,36 @@ def run_prediction(self, charge_limit, charge_window, export_window, export_limi
10661060 pv_in_period = pv_compare / step * charge_time_remains
10671061 potential_import = min ((charge_rate_now_curve * charge_time_remains ) - pv_in_period , (charge_limit_n - soc ))
10681062 metric_keep += max (potential_import * import_rate , 0 )
1063+ elif set_export_freeze and export_window_active and export_limit_now < 100.0 and (export_limit_now == 99.0 or set_export_freeze_only ):
1064+ # Freeze - the battery is not actively discharged to help export, but genuine PV
1065+ # surplus beyond what load+export_limit can absorb still charges it on some
1066+ # inverters rather than being clipped (#4207) - e.g. FoxESS "Feed-in First"
1067+ # prioritises load, then export, then the battery. Only the genuine overflow is
1068+ # charged (not the full charge rate), so freeze still holds SoC flat whenever the
1069+ # export limit alone can absorb all the surplus - matching the equivalent recapture
1070+ # logic in the force export branch above, just without any active discharge.
1071+ battery_draw = 0
1072+ pv_ac = pv_now * inverter_loss_ac
1073+ pv_dc = 0
1074+
1075+ diff = get_diff (battery_draw , pv_dc , pv_ac , load_yesterday , inverter_loss , inverter_loss_recp )
1076+ if diff < 0 and abs (diff ) > export_limit and self .inverter_can_charge_during_export :
1077+ over_limit = abs (diff ) - export_limit
1078+ if inverter_hybrid :
1079+ charge_rate_now_curve_dc = (
1080+ get_charge_rate_curve_cached (soc , battery_rate_max_charge_dc , soc_max , battery_rate_max_charge_dc , battery_charge_power_curve_tuple , battery_rate_min , battery_temperature , battery_temperature_charge_curve_tuple )
1081+ * battery_rate_max_scaling
1082+ )
1083+ charge_rate_now_curve_dc_step = charge_rate_now_curve_dc * step
1084+ battery_draw = max (- over_limit * inverter_loss_recp , - battery_to_max , - charge_rate_now_curve_dc_step )
1085+ else :
1086+ battery_draw = max (- over_limit * inverter_loss , - battery_to_max , - charge_rate_now_curve_step )
1087+
1088+ if battery_draw < 0 :
1089+ pv_dc = min (abs (battery_draw ), pv_now )
1090+ pv_ac = (pv_now - pv_dc ) * inverter_loss_ac
1091+
1092+ battery_state = "fz+" if battery_draw < 0 else "fz~"
10691093 else :
10701094 # ECO Mode
10711095 pv_ac = pv_now * inverter_loss_ac
@@ -1088,9 +1112,8 @@ def run_prediction(self, charge_limit, charge_window, export_window, export_limi
10881112 # Battery draw is only subject to inverter limit for the AC part
10891113 if inverter_hybrid :
10901114 charge_rate_now_dc = battery_rate_max_charge_dc
1091- # Freeze mode
1092- if set_export_freeze and export_window_active and export_limit_now < 100.0 and (export_limit_now == 99.0 or set_export_freeze_only ):
1093- charge_rate_now_dc = battery_rate_min # 0
1115+ # Freeze windows are handled by their own elif branch above and never reach
1116+ # here - no need to zero the charge rate for them in this branch.
10941117
10951118 charge_rate_now_curve_dc = (
10961119 get_charge_rate_curve_cached (soc , charge_rate_now_dc , soc_max , battery_rate_max_charge_dc , battery_charge_power_curve_tuple , battery_rate_min , battery_temperature , battery_temperature_charge_curve_tuple )
0 commit comments