From 6665ac1ad6ce29da940dcdc764fdf4552a22e095 Mon Sep 17 00:00:00 2001 From: Sandbox6168 <2820492+Sandbox6168@users.noreply.github.com> Date: Sat, 22 Aug 2026 22:21:32 +0100 Subject: [PATCH] fix(compare): fix compare tariffs using stale iBoost plan from live tariff iboost_plan (the low-rate slots iBoost may run in) was only ever computed once against the live tariff's rates. Compare scenarios swapped in each tariff's own rates but reused that stale plan, so iBoost timing in a compare-plan reflected the current live tariff instead of the one being compared. Add recompute_iboost(), matching the existing recompute_car_charging pattern, and call it per tariff in run_single(). Save/restore iboost_plan in run_all() alongside iboost_today. Co-Authored-By: Claude Sonnet 5 --- apps/predbat/compare.py | 14 +++++ apps/predbat/tests/test_compare.py | 93 ++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+) diff --git a/apps/predbat/compare.py b/apps/predbat/compare.py index 141fad9fa..48f4de16b 100644 --- a/apps/predbat/compare.py +++ b/apps/predbat/compare.py @@ -336,6 +336,7 @@ def run_single(self, tariff, rate_import_base, rate_export_base, end_record, deb my_predbat.manual_all_times = [] my_predbat.octopus_intelligent_charging = False + self.recompute_iboost() self.recompute_car_charging(car_charging_slots) self.log("Running scenario for tariff: {}".format(name)) @@ -448,6 +449,17 @@ def publish_only(self): self.select_best(compare_list, self.comparisons) self.publish_data() + def recompute_iboost(self): + """ + Recompute iBoost plan + """ + my_predbat = self.pb + + if my_predbat.iboost_enable and (((not my_predbat.iboost_solar) and (not my_predbat.iboost_charging)) or my_predbat.iboost_smart): + my_predbat.iboost_plan = my_predbat.plan_iboost_smart() + else: + my_predbat.iboost_plan = [] + def recompute_car_charging(self, car_charging_slots): """ Recompute car charging plan @@ -506,6 +518,7 @@ def run_all(self, debug=False, fetch_sensor=True): save_cost_today_sofar = my_predbat.cost_today_sofar save_carbon_today_sofar = my_predbat.carbon_today_sofar save_iboost_today = my_predbat.iboost_today + save_iboost_plan = my_predbat.iboost_plan save_import_today_now = my_predbat.import_today_now save_export_today_now = my_predbat.export_today_now save_octopus_intelligent_charging = my_predbat.octopus_intelligent_charging @@ -607,6 +620,7 @@ def run_all(self, debug=False, fetch_sensor=True): my_predbat.cost_today_sofar = save_cost_today_sofar my_predbat.carbon_today_sofar = save_carbon_today_sofar my_predbat.iboost_today = save_iboost_today + my_predbat.iboost_plan = save_iboost_plan my_predbat.import_today_now = save_import_today_now my_predbat.export_today_now = save_export_today_now my_predbat.octopus_intelligent_charging = save_octopus_intelligent_charging diff --git a/apps/predbat/tests/test_compare.py b/apps/predbat/tests/test_compare.py index 80c78b11e..ff94174e2 100644 --- a/apps/predbat/tests/test_compare.py +++ b/apps/predbat/tests/test_compare.py @@ -369,6 +369,99 @@ def _mock_fetch_config_options(): else: print("PASS T12: run_all() restores config: overrides inside the per-tariff loop, not just once at the end") + # ------------------------------------------------------------------ + # T13: run_single() re-plans iBoost against the tariff's own rates + # instead of leaving it stuck with the plan computed for the live + # tariff (the compare-plan iBoost timing bug) + # ------------------------------------------------------------------ + cmp, pb = _make_compare() + pb.iboost_enable = True + pb.iboost_solar = False + pb.iboost_charging = False + pb.iboost_smart = True + pb.iboost_today = 3.0 + pb.import_today_now = 0 + pb.export_today_now = 0 + pb.cost_today_sofar = 0 + pb.carbon_today_sofar = 0 + pb.forecast_plan_hours = 48 + pb.manual_charge_times = None + pb.manual_export_times = None + pb.manual_freeze_charge_times = None + pb.manual_freeze_export_times = None + pb.manual_demand_times = None + pb.manual_all_times = None + pb.octopus_intelligent_charging = False + pb.iboost_plan = ["stale_plan_from_live_tariff"] + + tariff_plan_calls = [] + + def _mock_plan_iboost_smart(): + tariff_plan_calls.append(1) + return ["fresh_plan_for_this_tariff"] + + pb.plan_iboost_smart = _mock_plan_iboost_smart + + cmp.fetch_config = lambda tariff: None + cmp.apply_hardware_overrides = lambda tariff, pb: None + cmp.fetch_rates = lambda tariff, base_import, base_export: "existing" + cmp.recompute_car_charging = lambda slots: None + cmp.run_scenario = lambda end_record: {"cost": 0, "metric": 0} + + result = cmp.run_single({"name": "test_tariff", "id": "test"}, {}, {}, 48 * 60, fetch_sensor=False) + + if not tariff_plan_calls: + print("ERROR T13: plan_iboost_smart() was not called during run_single()") + failed += 1 + elif pb.iboost_plan != ["fresh_plan_for_this_tariff"]: + print("ERROR T13: iboost_plan should be replaced with the tariff-specific plan, got {}".format(pb.iboost_plan)) + failed += 1 + else: + print("PASS T13: run_single() re-plans iBoost using the compared tariff's own rates") + + # ------------------------------------------------------------------ + # T14: run_single() clears iboost_plan (rather than reusing a stale one) + # when iBoost smart-rate planning isn't applicable for this tariff run + # ------------------------------------------------------------------ + cmp, pb = _make_compare() + pb.iboost_enable = True + pb.iboost_solar = True + pb.iboost_charging = True + pb.iboost_smart = False + pb.iboost_today = 0 + pb.import_today_now = 0 + pb.export_today_now = 0 + pb.cost_today_sofar = 0 + pb.carbon_today_sofar = 0 + pb.forecast_plan_hours = 48 + pb.manual_charge_times = None + pb.manual_export_times = None + pb.manual_freeze_charge_times = None + pb.manual_freeze_export_times = None + pb.manual_demand_times = None + pb.manual_all_times = None + pb.octopus_intelligent_charging = False + pb.iboost_plan = ["stale_plan_from_live_tariff"] + pb.plan_iboost_smart = lambda: tariff_plan_calls.append(1) or [] + + cmp.fetch_config = lambda tariff: None + cmp.apply_hardware_overrides = lambda tariff, pb: None + cmp.fetch_rates = lambda tariff, base_import, base_export: "existing" + cmp.recompute_car_charging = lambda slots: None + cmp.run_scenario = lambda end_record: {"cost": 0, "metric": 0} + + tariff_plan_calls.clear() + cmp.run_single({"name": "test_tariff2", "id": "test2"}, {}, {}, 48 * 60, fetch_sensor=False) + + if tariff_plan_calls: + print("ERROR T14: plan_iboost_smart() should not be called when iboost_charging/iboost_solar handle boosting and iboost_smart is off") + failed += 1 + elif pb.iboost_plan != []: + print("ERROR T14: stale iboost_plan from the live tariff should be cleared, got {}".format(pb.iboost_plan)) + failed += 1 + else: + print("PASS T14: run_single() clears stale iboost_plan when smart-rate planning isn't applicable") + if failed: print("**** compare tests FAILED: {} errors ****\n".format(failed)) else: