Skip to content

Commit e5bbcca

Browse files
Merge pull request #4582 from springfall2008/feat/freeze-export-recapture-modelling
2 parents f40fb40 + b12dceb commit e5bbcca

15 files changed

Lines changed: 256 additions & 146 deletions

‎apps/predbat/config.py‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2068,9 +2068,12 @@
20682068
"has_time_window": False,
20692069
"support_charge_freeze": True,
20702070
# "Feed-in first"/freeze export mode does not hold SoC flat on FoxESS - PV above the
2071-
# export limit still charges the battery instead of being clipped (#4207) - matching
2072-
# FoxCloud's entry below, which was already correctly set False for the same hardware.
2073-
"support_discharge_freeze": False,
2071+
# export limit still charges the battery instead of being clipped (#4207). That's now
2072+
# correctly modelled (prediction.py's freeze branch, gated on
2073+
# inverter_can_charge_during_export) rather than treated as a reason to disable freeze
2074+
# outright, so this can stay True - see FoxCloud's entry below for the same hardware via
2075+
# a different connection method.
2076+
"support_discharge_freeze": True,
20742077
"has_idle_time": False,
20752078
"can_span_midnight": True,
20762079
"charge_discharge_with_rate": False,
@@ -2099,7 +2102,8 @@
20992102
"write_and_poll_sleep": 2,
21002103
"has_time_window": False,
21012104
"support_charge_freeze": True,
2102-
"support_discharge_freeze": False,
2105+
# See FoxESS's entry above - same hardware, correctly modelled rather than disabled (#4207).
2106+
"support_discharge_freeze": True,
21032107
"has_idle_time": False,
21042108
"can_span_midnight": False,
21052109
"charge_discharge_with_rate": False,
@@ -2438,6 +2442,7 @@
24382442
"pause_start_time": {"type": "sensor_list", "sensor_type": "none|string", "modify": True, "entries": "num_inverters"},
24392443
"pause_end_time": {"type": "sensor_list", "sensor_type": "none|string", "modify": True, "entries": "num_inverters"},
24402444
"inverter_limit": {"type": "sensor_list", "sensor_type": "float", "modify": False, "zero": False, "entries": "num_inverters"},
2445+
"inverter_can_charge_during_export": {"type": "boolean"},
24412446
"pv_ac_limit": {"type": "float", "zero": True},
24422447
"inverter_limit_charge": {"type": "sensor_list", "sensor_type": "integer", "modify": False, "zero": False, "entries": "num_inverters"},
24432448
"inverter_limit_charge_dc": {"type": "sensor_list", "sensor_type": "integer", "modify": False, "zero": False, "entries": "num_inverters"},

‎apps/predbat/prediction.py‎

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -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)

‎apps/predbat/prediction_kernel.cpp‎

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
// falling back. Bumping makes the loader reject it and use the Python engine, which is the whole
4444
// point of the check.
4545
#define PK_ABI_VERSION 4
46-
#define PK_PARITY_REVISION 6
46+
#define PK_PARITY_REVISION 7
4747
#define PK_MAX_CARS 8
4848
#define PK_RUN_EVERY 5 // const.py RUN_EVERY
4949

@@ -905,13 +905,6 @@ static int32_t pk_run_one(const ContextStore *store, const PkScenario *s, PkResu
905905
}
906906
}
907907

