From ad4fb36645de989a64372f2dc8b1836bdf3de048 Mon Sep 17 00:00:00 2001 From: nickgee31 Date: Fri, 12 Jun 2026 12:07:20 +0100 Subject: [PATCH 1/5] Prune export windows below rate threshold Add threshold-based pruning and trimming for optimiser-selected export windows. Plan: introduce export_threshold_for_window, export_window_is_manual, export_window_above_threshold, trim_export_window_to_threshold and prune_export_windows_below_threshold; call pruning during plan filtering and recompute. Execute: respect export_window_above_threshold to avoid starting export (or scheduling freeze-only) when window rates are below the final threshold, log decisions and disable forced export. Tests: add unit tests and setup changes covering pruning, trimming of low-rate edges, preservation of manual overrides, and execute behavior when below threshold. --- apps/predbat/execute.py | 10 +- apps/predbat/plan.py | 122 ++++++++++++++++++ .../tests/test_discard_unused_export_slots.py | 108 ++++++++++++++++ apps/predbat/tests/test_execute.py | 18 +++ 4 files changed, 256 insertions(+), 2 deletions(-) diff --git a/apps/predbat/execute.py b/apps/predbat/execute.py index a20b9a8ca..02c8c5ae6 100644 --- a/apps/predbat/execute.py +++ b/apps/predbat/execute.py @@ -345,9 +345,15 @@ def execute_plan(self): discharge_start_time = self.midnight_utc + timedelta(minutes=minutes_start) discharge_end_time = self.midnight_utc + timedelta(minutes=(minutes_end + export_adjust)) # Add in 1 minute margin to allow Predbat to restore demand mode discharge_soc = max((int(self.export_limits_best[0]) * self.soc_max) / 100.0, self.reserve, self.best_soc_min) + export_window_above_threshold = self.export_window_above_threshold(window, self.export_limits_best[0]) self.log("Next export window will be: {} - {} at reserve {}".format(discharge_start_time, discharge_end_time, self.export_limits_best[0])) if (self.minutes_now >= minutes_start) and (self.minutes_now < minutes_end) and (self.export_limits_best[0] < 100.0): - if not self.set_export_freeze_only and self.export_limits_best[0] < 99.0 and (self.soc_kw > discharge_soc): + if not export_window_above_threshold: + export_rate = window.get("average", self.rate_export.get(window["start"], 0)) + self.log("Not exporting now as export rate {}{} is below optimisation threshold {}{}".format(dp2(export_rate), self.currency_symbols[1], dp2(self.export_threshold_for_window()), self.currency_symbols[1])) + inverter.adjust_force_export(False) + disabled_export = True + elif not self.set_export_freeze_only and self.export_limits_best[0] < 99.0 and (self.soc_kw > discharge_soc): if self.set_export_low_power: export_rate_adjust = 1 - (self.export_limits_best[0] - int(self.export_limits_best[0])) else: @@ -399,7 +405,7 @@ def execute_plan(self): self.isExporting_Target = inverter.soc_percent self.log("Export Hold (Demand mode) as export is now at/below target or freeze only is set - current SoC {}kWh and target {}kWh".format(self.soc_kw, discharge_soc)) else: - if (self.minutes_now < minutes_end) and ((minutes_start - self.minutes_now) <= self.set_window_minutes) and (self.export_limits_best[0] < 99.0): + if (self.minutes_now < minutes_end) and ((minutes_start - self.minutes_now) <= self.set_window_minutes) and (self.export_limits_best[0] < 99.0) and export_window_above_threshold: # We can't schedule freeze export only full export # Don't turn off ECO mode for GE inverters except when we are within the export window as it will stop the battery being used ge_inverters = inverter.inv_has_ge_eco_toggle or inverter.inv_has_ge_inverter_mode diff --git a/apps/predbat/plan.py b/apps/predbat/plan.py index 2ef436436..39524a046 100644 --- a/apps/predbat/plan.py +++ b/apps/predbat/plan.py @@ -1059,6 +1059,7 @@ def calculate_plan(self, recompute=True, debug_mode=False, publish=True): # Filter out the windows we disabled during clipping self.export_limits_best, self.export_window_best = self.discard_unused_export_slots(self.export_limits_best, self.export_window_best) + self.export_limits_best, self.export_window_best = self.prune_export_windows_below_threshold(self.export_limits_best, self.export_window_best) self.log("Export windows filtered {}".format(self.window_as_text(self.export_window_best, self.export_limits_best))) # Filter out any unused charge slots @@ -1123,6 +1124,9 @@ def calculate_plan(self, recompute=True, debug_mode=False, publish=True): else: self.log("New plan metric is significantly better from previous plan, using new plan") + if self.calculate_best_export and self.export_window_best: + self.export_limits_best, self.export_window_best = self.prune_export_windows_below_threshold(self.export_limits_best, self.export_window_best) + # Plan is now valid self.log("Plan valid is now true after recompute was {}".format(self.plan_valid)) if not self.update_pending: @@ -2283,6 +2287,124 @@ def discard_unused_export_slots(self, export_limits_best, export_window_best): return new_enable, new_best + def export_threshold_for_window(self): + """ + Return the final export threshold used to decide if optimiser-selected + export windows are still eligible. + """ + if self.rate_best_cost_threshold_export is not None: + return self.rate_best_cost_threshold_export + if self.rate_export_cost_threshold < 99: + return self.rate_export_cost_threshold + return None + + def export_window_is_manual(self, window): + """ + Return true when the export window is an explicit manual export/freeze override. + """ + window_start = window["start"] + window_start_orig = window.get("start_orig", window_start) + return ( + (window_start in self.manual_export_times) + or (window_start_orig in self.manual_export_times) + or (window_start in self.manual_freeze_export_times) + or (window_start_orig in self.manual_freeze_export_times) + ) + + def export_window_above_threshold(self, window, export_limit): + """ + Check if an export window is allowed by the final export rate threshold. + """ + if export_limit >= 100.0: + return True + if self.export_window_is_manual(window): + return True + + threshold = self.export_threshold_for_window() + if threshold is None: + return True + + export_rate = window.get("average", self.rate_export.get(window["start"], 0)) + return export_rate >= threshold + + def trim_export_window_to_threshold(self, window, threshold): + """ + Trim below-threshold rate periods from the start/end of an export window. + """ + new_window = copy.deepcopy(window) + window_start = new_window["start"] + window_end = new_window["end"] + rate_minutes = list(range(window_start, window_end, PREDICT_STEP)) + has_rate_data = any(minute in self.rate_export for minute in rate_minutes) + + if not has_rate_data: + export_rate = new_window.get("average", self.rate_export.get(window_start, 0)) + return new_window if export_rate >= threshold else None + + while window_start < window_end and self.rate_export.get(window_start, 0) < threshold: + window_start += PREDICT_STEP + + while window_end > window_start and self.rate_export.get(window_end - PREDICT_STEP, 0) < threshold: + window_end -= PREDICT_STEP + + if window_start >= window_end: + return None + + if (window_start != new_window["start"]) or (window_end != new_window["end"]): + self.log( + "Trim export window {} - {} to {} - {} using optimisation threshold {}{}".format( + self.time_abs_str(new_window["start"]), + self.time_abs_str(new_window["end"]), + self.time_abs_str(window_start), + self.time_abs_str(window_end), + dp2(threshold), + self.currency_symbols[1], + ) + ) + + new_window["start"] = window_start + new_window["end"] = window_end + rate_values = [self.rate_export[minute] for minute in range(window_start, window_end, PREDICT_STEP) if minute in self.rate_export] + if rate_values: + new_window["average"] = dp2(sum(rate_values) / len(rate_values)) + return new_window + + def prune_export_windows_below_threshold(self, export_limits_best, export_window_best): + """ + Remove optimiser-selected export windows below the final export threshold. + """ + new_best = [] + new_enable = [] + threshold = self.export_threshold_for_window() + + for window_n in range(len(export_window_best)): + window = export_window_best[window_n] + export_limit = export_limits_best[window_n] + if threshold is not None and (not self.export_window_is_manual(window)) and export_limit < 100.0: + window = self.trim_export_window_to_threshold(window, threshold) + + if (window is None) or (not self.export_window_above_threshold(window, export_limit)): + original_window = export_window_best[window_n] + if window is None: + export_rate = original_window.get("average", self.rate_export.get(original_window["start"], 0)) + else: + export_rate = window.get("average", self.rate_export.get(window["start"], 0)) + self.log( + "Discard export window {} - {} at {}{} below optimisation threshold {}{}".format( + self.time_abs_str(original_window["start"]), + self.time_abs_str(original_window["end"]), + dp2(export_rate), + self.currency_symbols[1], + dp2(threshold), + self.currency_symbols[1], + ) + ) + continue + new_best.append(window) + new_enable.append(export_limit) + + return new_enable, new_best + def update_target_values(self): """ Update target values for HTML plan diff --git a/apps/predbat/tests/test_discard_unused_export_slots.py b/apps/predbat/tests/test_discard_unused_export_slots.py index cc45c3cc8..744c04231 100644 --- a/apps/predbat/tests/test_discard_unused_export_slots.py +++ b/apps/predbat/tests/test_discard_unused_export_slots.py @@ -25,6 +25,10 @@ def run_discard_unused_export_slots_tests(my_predbat): failed |= test_mixed_slots(my_predbat) failed |= test_all_disabled(my_predbat) failed |= test_freeze_export_kept(my_predbat) + failed |= test_prune_export_below_threshold(my_predbat) + failed |= test_prune_export_trims_low_rate_edge(my_predbat) + failed |= test_prune_freeze_export_below_threshold(my_predbat) + failed |= test_prune_manual_export_below_threshold_kept(my_predbat) return failed @@ -39,6 +43,10 @@ def setup(my_predbat): reset_inverter(my_predbat) my_predbat.debug_enable = False my_predbat.manual_all_times = [] + my_predbat.manual_export_times = [] + my_predbat.manual_freeze_export_times = [] + my_predbat.rate_best_cost_threshold_export = None + my_predbat.rate_export_cost_threshold = 99 def test_discard_disabled(my_predbat): @@ -271,3 +279,103 @@ def test_freeze_export_kept(my_predbat): if not failed: print("PASS") return failed + + +def test_prune_export_below_threshold(my_predbat): + """Optimiser-selected export slots below the final export threshold should be removed""" + print("**** test_prune_export_below_threshold ****") + failed = False + setup(my_predbat) + my_predbat.rate_best_cost_threshold_export = 10.0 + + windows = [make_window(720, 750, average=8.0), make_window(780, 810, average=12.0)] + limits = [50.0, 40.0] + + result_limits, result_windows = my_predbat.prune_export_windows_below_threshold(limits, windows) + + if result_limits != [40.0]: + print("ERROR: Expected only the above-threshold limit [40.0] but got {}".format(result_limits)) + failed = True + elif len(result_windows) != 1 or result_windows[0]["start"] != 780: + print("ERROR: Expected only the above-threshold window to remain but got {}".format(result_windows)) + failed = True + + if not failed: + print("PASS") + return failed + + +def test_prune_export_trims_low_rate_edge(my_predbat): + """Below-threshold edge periods should be trimmed from optimiser export windows""" + print("**** test_prune_export_trims_low_rate_edge ****") + failed = False + setup(my_predbat) + my_predbat.rate_best_cost_threshold_export = 8.16 + my_predbat.rate_export = {955: 4.74} + for minute in range(960, 1140, 5): + my_predbat.rate_export[minute] = 10.75 + + windows = [make_window(955, 1140, average=10.75)] + limits = [17.0] + + result_limits, result_windows = my_predbat.prune_export_windows_below_threshold(limits, windows) + + if result_limits != [17.0]: + print("ERROR: Expected trimmed export limit [17.0] but got {}".format(result_limits)) + failed = True + elif len(result_windows) != 1 or result_windows[0]["start"] != 960 or result_windows[0]["end"] != 1140: + print("ERROR: Expected window to be trimmed to 960-1140 but got {}".format(result_windows)) + failed = True + + if not failed: + print("PASS") + return failed + + +def test_prune_freeze_export_below_threshold(my_predbat): + """Optimiser freeze export slots below the final export threshold should be removed""" + print("**** test_prune_freeze_export_below_threshold ****") + failed = False + setup(my_predbat) + my_predbat.rate_best_cost_threshold_export = 10.0 + + windows = [make_window(720, 750, average=8.0)] + limits = [99.0] + + result_limits, result_windows = my_predbat.prune_export_windows_below_threshold(limits, windows) + + if result_limits: + print("ERROR: Expected below-threshold freeze export to be removed but got {}".format(result_limits)) + failed = True + elif result_windows: + print("ERROR: Expected no windows but got {}".format(result_windows)) + failed = True + + if not failed: + print("PASS") + return failed + + +def test_prune_manual_export_below_threshold_kept(my_predbat): + """Manual export overrides should remain even when below the optimiser threshold""" + print("**** test_prune_manual_export_below_threshold_kept ****") + failed = False + setup(my_predbat) + my_predbat.rate_best_cost_threshold_export = 10.0 + my_predbat.manual_export_times = [720] + + windows = [make_window(720, 750, average=8.0)] + limits = [50.0] + + result_limits, result_windows = my_predbat.prune_export_windows_below_threshold(limits, windows) + + if result_limits != [50.0]: + print("ERROR: Expected manual below-threshold export to be kept but got {}".format(result_limits)) + failed = True + elif len(result_windows) != 1 or result_windows[0]["start"] != 720: + print("ERROR: Expected manual window to remain but got {}".format(result_windows)) + failed = True + + if not failed: + print("PASS") + return failed diff --git a/apps/predbat/tests/test_execute.py b/apps/predbat/tests/test_execute.py index b7b361116..c55ea3eb0 100644 --- a/apps/predbat/tests/test_execute.py +++ b/apps/predbat/tests/test_execute.py @@ -1847,6 +1847,24 @@ def run_execute_tests(my_predbat): failed |= run_execute_test(my_predbat, "no_discharge", export_window_best=export_window_best, export_limits_best=export_limits_best, assert_reserve=-1) failed |= run_execute_test(my_predbat, "no_discharge2", export_window_best=export_window_best, export_limits_best=export_limits_best, set_charge_window=True, set_export_window=True, soc_kw=0, assert_status="Hold exporting") + my_predbat.rate_best_cost_threshold_export = 5.0 + failed |= run_execute_test(my_predbat, "no_discharge_below_export_threshold", export_window_best=export_window_best, export_limits_best=export_limits_best, set_charge_window=True, set_export_window=True, soc_kw=5, assert_force_export=False, assert_status="Demand") + my_predbat.manual_export_times = [my_predbat.minutes_now] + failed |= run_execute_test( + my_predbat, + "manual_discharge_below_export_threshold", + export_window_best=export_window_best, + export_limits_best=export_limits_best, + set_charge_window=True, + set_export_window=True, + soc_kw=5, + assert_force_export=True, + assert_status="Exporting", + assert_immediate_soc_target=0, + assert_discharge_end_time_minutes=my_predbat.minutes_now + 60 + 1, + ) + my_predbat.manual_export_times = [] + my_predbat.rate_best_cost_threshold_export = None failed |= run_execute_test( my_predbat, "discharge_upcoming1", From 92d131e3bb195309ae0598d085ddca81cac0d233 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Fri, 12 Jun 2026 13:05:23 +0000 Subject: [PATCH 2/5] [pre-commit.ci lite] apply automatic fixes --- apps/predbat/plan.py | 7 +------ apps/predbat/tests/test_execute.py | 4 +++- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/apps/predbat/plan.py b/apps/predbat/plan.py index 39524a046..7e1cf5d58 100644 --- a/apps/predbat/plan.py +++ b/apps/predbat/plan.py @@ -2304,12 +2304,7 @@ def export_window_is_manual(self, window): """ window_start = window["start"] window_start_orig = window.get("start_orig", window_start) - return ( - (window_start in self.manual_export_times) - or (window_start_orig in self.manual_export_times) - or (window_start in self.manual_freeze_export_times) - or (window_start_orig in self.manual_freeze_export_times) - ) + return (window_start in self.manual_export_times) or (window_start_orig in self.manual_export_times) or (window_start in self.manual_freeze_export_times) or (window_start_orig in self.manual_freeze_export_times) def export_window_above_threshold(self, window, export_limit): """ diff --git a/apps/predbat/tests/test_execute.py b/apps/predbat/tests/test_execute.py index c55ea3eb0..c689944cf 100644 --- a/apps/predbat/tests/test_execute.py +++ b/apps/predbat/tests/test_execute.py @@ -1848,7 +1848,9 @@ def run_execute_tests(my_predbat): failed |= run_execute_test(my_predbat, "no_discharge", export_window_best=export_window_best, export_limits_best=export_limits_best, assert_reserve=-1) failed |= run_execute_test(my_predbat, "no_discharge2", export_window_best=export_window_best, export_limits_best=export_limits_best, set_charge_window=True, set_export_window=True, soc_kw=0, assert_status="Hold exporting") my_predbat.rate_best_cost_threshold_export = 5.0 - failed |= run_execute_test(my_predbat, "no_discharge_below_export_threshold", export_window_best=export_window_best, export_limits_best=export_limits_best, set_charge_window=True, set_export_window=True, soc_kw=5, assert_force_export=False, assert_status="Demand") + failed |= run_execute_test( + my_predbat, "no_discharge_below_export_threshold", export_window_best=export_window_best, export_limits_best=export_limits_best, set_charge_window=True, set_export_window=True, soc_kw=5, assert_force_export=False, assert_status="Demand" + ) my_predbat.manual_export_times = [my_predbat.minutes_now] failed |= run_execute_test( my_predbat, From 3d878b0b9fe83a21f8bdec706400fc90d52df9df Mon Sep 17 00:00:00 2001 From: nickgee31 Date: Fri, 12 Jun 2026 14:10:19 +0100 Subject: [PATCH 3/5] Initialize rate_export in test setup Set my_predbat.rate_export = {} in the test fixture to ensure the attribute exists for tests. This prevents errors when tests or code expect a dict for rate_export and makes the setup explicit about the default test state. --- apps/predbat/tests/test_discard_unused_export_slots.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/predbat/tests/test_discard_unused_export_slots.py b/apps/predbat/tests/test_discard_unused_export_slots.py index 744c04231..084bdd961 100644 --- a/apps/predbat/tests/test_discard_unused_export_slots.py +++ b/apps/predbat/tests/test_discard_unused_export_slots.py @@ -47,6 +47,7 @@ def setup(my_predbat): my_predbat.manual_freeze_export_times = [] my_predbat.rate_best_cost_threshold_export = None my_predbat.rate_export_cost_threshold = 99 + my_predbat.rate_export = {} def test_discard_disabled(my_predbat): From 409794220bb077d5e035833fbd32f17d11b48e9d Mon Sep 17 00:00:00 2001 From: nickgee31 Date: Fri, 12 Jun 2026 16:21:32 +0100 Subject: [PATCH 4/5] Use original window size for min improvement Compute original_window_size from the window's original start and use it when scaling metric_min_improvement_export. Previously the scaled minimum improvement used window_size (based on adjusted start), which could mis-scale the threshold; this change preserves the original window duration so min_improvement_scaled is calculated correctly relative to plan_interval_minutes. --- apps/predbat/plan.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/predbat/plan.py b/apps/predbat/plan.py index 7e1cf5d58..9f782f378 100644 --- a/apps/predbat/plan.py +++ b/apps/predbat/plan.py @@ -1755,6 +1755,7 @@ def optimise_export(self, window_n, record_charge_windows, try_charge_limit, cha ) window_size = try_export_window[window_n]["end"] - start + original_window_size = try_export_window[window_n]["end"] - window["start"] window_key = str(dp2(this_export_limit)) + "_" + str(window_size) window_results[window_key] = [metric, cost] @@ -1767,7 +1768,7 @@ def optimise_export(self, window_n, record_charge_windows, try_charge_limit, cha elif all_n: min_improvement_scaled = self.metric_min_improvement_export * rate_scale * len(all_n) else: - min_improvement_scaled = self.metric_min_improvement_export * window_size * rate_scale / float(self.plan_interval_minutes) + min_improvement_scaled = self.metric_min_improvement_export * original_window_size * rate_scale / float(self.plan_interval_minutes) # Only select an export if it makes a notable improvement has defined by min_improvement (divided in M windows) # Also require cost improvement to prevent exports that only game metric_keep without actual savings (issue #2984) From 72606b82463155063d8d4302f83bb8ececd49ae4 Mon Sep 17 00:00:00 2001 From: nickgee31 Date: Fri, 12 Jun 2026 16:29:14 +0100 Subject: [PATCH 5/5] Restore export windows trimmed above threshold Add logic to undo optimiser tail-trimming for export windows when the skipped original start period remains above the final export threshold. Hook restore_export_windows_above_threshold into the full optimisation flow (guarded by calculate_best_export and export_window_best). The new method checks the export threshold, skips fully-exported windows, inspects per-minute rate_export (using PREDICT_STEP) or window average, logs the restoration, and resets the window start to start_orig when appropriate. Also add unit tests verifying restoration when skipped rates are above threshold and non-restoration when there are below-threshold rates. --- apps/predbat/plan.py | 40 +++++++++++++ .../tests/test_discard_unused_export_slots.py | 60 +++++++++++++++++++ 2 files changed, 100 insertions(+) diff --git a/apps/predbat/plan.py b/apps/predbat/plan.py index 9f782f378..ca6e80034 100644 --- a/apps/predbat/plan.py +++ b/apps/predbat/plan.py @@ -1015,6 +1015,8 @@ def calculate_plan(self, recompute=True, debug_mode=False, publish=True): # Full plan self.optimise_all_windows(metric, metric_keep, debug_mode) + if self.calculate_best_export and self.export_window_best: + self.restore_export_windows_above_threshold() # Update target values, will be refined via clipping self.update_target_values() @@ -2365,6 +2367,44 @@ def trim_export_window_to_threshold(self, window, threshold): new_window["average"] = dp2(sum(rate_values) / len(rate_values)) return new_window + def restore_export_windows_above_threshold(self): + """ + Undo optimiser tail-trimming for selected export windows when the skipped + start of the original window is still above the final export threshold. + """ + threshold = self.export_threshold_for_window() + if threshold is None: + return + + for window_n in range(len(self.export_window_best)): + window = self.export_window_best[window_n] + if self.export_limits_best[window_n] >= 100.0: + continue + + window_start = window["start"] + window_start_orig = window.get("start_orig", window_start) + if window_start_orig >= window_start: + continue + + rate_minutes = list(range(window_start_orig, window_start, PREDICT_STEP)) + has_rate_data = any(minute in self.rate_export for minute in rate_minutes) + if has_rate_data: + skipped_above_threshold = all(self.rate_export.get(minute, 0) >= threshold for minute in rate_minutes) + else: + skipped_above_threshold = window.get("average", self.rate_export.get(window_start_orig, 0)) >= threshold + + if skipped_above_threshold: + self.log( + "Restore export window start {} - {} to {} using optimisation threshold {}{}".format( + self.time_abs_str(window_start), + self.time_abs_str(window["end"]), + self.time_abs_str(window_start_orig), + dp2(threshold), + self.currency_symbols[1], + ) + ) + window["start"] = window_start_orig + def prune_export_windows_below_threshold(self, export_limits_best, export_window_best): """ Remove optimiser-selected export windows below the final export threshold. diff --git a/apps/predbat/tests/test_discard_unused_export_slots.py b/apps/predbat/tests/test_discard_unused_export_slots.py index 084bdd961..e2b04fe1b 100644 --- a/apps/predbat/tests/test_discard_unused_export_slots.py +++ b/apps/predbat/tests/test_discard_unused_export_slots.py @@ -29,6 +29,8 @@ def run_discard_unused_export_slots_tests(my_predbat): failed |= test_prune_export_trims_low_rate_edge(my_predbat) failed |= test_prune_freeze_export_below_threshold(my_predbat) failed |= test_prune_manual_export_below_threshold_kept(my_predbat) + failed |= test_restore_export_window_above_threshold(my_predbat) + failed |= test_restore_export_window_below_threshold_kept_trimmed(my_predbat) return failed @@ -380,3 +382,61 @@ def test_prune_manual_export_below_threshold_kept(my_predbat): if not failed: print("PASS") return failed + + +def test_restore_export_window_above_threshold(my_predbat): + """Tail-trimmed optimiser exports are restored when skipped rates are above threshold""" + print("**** test_restore_export_window_above_threshold ****") + failed = False + setup(my_predbat) + my_predbat.rate_best_cost_threshold_export = 10.0 + my_predbat.rate_export = { + 720: 12.0, + 725: 12.0, + 730: 12.0, + 735: 12.0, + 740: 12.0, + 745: 12.0, + } + my_predbat.export_window_best = [make_window(740, 750, average=12.0)] + my_predbat.export_window_best[0]["start_orig"] = 720 + my_predbat.export_limits_best = [50.0] + + my_predbat.restore_export_windows_above_threshold() + + if my_predbat.export_window_best[0]["start"] != 720: + print("ERROR: Expected export window start to restore to 720 but got {}".format(my_predbat.export_window_best[0]["start"])) + failed = True + + if not failed: + print("PASS") + return failed + + +def test_restore_export_window_below_threshold_kept_trimmed(my_predbat): + """Tail-trimmed optimiser exports are not restored across below-threshold rates""" + print("**** test_restore_export_window_below_threshold_kept_trimmed ****") + failed = False + setup(my_predbat) + my_predbat.rate_best_cost_threshold_export = 10.0 + my_predbat.rate_export = { + 720: 8.0, + 725: 8.0, + 730: 12.0, + 735: 12.0, + 740: 12.0, + 745: 12.0, + } + my_predbat.export_window_best = [make_window(730, 750, average=12.0)] + my_predbat.export_window_best[0]["start_orig"] = 720 + my_predbat.export_limits_best = [50.0] + + my_predbat.restore_export_windows_above_threshold() + + if my_predbat.export_window_best[0]["start"] != 730: + print("ERROR: Expected export window start to remain 730 but got {}".format(my_predbat.export_window_best[0]["start"])) + failed = True + + if not failed: + print("PASS") + return failed