Skip to content

Commit

Permalink
Fixes for metric keep & iboost scaling (#1277)
Browse files Browse the repository at this point in the history
* Update .flake8

* New test

* [pre-commit.ci lite] apply automatic fixes

* Fixes

* New iboost scaling option

* [pre-commit.ci lite] apply automatic fixes

---------

Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
  • Loading branch information
springfall2008 and pre-commit-ci-lite[bot] authored Jul 2, 2024
1 parent acf0f20 commit 1e413b5
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 41 deletions.
2 changes: 1 addition & 1 deletion .flake8
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[flake8]
max-line-length = 180
max-line-length = 250
max-complexity = 15
exclude = build/*
extend-ignore =
Expand Down
12 changes: 12 additions & 0 deletions apps/predbat/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -892,6 +892,18 @@
"enable": "iboost_enable",
"default": 0.0,
},
{
"name": "iboost_value_scaling",
"friendly_name": "iBoost value scaling",
"type": "input_number",
"min": 0,
"max": 2.0,
"step": 0.1,
"unit": "*",
"enable": "iboost_enable",
"icon": "mdi:multiplication",
"default": 0.75,
},
{
"name": "holiday_days_left",
"friendly_name": "Holiday days left",
Expand Down
14 changes: 10 additions & 4 deletions apps/predbat/predbat.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
import asyncio
import json

THIS_VERSION = "v8.2.2"
THIS_VERSION = "v8.2.3"
PREDBAT_FILES = ["predbat.py", "config.py", "prediction.py", "utils.py", "inverter.py", "ha.py", "download.py", "unit_test.py"]
from download import predbat_update_move, predbat_update_download, check_install

Expand Down Expand Up @@ -5497,8 +5497,8 @@ def compute_metric(
# ie. how much extra battery is worth to us in future, assume it's the same as low rate
rate_min = self.rate_min_forward.get(end_record, self.rate_min) / self.inverter_loss / self.battery_loss + self.metric_battery_cycle
rate_export_min = self.rate_export_min * self.inverter_loss * self.battery_loss_discharge - self.metric_battery_cycle - rate_min
metric -= (soc + final_iboost) * max(rate_min, 1.0, rate_export_min) * self.metric_battery_value_scaling
metric10 -= (soc10 + final_iboost10) * max(rate_min, 1.0, rate_export_min) * self.metric_battery_value_scaling
metric -= (soc * self.metric_battery_value_scaling + final_iboost * self.iboost_value_scaling) * max(rate_min, 1.0, rate_export_min)
metric10 -= (soc10 * self.metric_battery_value_scaling + final_iboost10 * self.iboost_value_scaling) * max(rate_min, 1.0, rate_export_min)
# Metric adjustment based on 10% outcome weighting
if metric10 > metric:
metric_diff = metric10 - metric
Expand Down Expand Up @@ -5965,7 +5965,7 @@ def optimise_discharge(

if self.debug_enable:
self.log(
"Sim: Discharge {} window {} start {} end {}, import {} export {} min_soc {} @ {} soc {} cost {} cost10 {} metric {} cycle {} end_record {}".format(
"Sim: Discharge {} window {} start {} end {}, import {} export {} min_soc {} @ {} soc {} soc10 {} cost {} cost10 {} metric {} cycle {} iboost {} iboost10 {} carbon {} keep {} end_record {}".format(
this_discharge_limit,
window_n,
self.time_abs_str(start),
Expand All @@ -5975,10 +5975,15 @@ def optimise_discharge(
self.dp4(soc_min),
self.time_abs_str(soc_min_minute),
self.dp4(soc),
self.dp4(soc10),
self.dp4(cost),
self.dp4(cost10),
self.dp4(metric),
self.dp4(battery_cycle * self.metric_battery_cycle),
self.dp4(final_iboost),
self.dp4(final_iboost10),
self.dp4(final_carbon_g),
self.dp4(metric_keep),
end_record,
)
)
Expand Down Expand Up @@ -9538,6 +9543,7 @@ def fetch_config_options(self):
self.iboost_min_power = self.get_arg("iboost_min_power") / MINUTE_WATT
self.iboost_min_soc = self.get_arg("iboost_min_soc")
self.iboost_today = self.get_arg("iboost_today")
self.iboost_value_scaling = self.get_arg("iboost_value_scaling")
self.iboost_next = self.iboost_today
self.iboost_running = False
self.iboost_energy_today = {}
Expand Down
66 changes: 32 additions & 34 deletions apps/predbat/prediction.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ def __init__(self, base=None, pv_forecast_minute_step=None, pv_forecast_minute10
if base:
self.minutes_now = base.minutes_now
self.log = base.log
self.time_abs_str = base.time_abs_str
self.forecast_minutes = base.forecast_minutes
self.midnight_utc = base.midnight_utc
self.soc_kw = base.soc_kw
Expand Down Expand Up @@ -432,8 +433,8 @@ def run_prediction(self, charge_limit, charge_window, discharge_window, discharg
if save and (save in ["best", "test"]):
self.predict_soc_best[minute] = round(soc, 3)
self.predict_metric_best[minute] = round(metric, 3)
self.predict_iboost_best[minute] = iboost_today_kwh
self.predict_carbon_best[minute] = carbon_g
self.predict_iboost_best[minute] = round(iboost_today_kwh, 2)
self.predict_carbon_best[minute] = round(carbon_g, 0)

# Add in standing charge, only for the final plan when we save the results
if save and (save in ["best", "base", "base10", "best10", "test"]) and (minute_absolute % (24 * 60)) < step:
Expand Down Expand Up @@ -461,7 +462,7 @@ def run_prediction(self, charge_limit, charge_window, discharge_window, discharg
car_load_scale = car_load[car_n] * step / 60.0
car_load_scale = car_load_scale * self.car_charging_loss
car_load_scale = max(min(car_load_scale, self.car_charging_limit[car_n] - car_soc[car_n]), 0)
car_soc[car_n] = round(car_soc[car_n] + car_load_scale, 3)
car_soc[car_n] = car_soc[car_n] + car_load_scale
load_yesterday += car_load_scale / self.car_charging_loss
# Model not allowing the car to charge from the battery
if not self.car_charging_from_battery:
Expand Down Expand Up @@ -606,13 +607,20 @@ def run_prediction(self, charge_limit, charge_window, discharge_window, discharg
pv_dc = 0
pv_ac = (pv_now - pv_dc) * inverter_loss_ac

if save == "test":
print(
"minute {} charge_limit {} soc {} pv_now {} charge left {} pv_ac {} pv_dc {} max charge {} pv_compare {}".format(
minute, charge_limit_n, soc, pv_now, charge_limit_n - soc, pv_ac, pv_dc, charge_limit_n - soc, pv_ac + pv_dc
)
)

if (charge_limit_n - soc) < (charge_rate_now_curve * step):
# The battery will hit the charge limit in this period, so if the charge was spread over the period
# it could be done from solar, but in reality it will be full rate and then stop meaning the solar
# won't cover it and it will likely create an import.
pv_compare = pv_dc + pv_ac
if pv_dc >= (charge_limit_n - soc) and (pv_compare < (charge_rate_now_curve * step)):
potential_import = (charge_rate_now_curve * step) - pv_compare
potential_import = min((charge_rate_now_curve * step) - pv_compare, (charge_limit_n - soc))
metric_keep += potential_import * rate_import.get(minute_absolute, 0)
else:
# ECO Mode
Expand Down Expand Up @@ -702,6 +710,7 @@ def run_prediction(self, charge_limit, charge_window, discharge_window, discharg
soc = max(soc - battery_draw / self.battery_loss_discharge, reserve_expected)
else:
soc = min(soc - battery_draw * self.battery_loss, self.soc_max)
soc = round(soc, 6)

# iBoost solar diverter model
if self.iboost_enable:
Expand All @@ -727,15 +736,11 @@ def run_prediction(self, charge_limit, charge_window, discharge_window, discharg
else:
self.iboost_running = False

# Rounding on SOC
soc = round(soc, 6)

# Count battery cycles
battery_cycle = round(battery_cycle + abs(battery_draw), 4)
battery_cycle = battery_cycle + abs(battery_draw)

# Work out left over energy after battery adjustment
diff = get_diff(battery_draw, pv_dc, pv_ac, load_yesterday, inverter_loss)
diff = round(diff, 6)

# Metric keep - pretend the battery is empty and you have to import instead of using the battery
if soc < self.best_soc_keep:
Expand All @@ -744,7 +749,6 @@ def run_prediction(self, charge_limit, charge_window, discharge_window, discharg
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 diff > 0:
# Import
# All imports must go to home (no inverter loss) or to the battery (inverter loss accounted before above)
Expand Down Expand Up @@ -776,12 +780,6 @@ def run_prediction(self, charge_limit, charge_window, discharge_window, discharg
else:
grid_state = "~"

# Rounding for next stage
metric = round(metric, 4)
import_kwh_battery = round(import_kwh_battery, 6)
import_kwh_house = round(import_kwh_house, 6)
export_kwh = round(export_kwh, 6)

# Store the number of minutes until the battery runs out
if record and soc <= self.reserve:
minute_left = min(minute, minute_left)
Expand Down Expand Up @@ -849,23 +847,23 @@ def run_prediction(self, charge_limit, charge_window, discharge_window, discharg
self.hours_left = hours_left
self.final_car_soc = final_car_soc
self.predict_car_soc_time = predict_car_soc_time
self.final_soc = final_soc
self.final_metric = final_metric
self.final_metric_keep = final_metric_keep
self.final_import_kwh = final_import_kwh
self.final_import_kwh_battery = final_import_kwh_battery
self.final_import_kwh_house = final_import_kwh_house
self.final_export_kwh = final_export_kwh
self.final_load_kwh = final_load_kwh
self.final_pv_kwh = final_pv_kwh
self.final_iboost_kwh = final_iboost_kwh
self.final_battery_cycle = final_battery_cycle
self.final_soc_min = soc_min
self.final_soc = round(final_soc, 4)
self.final_metric = round(final_metric, 4)
self.final_metric_keep = round(final_metric_keep, 4)
self.final_import_kwh = round(final_import_kwh, 4)
self.final_import_kwh_battery = round(final_import_kwh_battery, 4)
self.final_import_kwh_house = round(final_import_kwh_house, 4)
self.final_export_kwh = round(final_export_kwh, 4)
self.final_load_kwh = round(final_load_kwh, 4)
self.final_pv_kwh = round(final_pv_kwh, 4)
self.final_iboost_kwh = round(final_iboost_kwh, 4)
self.final_battery_cycle = round(final_battery_cycle, 4)
self.final_soc_min = round(soc_min, 4)
self.final_soc_min_minute = soc_min_minute
self.export_to_first_charge = export_to_first_charge
self.predict_soc_time = predict_soc_time
self.first_charge = first_charge
self.first_charge_soc = first_charge_soc
self.first_charge_soc = round(first_charge_soc, 4)
self.predict_state = predict_state
self.predict_battery_power = predict_battery_power
self.predict_battery_power = predict_battery_power
Expand All @@ -879,10 +877,10 @@ def run_prediction(self, charge_limit, charge_window, discharge_window, discharg
self.record_time = record_time
self.predict_battery_cycle = predict_battery_cycle
self.predict_battery_power = predict_battery_power
self.pv_kwh_h0 = pv_kwh_h0
self.import_kwh_h0 = import_kwh_h0
self.export_kwh_h0 = export_kwh_h0
self.load_kwh_h0 = load_kwh_h0
self.pv_kwh_h0 = round(pv_kwh_h0, 4)
self.import_kwh_h0 = round(import_kwh_h0, 4)
self.export_kwh_h0 = round(export_kwh_h0, 4)
self.load_kwh_h0 = round(load_kwh_h0, 4)
self.load_kwh_time = load_kwh_time
self.pv_kwh_time = pv_kwh_time
self.import_kwh_time = import_kwh_time
Expand All @@ -893,7 +891,7 @@ def run_prediction(self, charge_limit, charge_window, discharge_window, discharg
round(import_kwh_battery, 4),
round(import_kwh_house, 4),
round(export_kwh, 4),
soc_min,
round(soc_min, 4),
round(final_soc, 4),
soc_min_minute,
round(final_battery_cycle, 4),
Expand Down
34 changes: 32 additions & 2 deletions apps/predbat/unit_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,9 @@ def simple_scenario(
my_predbat.reserve = reserve
my_predbat.inverter_loss = inverter_loss
my_predbat.battery_rate_max_charge = battery_rate_max_charge / 60.0
my_predbat.battery_rate_max_discharge = battery_rate_max_charge / 60.0
my_predbat.battery_rate_max_charge_scaled = battery_rate_max_charge / 60.0
my_predbat.battery_rate_max_discharge_scaled = battery_rate_max_charge / 60.0
my_predbat.car_charging_from_battery = car_charging_from_battery

my_predbat.iboost_enable = iboost_solar or iboost_gas or iboost_charging
Expand Down Expand Up @@ -780,6 +783,20 @@ def run_model_tests(my_predbat):
with_battery=True,
charge_car=4.0,
)
failed |= simple_scenario(
"load_discharge_car2",
my_predbat,
0,
0,
assert_final_metric=import_rate * 24 * 1.5,
assert_final_soc=100 - 24 * 2.5,
battery_soc=100.0,
with_battery=True,
charge_car=4.0,
discharge=0,
inverter_limit=3.5,
battery_rate_max_charge=2.5,
)
failed |= simple_scenario("load_discharge_fast", my_predbat, 2, 0, assert_final_metric=import_rate * 38, assert_final_soc=0, battery_soc=10.0, with_battery=True)
failed |= simple_scenario("load_discharge_fast_big", my_predbat, 2, 0, assert_final_metric=import_rate * 24, assert_final_soc=76, battery_soc=100.0, with_battery=True)
failed |= simple_scenario(
Expand Down Expand Up @@ -964,7 +981,7 @@ def run_model_tests(my_predbat):
hybrid=True,
)
failed |= simple_scenario(
"battery_charge_pv_term_dc",
"battery_charge_pv_term_dc1",
my_predbat,
0,
0.5,
Expand All @@ -974,7 +991,20 @@ def run_model_tests(my_predbat):
charge=10,
battery_size=100,
hybrid=True,
assert_keep=import_rate / 60 * 5 * 0.5,
assert_keep=0,
)
failed |= simple_scenario(
"battery_charge_pv_term_dc2",
my_predbat,
0,
0.5,
assert_final_metric=import_rate * 10 * 0.5,
assert_final_soc=10 + 14 * 0.5,
with_battery=True,
charge=9.95,
battery_size=100,
hybrid=True,
assert_keep=((1 / 60 * 5) - 0.05) * import_rate,
)
failed |= simple_scenario(
"battery_charge_pv_load1",
Expand Down

0 comments on commit 1e413b5

Please sign in to comment.