908-
// Discharge freeze - prediction.py:764-768
909-
if (c->set_export_freeze) {
910-
if (export_window_active && export_limit_now < 100.0 && (c->set_export_freeze && (export_limit_now == 99.0 || c->set_export_freeze_only))) {
911-
charge_rate_now = battery_rate_min; // 0
912-
}
913-
}
914-
915908
// Set discharge during charge - prediction.py:770-775
916909
if (charge_window_active) {
917910
if (!c->set_discharge_during_charge) {
@@ -1059,6 +1052,31 @@ static int32_t pk_run_one(const ContextStore *store, const PkScenario *s, PkResu
10591052
metric_keep += std::max(potential_import * import_rate, 0.0);
10601053
}
10611054
}
1055+
} else if (c->set_export_freeze && export_window_active && export_limit_now < 100.0 && (export_limit_now == 99.0 || c->set_export_freeze_only)) {
1056+
// Freeze - not an active discharge, but genuine PV surplus beyond what
1057+
// load+export_limit can absorb still charges the battery on some inverters rather
1058+
// than being clipped (#4207) - mirrors the recapture logic in the force export
1059+
// branch above, without any active discharge. prediction.py's matching elif.
1060+
battery_draw = 0;
1061+
pv_ac = pv_now * inverter_loss_ac;
1062+
pv_dc = 0;
1063+
1064+
const double diff_freeze = get_diff(battery_draw, pv_dc, pv_ac, load_yesterday, inverter_loss, inverter_loss_recp);
1065+
if (diff_freeze < 0 && std::fabs(diff_freeze) > export_limit && c->inverter_can_charge_during_export) {
1066+
const double over_limit = std::fabs(diff_freeze) - export_limit;
1067+
if (inverter_hybrid) {
1068+
const double charge_rate_now_curve_dc = rate_curve(soc, battery_rate_max_charge_dc, battery_rate_max_charge_dc, c->temp_charge_cap[k], c->charge_curve, soc_max, battery_rate_min) * battery_rate_max_scaling;
1069+
const double charge_rate_now_curve_dc_step = charge_rate_now_curve_dc * step;
1070+
battery_draw = std::max({-over_limit * inverter_loss_recp, -battery_to_max, -charge_rate_now_curve_dc_step});
1071+
} else {
1072+
battery_draw = std::max({-over_limit * inverter_loss, -battery_to_max, -charge_rate_now_curve_step});
1073+
}
1074+
1075+
if (battery_draw < 0) {
1076+
pv_dc = std::min(std::fabs(battery_draw), pv_now);
1077+
pv_ac = (pv_now - pv_dc) * inverter_loss_ac;
1078+
}
1079+
}
10621080
} else {
10631081
// ECO Mode - prediction.py:951-997
10641082
pv_ac = pv_now * inverter_loss_ac;
@@ -1075,11 +1093,9 @@ static int32_t pk_run_one(const ContextStore *store, const PkScenario *s, PkResu
10751093
battery_draw = std::min({diff, discharge_rate_now_curve_step, inverter_limit, battery_to_min});
10761094
} else {
10771095
if (inverter_hybrid) {
1078-
double charge_rate_now_dc = battery_rate_max_charge_dc;
1079-
// Freeze mode - prediction.py:973-975
1080-
if (c->set_export_freeze && export_window_active && export_limit_now < 100.0 && (export_limit_now == 99.0 || c->set_export_freeze_only)) {
1081-
charge_rate_now_dc = battery_rate_min; // 0
1082-
}
1096+
const double charge_rate_now_dc = battery_rate_max_charge_dc;
1097+
// Freeze windows are handled by their own else-if branch above and never
1098+
// reach here - no need to zero the charge rate for them in this branch.
10831099
// Note: Python passes the un-rounded soc for the DC-rate lookup here
10841100
const double charge_rate_now_curve_dc = rate_curve(soc, charge_rate_now_dc, battery_rate_max_charge_dc, c->temp_charge_cap[k], c->charge_curve, soc_max, battery_rate_min) * battery_rate_max_scaling;
10851101
const double charge_rate_now_curve_dc_step = charge_rate_now_curve_dc * step;

‎apps/predbat/prediction_kernel.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232

3333
# Expected ABI/parity revisions of the shared library (see prediction_kernel.cpp)
3434
KERNEL_ABI_VERSION = 4
35-
KERNEL_PARITY_REVISION = 6
35+
KERNEL_PARITY_REVISION = 7
3636

3737
# Maximum number of cars supported by the kernel (PK_MAX_CARS in prediction_kernel.cpp)
3838
KERNEL_MAX_CARS = PREDBAT_MAX_CARS
368 Bytes
Binary file not shown.
400 Bytes
Binary file not shown.
0 Bytes
Binary file not shown.
0 Bytes
Binary file not shown.
176 Bytes
Binary file not shown.
16 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)