Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions apps/predbat/prediction.py
Original file line number Diff line number Diff line change
Expand Up @@ -795,8 +795,10 @@ def run_prediction(self, charge_limit, charge_window, export_window, export_limi

# discharge freeze, reset charge rate by default
if set_export_freeze:
# Freeze mode
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)):
# Freeze mode - PV surplus beyond load/export still charges the battery on inverters that
# route it there during an export freeze (e.g. FoxESS "Feed-in First"), rather than clipping
# it, so only force the charge rate to zero when the inverter genuinely can't do that (#4207).
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)) and not self.inverter_can_charge_during_export:
charge_rate_now = battery_rate_min # 0

# Set discharge during charge?
Expand Down Expand Up @@ -1010,8 +1012,9 @@ def run_prediction(self, charge_limit, charge_window, export_window, export_limi
# Battery draw is only subject to inverter limit for the AC part
if inverter_hybrid:
charge_rate_now_dc = battery_rate_max_charge_dc
# Freeze mode
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):
# Freeze mode - see the equivalent non-hybrid guard above (#4207): only clip PV
# surplus to zero-charge when the inverter can't route it to the battery itself.
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) and not self.inverter_can_charge_during_export:
charge_rate_now_dc = battery_rate_min # 0

charge_rate_now_curve_dc = (
Expand Down
13 changes: 8 additions & 5 deletions apps/predbat/prediction_kernel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
#include <vector>

#define PK_ABI_VERSION 2
#define PK_PARITY_REVISION 2
#define PK_PARITY_REVISION 3
#define PK_MAX_CARS 4
#define PK_RUN_EVERY 5 // const.py RUN_EVERY

Expand Down Expand Up @@ -579,9 +579,12 @@ int32_t pk_run(int64_t handle, const PkScenario *s, PkResult *out)
}
}

// Discharge freeze - prediction.py:764-768
// Discharge freeze - prediction.py:797-802
// PV surplus beyond load/export still charges the battery on inverters that route it there during
// an export freeze (e.g. FoxESS "Feed-in First"), rather than clipping it, so only force the charge
// rate to zero when the inverter genuinely can't do that (#4207).
if (c->set_export_freeze) {
if (export_window_active && export_limit_now < 100.0 && (c->set_export_freeze && (export_limit_now == 99.0 || c->set_export_freeze_only))) {
if (export_window_active && export_limit_now < 100.0 && (c->set_export_freeze && (export_limit_now == 99.0 || c->set_export_freeze_only)) && !c->inverter_can_charge_during_export) {
charge_rate_now = battery_rate_min; // 0
}
}
Expand Down Expand Up @@ -747,8 +750,8 @@ int32_t pk_run(int64_t handle, const PkScenario *s, PkResult *out)
} else {
if (inverter_hybrid) {
double charge_rate_now_dc = battery_rate_max_charge_dc;
// Freeze mode - prediction.py:973-975
if (c->set_export_freeze && export_window_active && export_limit_now < 100.0 && (export_limit_now == 99.0 || c->set_export_freeze_only)) {
// Freeze mode - prediction.py:1013-1017, see the equivalent non-hybrid guard above (#4207)
if (c->set_export_freeze && export_window_active && export_limit_now < 100.0 && (export_limit_now == 99.0 || c->set_export_freeze_only) && !c->inverter_can_charge_during_export) {
charge_rate_now_dc = battery_rate_min; // 0
}
// Note: Python passes the un-rounded soc for the DC-rate lookup here
Expand Down
2 changes: 1 addition & 1 deletion apps/predbat/prediction_kernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@

# Expected ABI/parity revisions of the shared library (see prediction_kernel.cpp)
KERNEL_ABI_VERSION = 2
KERNEL_PARITY_REVISION = 2
KERNEL_PARITY_REVISION = 3

# Maximum number of cars supported by the kernel (PK_MAX_CARS in prediction_kernel.cpp)
KERNEL_MAX_CARS = 4
Expand Down
Binary file modified apps/predbat/prediction_kernel_lib_aarch64.so
Binary file not shown.
Binary file modified apps/predbat/prediction_kernel_lib_armv7l.so
Binary file not shown.
Binary file modified apps/predbat/prediction_kernel_lib_darwin_arm64.so
Binary file not shown.
Binary file modified apps/predbat/prediction_kernel_lib_darwin_x86_64.so
Binary file not shown.
Binary file modified apps/predbat/prediction_kernel_lib_i686.so
Binary file not shown.
Binary file modified apps/predbat/prediction_kernel_lib_x86_64.so
Binary file not shown.
18 changes: 15 additions & 3 deletions apps/predbat/tests/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -1153,9 +1153,21 @@ def run_model_tests(my_predbat, prediction_kernel=False):
inverter_loss=0.5,
assert_clipped=24 * 2,
)
failed |= simple_scenario("battery_discharge_freeze", my_predbat, 0, 0.5, assert_final_metric=-export_rate * 24 * 0.5, assert_final_soc=10, with_battery=True, discharge=99, battery_soc=10)
failed |= simple_scenario("battery_discharge_freeze2", my_predbat, 0, 0.5, assert_final_metric=-export_rate * 24 * 0.5, assert_final_soc=10, with_battery=True, discharge=99, battery_soc=10, set_export_freeze_only=True)
failed |= simple_scenario("battery_discharge_freeze_only", my_predbat, 0, 0.5, assert_final_metric=-export_rate * 24 * 0.5, assert_final_soc=10, with_battery=True, discharge=0, battery_soc=10, set_export_freeze_only=True)
# These freeze scenarios test an inverter that genuinely can't route PV surplus to the battery during
# an export freeze, hence inverter_can_charge_during_export=False - see battery_discharge_freeze_pv_charge
# below for the default (True) case, where surplus PV charges the battery instead of being clipped (#4207).
failed |= simple_scenario("battery_discharge_freeze", my_predbat, 0, 0.5, assert_final_metric=-export_rate * 24 * 0.5, assert_final_soc=10, with_battery=True, discharge=99, battery_soc=10, inverter_can_charge_during_export=False)
failed |= simple_scenario(
"battery_discharge_freeze2", my_predbat, 0, 0.5, assert_final_metric=-export_rate * 24 * 0.5, assert_final_soc=10, with_battery=True, discharge=99, battery_soc=10, set_export_freeze_only=True, inverter_can_charge_during_export=False
)
failed |= simple_scenario(
"battery_discharge_freeze_only", my_predbat, 0, 0.5, assert_final_metric=-export_rate * 24 * 0.5, assert_final_soc=10, with_battery=True, discharge=0, battery_soc=10, set_export_freeze_only=True, inverter_can_charge_during_export=False
)
# #4207: with the default inverter_can_charge_during_export=True, PV surplus beyond load during a freeze
# charges the battery (e.g. FoxESS "Feed-in First") rather than being clipped - SoC should rise by the
# full PV amount over the 24h window instead of staying flat at battery_soc.
failed |= simple_scenario("battery_discharge_freeze_pv_charge", my_predbat, 0, 0.5, assert_final_metric=0, assert_final_soc=10 + 24 * 0.5, with_battery=True, discharge=99, battery_soc=10)
failed |= simple_scenario("battery_discharge_freeze_pv_charge_hybrid", my_predbat, 0, 0.5, assert_final_metric=0, assert_final_soc=10 + 24 * 0.5, with_battery=True, discharge=99, battery_soc=10, hybrid=True)

# Force discharge with PV: penalty = discharge_hours * pv_kw * export_rate = 24 * 0.5 * export_rate (full 24h forecast window in these model tests)
failed |= simple_scenario(
Expand Down
Loading