diff --git a/apps/predbat/config.py b/apps/predbat/config.py index 0db15cfb..e32452c4 100644 --- a/apps/predbat/config.py +++ b/apps/predbat/config.py @@ -298,6 +298,18 @@ "icon": "mdi:battery-50", "default": 0.5, }, + { + "name": "best_soc_keep_weight", + "friendly_name": "Best SOC Keep Weighting", + "type": "input_number", + "min": 0.1, + "max": 5, + "step": 0.10, + "unit": "*", + "icon": "mdi:multiplication", + "default": 0.5, + "enable": "expert_mode", + }, { "name": "metric_min_improvement", "friendly_name": "Metric Min Improvement", diff --git a/apps/predbat/fetch.py b/apps/predbat/fetch.py index 8e346d86..02589c5b 100644 --- a/apps/predbat/fetch.py +++ b/apps/predbat/fetch.py @@ -1759,6 +1759,7 @@ def fetch_config_options(self): self.best_soc_min = self.get_arg("best_soc_min") self.best_soc_max = self.get_arg("best_soc_max") self.best_soc_keep = self.get_arg("best_soc_keep") + self.best_soc_keep_weight = self.get_arg("best_soc_keep_weight") self.set_soc_minutes = 30 self.set_window_minutes = 30 self.inverter_set_charge_before = self.get_arg("inverter_set_charge_before") diff --git a/apps/predbat/predbat.py b/apps/predbat/predbat.py index c99d5c09..6b8587dc 100644 --- a/apps/predbat/predbat.py +++ b/apps/predbat/predbat.py @@ -344,6 +344,7 @@ def reset(self): self.best_soc_max = 0 self.best_soc_margin = 0 self.best_soc_keep = 0 + self.best_soc_keep_weight = 0.5 self.rate_min = 0 self.rate_min_minute = 0 self.rate_min_forward = {} diff --git a/apps/predbat/prediction.py b/apps/predbat/prediction.py index 35e4da01..2e2c998f 100644 --- a/apps/predbat/prediction.py +++ b/apps/predbat/prediction.py @@ -154,6 +154,7 @@ def __init__(self, base=None, pv_forecast_minute_step=None, pv_forecast_minute10 self.battery_loss = base.battery_loss self.battery_loss_discharge = base.battery_loss_discharge self.best_soc_keep = base.best_soc_keep + self.best_soc_keep_weight = base.best_soc_keep_weight self.best_soc_min = base.best_soc_min self.car_charging_battery_size = base.car_charging_battery_size self.rate_import = base.rate_import @@ -411,9 +412,9 @@ def run_prediction(self, charge_limit, charge_window, export_window, export_limi if minute < 4 * 60: keep_minute_scaling = 0 else: - keep_minute_scaling = min(((minute - 4 * 60) / (2 * 60)), 1.0) * 0.5 + keep_minute_scaling = min(((minute - 4 * 60) / (2 * 60)), 1.0) * self.best_soc_keep_weight else: - keep_minute_scaling = 0.5 + keep_minute_scaling = self.best_soc_keep_weight # Find charge & discharge windows charge_window_n = charge_window_optimised.get(minute_absolute, -1) @@ -808,12 +809,8 @@ def run_prediction(self, charge_limit, charge_window, export_window, export_limi diff = get_diff(battery_draw, pv_dc, pv_ac, load_yesterday, inverter_loss) # Metric keep - pretend the battery is empty and you have to import instead of using the battery - if soc < self.best_soc_keep: - # 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: - metric_keep += rate_import[minute_absolute] * keep_diff * keep_minute_scaling + if self.best_soc_keep > 0 and soc <= self.best_soc_keep: + metric_keep += (self.best_soc_keep - soc) * rate_import[minute_absolute] * keep_minute_scaling * step / 60.0 if diff > 0: # Import # All imports must go to home (no inverter loss) or to the battery (inverter loss accounted before above) diff --git a/apps/predbat/unit_test.py b/apps/predbat/unit_test.py index 29a59ff6..93c14c7d 100644 --- a/apps/predbat/unit_test.py +++ b/apps/predbat/unit_test.py @@ -1196,6 +1196,7 @@ def simple_scenario( carbon=0, assert_final_carbon=0.0, keep=0.0, + keep_weight=0.5, assert_keep=0.0, save="best", quiet=False, @@ -1250,6 +1251,7 @@ def simple_scenario( my_predbat.iboost_gas_scale = gas_scale my_predbat.iboost_charging = iboost_charging my_predbat.best_soc_keep = keep + my_predbat.best_soc_keep_weight = keep_weight my_predbat.car_charging_soc[0] = 0 my_predbat.car_charging_limit[0] = 100.0 @@ -1345,7 +1347,7 @@ def simple_scenario( if abs(final_carbon_g - assert_final_carbon) >= 0.1: print("ERROR: Final Carbon {} should be {}".format(final_carbon_g, assert_final_carbon)) failed = True - if abs(metric_keep - assert_keep) >= 0.1: + if abs(metric_keep - assert_keep) >= 0.5: print("ERROR: Metric keep {} should be {}".format(metric_keep, assert_keep)) failed = True if assert_iboost_running != prediction.iboost_running: @@ -3168,6 +3170,7 @@ def run_optimise_all_windows( hybrid=False, inverter_loss=1.0, best_soc_keep=0.0, + best_soc_keep_weight=0.5, ): print("Starting optimise all windows test {}".format(name)) end_record = my_predbat.forecast_minutes @@ -3179,6 +3182,7 @@ def run_optimise_all_windows( my_predbat.inverter_hybrid = hybrid my_predbat.inverter_loss = inverter_loss my_predbat.best_soc_keep = best_soc_keep + my_predbat.best_soc_keep_weight = best_soc_keep_weight reset_rates(my_predbat, rate_import, rate_export) update_rates_import(my_predbat, charge_window_best) @@ -3276,7 +3280,7 @@ def run_optimise_all_windows_tests(my_predbat): for n in range(0, 48): price = 16 - n % 16 charge_window_best.append({"start": my_predbat.minutes_now + 30 * n, "end": my_predbat.minutes_now + 30 * (n + 1), "average": price}) - expect_charge_limit.append(100 if price <= 5.0 else 0) + expect_charge_limit.append(10 if price <= 5.0 else 0) failed |= run_optimise_all_windows( "created2", my_predbat, @@ -3286,7 +3290,26 @@ def run_optimise_all_windows_tests(my_predbat): pv_amount=0, expect_best_price=5 / 0.9, inverter_loss=0.9, - best_soc_keep=0.5, + best_soc_keep=0.0, + battery_size=10, + ) + if failed: + return failed + + # One extra charge as we will fall below keep otherwise + expect_charge_limit[10] = 10.0 + failed |= run_optimise_all_windows( + "created3", + my_predbat, + charge_window_best=charge_window_best, + expect_charge_limit=expect_charge_limit, + load_amount=0.2, + pv_amount=0, + expect_best_price=5 / 0.9, + inverter_loss=0.9, + best_soc_keep=1, + battery_soc=2, + battery_size=10, ) if failed: return failed @@ -4061,8 +4084,9 @@ def run_model_tests(my_predbat): with_battery=True, discharge=0, battery_soc=10, - assert_keep=1 * import_rate * KEEP_SCALE, + assert_keep=14 * import_rate * 0.5 + ((1 + (1 / 12)) * import_rate * 0.5 * 0.5), keep=1, + keep_weight=0.5, ) failed |= simple_scenario( "battery_discharge_loss", @@ -4110,8 +4134,9 @@ 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=14 * import_rate + 1 * import_rate * 0.5, keep=1.0, + keep_weight=1.0, ) failed |= simple_scenario( "battery_discharge_load_keep_mode_test1", @@ -4123,8 +4148,9 @@ 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=14 * import_rate * 0.8 + 1 * import_rate * 0.8 * 0.5, keep=1.0, + keep_weight=0.8, save="test", ) failed |= simple_scenario( @@ -4137,8 +4163,9 @@ 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=14 * import_rate * 0.8 + 1 * import_rate * 0.8 * 0.5, keep=1.0, + keep_weight=0.8, save="none", ) failed |= simple_scenario( @@ -4781,8 +4808,9 @@ def run_model_tests(my_predbat): discharge=0, battery_size=10, keep=1.0, + keep_weight=1.0, assert_final_iboost=0, - assert_keep=import_rate * 14 * 0.5 * KEEP_SCALE + import_rate * 1 * KEEP_SCALE, + assert_keep=import_rate * 14 + import_rate * 1 * 0.5, ) # Alternating high/low rates diff --git a/docs/customisation.md b/docs/customisation.md index 0801273b..69c65c86 100644 --- a/docs/customisation.md +++ b/docs/customisation.md @@ -254,9 +254,15 @@ you want to export as late in the day as you can. **input_number.predbat_best_soc_keep** is the minimum battery level in kWh that Predbat will to try to keep the battery above for the Predbat plan. This is a soft constraint only that's used for longer term planning and is ignored for the forthcoming first 4 hours of the plan. As this is not used for short-term planning it's possible for your SoC to drop below this - use **input_number.predbat_best_soc_min** -if you need a hard SoC constraint that will always be maintained. +if you want to force all charges to be above a set level. It's usually good to have best_soc_keep set to a value above 0 to allow some margin in case you use more energy than planned between charge slots. +**input_number.predbat_best_soc_keep_weight** (_expert_mode_) Is used to tune how strongly you want the keep metric to apply. +A value of 0 would essentially ignore keep while higher values will make it more important to always stay above your keep threshold even if it costs +more money to do so. + +The default is 0.5 - this is the recommended setting. + **input_number.predbat_best_soc_min** (_expert mode_) sets the minimum charge level (in kWh) for charging during each slot and the minimum force export level also (set to 0 if you want to skip some slots). If you set this to a non-zero value you will need to use the low rate threshold to control which slots you charge from or you may charge all the time.