From a444186f22cb67b6c265c3a9f5e23e230f1e915d Mon Sep 17 00:00:00 2001 From: Trefor Southwell <48591903+springfall2008@users.noreply.github.com> Date: Fri, 13 Dec 2024 08:18:22 +0000 Subject: [PATCH 1/4] Fixes related to export planning --- apps/predbat/plan.py | 4 ++-- apps/predbat/prediction.py | 30 +++++++++++++----------------- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/apps/predbat/plan.py b/apps/predbat/plan.py index 252410ce..50a5e3b6 100644 --- a/apps/predbat/plan.py +++ b/apps/predbat/plan.py @@ -1798,7 +1798,7 @@ def update_target_values(self): self.export_window_best[window_n]["target"] = self.export_limits_best[window_n] for window_n in range(len(self.charge_limit_best)): self.charge_window_best[window_n]["target"] = self.charge_limit_best[window_n] - + def tweak_plan(self, end_record, best_metric, metric_keep): """ Tweak existing plan only @@ -1866,7 +1866,7 @@ def optimise_all_windows(self, best_metric, metric_keep, debug_mode=False): record_charge_windows = max(self.max_charge_windows(self.end_record + self.minutes_now, self.charge_window_best), 1) record_export_windows = max(self.max_charge_windows(self.end_record + self.minutes_now, self.export_window_best), 1) window_sorted, window_index, price_set, price_links = self.sort_window_by_price_combined(self.charge_window_best[:record_charge_windows], self.export_window_best[:record_export_windows]) - + best_soc = self.soc_max best_cost = best_metric best_keep = metric_keep diff --git a/apps/predbat/prediction.py b/apps/predbat/prediction.py index 8184ed9f..4a1b4b65 100644 --- a/apps/predbat/prediction.py +++ b/apps/predbat/prediction.py @@ -586,13 +586,9 @@ def run_prediction(self, charge_limit, charge_window, export_window, export_limi if export_window_n >= 0: discharge_min = max(self.soc_max * export_limits[export_window_n] / 100.0, self.reserve, self.best_soc_min) - if not self.set_export_freeze_only and (export_window_n >= 0) and export_limits[export_window_n] < 100.0 and (soc - step * self.battery_rate_max_discharge_scaled) < discharge_min: - # Export runs out of battery - give penalty for this case, we might not stop it in time - metric_keep += step * self.battery_rate_max_discharge_scaled * rate_import.get(minute_absolute, 0) * 0.5 - - if not self.set_export_freeze_only and (export_window_n >= 0) and export_limits[export_window_n] < 100.0 and (soc - step * self.battery_rate_max_discharge_scaled) >= discharge_min: + if not self.set_export_freeze_only and (export_window_n >= 0) and export_limits[export_window_n] < 99.0 and (soc - step * self.battery_rate_max_discharge_scaled) >= discharge_min: # Discharge enable - discharge_rate_now = self.battery_rate_max_discharge # Assume discharge becomes enabled here + discharge_rate_now = self.battery_rate_max_discharge # Assume discharge becomes enabled here discharge_rate_now_curve = get_discharge_rate_curve(soc, discharge_rate_now, self.soc_max, self.battery_rate_max_discharge, self.battery_discharge_power_curve, self.battery_rate_min) * self.battery_rate_max_scaling_discharge battery_draw = min(discharge_rate_now_curve * step, battery_to_min) @@ -649,19 +645,19 @@ def run_prediction(self, charge_limit, charge_window, export_window, export_limi if save in ["best", "best10", "test"]: # Only tune charge rate on final plan not every simulation charge_rate_now = find_charge_rate( - minute_absolute, - soc, - charge_window[charge_window_n], - charge_limit_n, - self.battery_rate_max_charge, - self.soc_max, - self.battery_charge_power_curve, - self.set_charge_low_power, + minute_absolute, + soc, + charge_window[charge_window_n], + charge_limit_n, + self.battery_rate_max_charge, + self.soc_max, + self.battery_charge_power_curve, + self.set_charge_low_power, self.charge_low_power_margin, self.battery_rate_min, self.battery_rate_max_scaling, self.battery_loss, - None, + None ) else: charge_rate_now = self.battery_rate_max_charge # Assume charge becomes enabled here @@ -815,8 +811,8 @@ def run_prediction(self, charge_limit, charge_window, export_window, export_limi if soc < self.best_soc_keep and battery_draw > 0: # Apply keep as a percentage of the time in the future so it gets stronger over an 4 hour period # Weight to 50% chance of the scenario - # keep_diff = max(get_diff(0, 0, pv_now, load_yesterday, inverter_loss), battery_draw) - # if keep_diff > 0: + #keep_diff = max(get_diff(0, 0, pv_now, load_yesterday, inverter_loss), battery_draw) + #if keep_diff > 0: metric_keep += rate_import[minute_absolute] * battery_draw * keep_minute_scaling if diff > 0: # Import From c27a730407789e94d73fe865ea1651d3be83b843 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, 13 Dec 2024 08:20:43 +0000 Subject: [PATCH 2/4] [pre-commit.ci lite] apply automatic fixes --- apps/predbat/plan.py | 4 ++-- apps/predbat/prediction.py | 24 ++++++++++++------------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/apps/predbat/plan.py b/apps/predbat/plan.py index 50a5e3b6..252410ce 100644 --- a/apps/predbat/plan.py +++ b/apps/predbat/plan.py @@ -1798,7 +1798,7 @@ def update_target_values(self): self.export_window_best[window_n]["target"] = self.export_limits_best[window_n] for window_n in range(len(self.charge_limit_best)): self.charge_window_best[window_n]["target"] = self.charge_limit_best[window_n] - + def tweak_plan(self, end_record, best_metric, metric_keep): """ Tweak existing plan only @@ -1866,7 +1866,7 @@ def optimise_all_windows(self, best_metric, metric_keep, debug_mode=False): record_charge_windows = max(self.max_charge_windows(self.end_record + self.minutes_now, self.charge_window_best), 1) record_export_windows = max(self.max_charge_windows(self.end_record + self.minutes_now, self.export_window_best), 1) window_sorted, window_index, price_set, price_links = self.sort_window_by_price_combined(self.charge_window_best[:record_charge_windows], self.export_window_best[:record_export_windows]) - + best_soc = self.soc_max best_cost = best_metric best_keep = metric_keep diff --git a/apps/predbat/prediction.py b/apps/predbat/prediction.py index 4a1b4b65..54fad279 100644 --- a/apps/predbat/prediction.py +++ b/apps/predbat/prediction.py @@ -588,7 +588,7 @@ def run_prediction(self, charge_limit, charge_window, export_window, export_limi if not self.set_export_freeze_only and (export_window_n >= 0) and export_limits[export_window_n] < 99.0 and (soc - step * self.battery_rate_max_discharge_scaled) >= discharge_min: # Discharge enable - discharge_rate_now = self.battery_rate_max_discharge # Assume discharge becomes enabled here + discharge_rate_now = self.battery_rate_max_discharge # Assume discharge becomes enabled here discharge_rate_now_curve = get_discharge_rate_curve(soc, discharge_rate_now, self.soc_max, self.battery_rate_max_discharge, self.battery_discharge_power_curve, self.battery_rate_min) * self.battery_rate_max_scaling_discharge battery_draw = min(discharge_rate_now_curve * step, battery_to_min) @@ -645,19 +645,19 @@ def run_prediction(self, charge_limit, charge_window, export_window, export_limi if save in ["best", "best10", "test"]: # Only tune charge rate on final plan not every simulation charge_rate_now = find_charge_rate( - minute_absolute, - soc, - charge_window[charge_window_n], - charge_limit_n, - self.battery_rate_max_charge, - self.soc_max, - self.battery_charge_power_curve, - self.set_charge_low_power, + minute_absolute, + soc, + charge_window[charge_window_n], + charge_limit_n, + self.battery_rate_max_charge, + self.soc_max, + self.battery_charge_power_curve, + self.set_charge_low_power, self.charge_low_power_margin, self.battery_rate_min, self.battery_rate_max_scaling, self.battery_loss, - None + None, ) else: charge_rate_now = self.battery_rate_max_charge # Assume charge becomes enabled here @@ -811,8 +811,8 @@ def run_prediction(self, charge_limit, charge_window, export_window, export_limi if soc < self.best_soc_keep and battery_draw > 0: # Apply keep as a percentage of the time in the future so it gets stronger over an 4 hour period # Weight to 50% chance of the scenario - #keep_diff = max(get_diff(0, 0, pv_now, load_yesterday, inverter_loss), battery_draw) - #if keep_diff > 0: + # keep_diff = max(get_diff(0, 0, pv_now, load_yesterday, inverter_loss), battery_draw) + # if keep_diff > 0: metric_keep += rate_import[minute_absolute] * battery_draw * keep_minute_scaling if diff > 0: # Import From a9f71dcd750a88043c1b89ed534f6f40490cb2d2 Mon Sep 17 00:00:00 2001 From: Trefor Southwell <48591903+springfall2008@users.noreply.github.com> Date: Fri, 13 Dec 2024 08:43:41 +0000 Subject: [PATCH 3/4] Testing and version --- apps/predbat/predbat.py | 4 +-- apps/predbat/unit_test.py | 61 ++++++++++++++++++++------------------- 2 files changed, 33 insertions(+), 32 deletions(-) diff --git a/apps/predbat/predbat.py b/apps/predbat/predbat.py index 56e48ee7..17a105cd 100644 --- a/apps/predbat/predbat.py +++ b/apps/predbat/predbat.py @@ -24,11 +24,9 @@ try: import adbase as ad import appdaemon.plugins.hass.hassapi as hass - IS_APPDAEMON = True except: import hass as hass - IS_APPDAEMON = False import pytz @@ -38,7 +36,7 @@ import asyncio import json -THIS_VERSION = "v8.8.5" +THIS_VERSION = "v8.8.6" # fmt: off PREDBAT_FILES = ["predbat.py", "config.py", "prediction.py", "gecloud.py","utils.py", "inverter.py", "ha.py", "download.py", "unit_test.py", "web.py", "predheat.py", "futurerate.py", "octopus.py", "solcast.py","execute.py", "plan.py", "fetch.py", "output.py", "userinterface.py"] diff --git a/apps/predbat/unit_test.py b/apps/predbat/unit_test.py index aaa91a06..14eb2e03 100644 --- a/apps/predbat/unit_test.py +++ b/apps/predbat/unit_test.py @@ -907,8 +907,9 @@ def run_single_debug(my_predbat, debug_file): # Force off combine export XXX: print("Combined export slots {}".format(my_predbat.combine_export_slots)) - # my_predbat.combine_export_slots = False - my_predbat.best_soc_keep = 1.0 + #my_predbat.combine_export_slots = False + #my_predbat.best_soc_keep = 1.0 + my_predbat.metric_min_improvement_export = 5 if re_do_rates: # Find discharging windows @@ -952,13 +953,13 @@ def run_single_debug(my_predbat, debug_file): load_forecast=my_predbat.load_forecast, load_scaling_dynamic=my_predbat.load_scaling_dynamic, cloud_factor=min(my_predbat.metric_load_divergence + 0.5, 1.0) if my_predbat.metric_load_divergence else None, - ) + ) pv_step = my_predbat.pv_forecast_minute_step pv10_step = my_predbat.pv_forecast_minute10_step load_step = my_predbat.load_minutes_step load10_step = my_predbat.load_minutes_step10 - + my_predbat.prediction = Prediction(my_predbat, pv_step, pv_step, load_step, load_step) my_predbat.debug_enable = True @@ -1047,6 +1048,7 @@ def run_single_debug(my_predbat, debug_file): print("Wrote plan to plan_final.html") + def run_execute_tests(my_predbat): print("**** Running execute tests ****\n") reset_inverter(my_predbat) @@ -1297,22 +1299,22 @@ def run_execute_tests(my_predbat): return failed my_predbat.battery_charge_power_curve = { - 100: 0.50, - 99: 0.50, - 98: 0.50, - 97: 0.50, - 96: 0.50, - 95: 0.50, - 94: 1.00, - 93: 1.00, - 92: 1.00, - 91: 1.00, - 90: 1.00, - 89: 1.00, - 88: 1.00, - 87: 1.00, - 86: 1.00, - 85: 1.00, + 100 : 0.50, + 99 : 0.50, + 98 : 0.50, + 97 : 0.50, + 96 : 0.50, + 95 : 0.50, + 94 : 1.00, + 93 : 1.00, + 92 : 1.00, + 91 : 1.00, + 90 : 1.00, + 89 : 1.00, + 88 : 1.00, + 87 : 1.00, + 86 : 1.00, + 85 : 1.00, } # 60 minutes - 10 minute margin = 50 minutes to add 0.75kWh to each battery (x2 inverters) @@ -2436,7 +2438,7 @@ def run_optimise_all_windows( expect_export_limit=[], expect_best_price=0.0, rate_import=10.0, - rate_export=5.0, + rate_export=5.5, battery_size=100.0, battery_soc=0.0, hybrid=False, @@ -2939,10 +2941,10 @@ def run_model_tests(my_predbat): failed |= simple_scenario("load_only", my_predbat, 1, 0, assert_final_metric=import_rate * 24, assert_final_soc=0, with_battery=False) failed |= simple_scenario("load_bat_ac", my_predbat, 4, 0, assert_final_metric=import_rate * 24 * 3.2, assert_final_soc=100 - 24, with_battery=True, battery_soc=100.0, inverter_loss=0.8) failed |= simple_scenario("load_bat_dc", my_predbat, 4, 0, assert_final_metric=import_rate * 24 * 3.2, assert_final_soc=100 - 24, with_battery=True, battery_soc=100.0, inverter_loss=0.8, hybrid=True) - failed |= simple_scenario("load_bat_ac2", my_predbat, 0.5, 0, assert_final_metric=0, assert_final_soc=100 - 12 / 0.8, with_battery=True, battery_soc=100.0, inverter_loss=0.8) - failed |= simple_scenario("load_bat_dc2", my_predbat, 0.5, 0, assert_final_metric=0, assert_final_soc=100 - 12 / 0.8, with_battery=True, battery_soc=100.0, inverter_loss=0.8, hybrid=True) - failed |= simple_scenario("load_bat_ac3", my_predbat, 1.0, 0, assert_final_metric=import_rate * 0.2 * 24, assert_final_soc=100 - 24, with_battery=True, battery_soc=100.0, inverter_loss=0.8) - failed |= simple_scenario("load_bat_dc3", my_predbat, 1.0, 0, assert_final_metric=import_rate * 0.2 * 24, assert_final_soc=100 - 24, with_battery=True, battery_soc=100.0, inverter_loss=0.8, hybrid=True) + failed |= simple_scenario("load_bat_ac2", my_predbat, 0.5, 0, assert_final_metric=0, assert_final_soc=100 - 12/0.8, with_battery=True, battery_soc=100.0, inverter_loss=0.8) + failed |= simple_scenario("load_bat_dc2", my_predbat, 0.5, 0, assert_final_metric=0, assert_final_soc=100 - 12/0.8, with_battery=True, battery_soc=100.0, inverter_loss=0.8, hybrid=True) + failed |= simple_scenario("load_bat_ac3", my_predbat, 1.0, 0, assert_final_metric=import_rate * 0.2*24, assert_final_soc=100 - 24, with_battery=True, battery_soc=100.0, inverter_loss=0.8) + failed |= simple_scenario("load_bat_dc3", my_predbat, 1.0, 0, assert_final_metric=import_rate * 0.2*24, assert_final_soc=100 - 24, with_battery=True, battery_soc=100.0, inverter_loss=0.8, hybrid=True) failed |= simple_scenario( "load_bat_dc_pv", @@ -3384,7 +3386,7 @@ def run_model_tests(my_predbat): with_battery=True, discharge=0, battery_soc=10, - assert_keep=14 * import_rate * 0.5 * KEEP_SCALE + 1 * import_rate * KEEP_SCALE, + assert_keep=1 * import_rate * KEEP_SCALE, keep=1.0, ) failed |= simple_scenario( @@ -3397,7 +3399,7 @@ def run_model_tests(my_predbat): with_battery=True, discharge=0, battery_soc=10, - assert_keep=14 * import_rate * 0.5 * KEEP_SCALE + 1 * import_rate * KEEP_SCALE, + assert_keep=1 * import_rate * KEEP_SCALE, keep=1.0, save="test", ) @@ -3411,7 +3413,7 @@ def run_model_tests(my_predbat): with_battery=True, discharge=0, battery_soc=10, - assert_keep=14 * import_rate * 0.5 * KEEP_SCALE + 1 * import_rate * KEEP_SCALE, + assert_keep=1 * import_rate * KEEP_SCALE, keep=1.0, save="none", ) @@ -4056,7 +4058,7 @@ def run_model_tests(my_predbat): battery_size=10, keep=1.0, assert_final_iboost=0, - assert_keep=import_rate * 14 * 0.5 * KEEP_SCALE + import_rate * 1 * KEEP_SCALE, + assert_keep=import_rate * 1 * KEEP_SCALE, ) # Alternating high/low rates @@ -4138,6 +4140,7 @@ def main(): parser = argparse.ArgumentParser(description="Predbat unit tests") parser.add_argument("--debug_file", action="store", help="Enable debug output") args = parser.parse_args() + print("**** Starting Predbat tests ****") my_predbat = PredBat() From 0da3c62a6a2c9cba61c9b7b5097de8a3662db37f 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, 13 Dec 2024 08:44:24 +0000 Subject: [PATCH 4/4] [pre-commit.ci lite] apply automatic fixes --- apps/predbat/predbat.py | 2 ++ apps/predbat/unit_test.py | 50 +++++++++++++++++++-------------------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/apps/predbat/predbat.py b/apps/predbat/predbat.py index 17a105cd..d888f89a 100644 --- a/apps/predbat/predbat.py +++ b/apps/predbat/predbat.py @@ -24,9 +24,11 @@ try: import adbase as ad import appdaemon.plugins.hass.hassapi as hass + IS_APPDAEMON = True except: import hass as hass + IS_APPDAEMON = False import pytz diff --git a/apps/predbat/unit_test.py b/apps/predbat/unit_test.py index 14eb2e03..f6622bf7 100644 --- a/apps/predbat/unit_test.py +++ b/apps/predbat/unit_test.py @@ -907,8 +907,8 @@ def run_single_debug(my_predbat, debug_file): # Force off combine export XXX: print("Combined export slots {}".format(my_predbat.combine_export_slots)) - #my_predbat.combine_export_slots = False - #my_predbat.best_soc_keep = 1.0 + # my_predbat.combine_export_slots = False + # my_predbat.best_soc_keep = 1.0 my_predbat.metric_min_improvement_export = 5 if re_do_rates: @@ -953,13 +953,13 @@ def run_single_debug(my_predbat, debug_file): load_forecast=my_predbat.load_forecast, load_scaling_dynamic=my_predbat.load_scaling_dynamic, cloud_factor=min(my_predbat.metric_load_divergence + 0.5, 1.0) if my_predbat.metric_load_divergence else None, - ) + ) pv_step = my_predbat.pv_forecast_minute_step pv10_step = my_predbat.pv_forecast_minute10_step load_step = my_predbat.load_minutes_step load10_step = my_predbat.load_minutes_step10 - + my_predbat.prediction = Prediction(my_predbat, pv_step, pv_step, load_step, load_step) my_predbat.debug_enable = True @@ -1048,7 +1048,6 @@ def run_single_debug(my_predbat, debug_file): print("Wrote plan to plan_final.html") - def run_execute_tests(my_predbat): print("**** Running execute tests ****\n") reset_inverter(my_predbat) @@ -1299,22 +1298,22 @@ def run_execute_tests(my_predbat): return failed my_predbat.battery_charge_power_curve = { - 100 : 0.50, - 99 : 0.50, - 98 : 0.50, - 97 : 0.50, - 96 : 0.50, - 95 : 0.50, - 94 : 1.00, - 93 : 1.00, - 92 : 1.00, - 91 : 1.00, - 90 : 1.00, - 89 : 1.00, - 88 : 1.00, - 87 : 1.00, - 86 : 1.00, - 85 : 1.00, + 100: 0.50, + 99: 0.50, + 98: 0.50, + 97: 0.50, + 96: 0.50, + 95: 0.50, + 94: 1.00, + 93: 1.00, + 92: 1.00, + 91: 1.00, + 90: 1.00, + 89: 1.00, + 88: 1.00, + 87: 1.00, + 86: 1.00, + 85: 1.00, } # 60 minutes - 10 minute margin = 50 minutes to add 0.75kWh to each battery (x2 inverters) @@ -2941,10 +2940,10 @@ def run_model_tests(my_predbat): failed |= simple_scenario("load_only", my_predbat, 1, 0, assert_final_metric=import_rate * 24, assert_final_soc=0, with_battery=False) failed |= simple_scenario("load_bat_ac", my_predbat, 4, 0, assert_final_metric=import_rate * 24 * 3.2, assert_final_soc=100 - 24, with_battery=True, battery_soc=100.0, inverter_loss=0.8) failed |= simple_scenario("load_bat_dc", my_predbat, 4, 0, assert_final_metric=import_rate * 24 * 3.2, assert_final_soc=100 - 24, with_battery=True, battery_soc=100.0, inverter_loss=0.8, hybrid=True) - failed |= simple_scenario("load_bat_ac2", my_predbat, 0.5, 0, assert_final_metric=0, assert_final_soc=100 - 12/0.8, with_battery=True, battery_soc=100.0, inverter_loss=0.8) - failed |= simple_scenario("load_bat_dc2", my_predbat, 0.5, 0, assert_final_metric=0, assert_final_soc=100 - 12/0.8, with_battery=True, battery_soc=100.0, inverter_loss=0.8, hybrid=True) - failed |= simple_scenario("load_bat_ac3", my_predbat, 1.0, 0, assert_final_metric=import_rate * 0.2*24, assert_final_soc=100 - 24, with_battery=True, battery_soc=100.0, inverter_loss=0.8) - failed |= simple_scenario("load_bat_dc3", my_predbat, 1.0, 0, assert_final_metric=import_rate * 0.2*24, assert_final_soc=100 - 24, with_battery=True, battery_soc=100.0, inverter_loss=0.8, hybrid=True) + failed |= simple_scenario("load_bat_ac2", my_predbat, 0.5, 0, assert_final_metric=0, assert_final_soc=100 - 12 / 0.8, with_battery=True, battery_soc=100.0, inverter_loss=0.8) + failed |= simple_scenario("load_bat_dc2", my_predbat, 0.5, 0, assert_final_metric=0, assert_final_soc=100 - 12 / 0.8, with_battery=True, battery_soc=100.0, inverter_loss=0.8, hybrid=True) + failed |= simple_scenario("load_bat_ac3", my_predbat, 1.0, 0, assert_final_metric=import_rate * 0.2 * 24, assert_final_soc=100 - 24, with_battery=True, battery_soc=100.0, inverter_loss=0.8) + failed |= simple_scenario("load_bat_dc3", my_predbat, 1.0, 0, assert_final_metric=import_rate * 0.2 * 24, assert_final_soc=100 - 24, with_battery=True, battery_soc=100.0, inverter_loss=0.8, hybrid=True) failed |= simple_scenario( "load_bat_dc_pv", @@ -4140,7 +4139,6 @@ def main(): parser = argparse.ArgumentParser(description="Predbat unit tests") parser.add_argument("--debug_file", action="store", help="Enable debug output") args = parser.parse_args() - print("**** Starting Predbat tests ****") my_predbat = PredBat()