diff --git a/apps/predbat/predbat.py b/apps/predbat/predbat.py
index f77e4ef0..fdb49ec1 100644
--- a/apps/predbat/predbat.py
+++ b/apps/predbat/predbat.py
@@ -16,7 +16,7 @@
import appdaemon.plugins.hass.hassapi as hass
import adbase as ad
-THIS_VERSION = "v7.13.14"
+THIS_VERSION = "v7.13.15"
TIME_FORMAT = "%Y-%m-%dT%H:%M:%S%z"
TIME_FORMAT_SECONDS = "%Y-%m-%dT%H:%M:%S.%f%z"
TIME_FORMAT_OCTOPUS = "%Y-%m-%d %H:%M:%S%z"
@@ -313,6 +313,7 @@
{"name": "iboost_min_soc", "friendly_name": "IBoost min soc", "type": "input_number", "min": 0, "max": 100, "step": 5, "unit": "%", "icon": "mdi:percent", 'enable' : 'iboost_enable', 'default' : 0.0},
{"name": "holiday_days_left", "friendly_name": "Holiday days left", "type": "input_number", "min": 0, "max": 28, "step": 1, "unit": "days", "icon": "mdi:clock-end", 'default' : 0},
{"name": "forecast_plan_hours", "friendly_name": "Plan forecast hours", "type": "input_number", "min": 8, "max": 96, "step": 1, "unit": "hours", "icon": "mdi:clock-end", 'enable' : 'expert_mode', 'default' : 96},
+ {"name": "plan_debug", "friendly_name": "HTML Plan Debug", "type": "switch", 'default' : False},
]
"""
@@ -3597,6 +3598,9 @@ def run_prediction(
predict_grid_power[stamp] = self.dp3(diff * (60 / step))
predict_load_power[stamp] = self.dp3(load_yesterday * (60 / step))
+ if save and save == "best":
+ self.predict_metric_best[minute] = self.dp2(metric)
+
minute += step
hours_left = minute_left / 60.0
@@ -5136,11 +5140,16 @@ def publish_html_plan(self, pv_forecast_minute_step, load_minutes_step, end_reco
"""
Publish the current plan in HTML format
"""
+ plan_debug = self.get_arg('plan_debug')
html = "
"
html += ""
html += "Time | "
- html += "Import p | "
- html += "Export p | "
+ if plan_debug:
+ html += "Import p (w/loss) | "
+ html += "Export p (w/loss) | "
+ else:
+ html += "Import p | "
+ html += "Export p | "
html += "State | "
html += "Limit % | "
html += "PV kWh | "
@@ -5148,6 +5157,7 @@ def publish_html_plan(self, pv_forecast_minute_step, load_minutes_step, end_reco
if self.num_cars > 0:
html += "Car kWh | "
html += "SOC % | "
+ html += "Cost | "
html += "
"
minute_now_align = int(self.minutes_now / 30) * 30
@@ -5189,9 +5199,13 @@ def publish_html_plan(self, pv_forecast_minute_step, load_minutes_step, end_reco
load_forecast = self.dp2(load_forecast)
soc_percent = int(self.dp2((self.predict_soc_best.get(minute_relative, 0.0) / self.soc_max) * 100.0) + 0.5)
- soc_percent_end = int(self.dp2((self.predict_soc_best.get(minute_relative + 30.0, 0.0) / self.soc_max) * 100.0) + 0.5)
+ soc_percent_end = int(self.dp2((self.predict_soc_best.get(minute_relative + 30, 0.0) / self.soc_max) * 100.0) + 0.5)
soc_percent_max = max(soc_percent, soc_percent_end)
+ soc_percent_min = min(soc_percent, soc_percent_end)
soc_change = self.predict_soc_best.get(minute_relative + 30, 0.0) - self.predict_soc_best.get(minute_relative, 0.0)
+ metric_start = self.predict_metric_best.get(minute_relative, 0.0)
+ metric_end = self.predict_metric_best.get(minute_relative + 30, 0.0)
+ metric_change = metric_end - metric_start
soc_sym = ""
if abs(soc_change) < 0.05:
@@ -5241,14 +5255,21 @@ def publish_html_plan(self, pv_forecast_minute_step, load_minutes_step, end_reco
if charge_window_n >= 0:
limit = self.charge_limit_best[charge_window_n]
+ limit_percent = int(self.charge_limit_percent_best[charge_window_n])
if limit > 0.0:
if self.set_charge_freeze and (limit == self.reserve):
state = "FreezeChrg→"
state_color = "#EEEEEE"
+ elif limit_percent == soc_percent_min:
+ state = "HoldChrg→"
+ state_color = "#34dbeb"
+ elif limit_percent < soc_percent_min:
+ state = "NoCharge↘"
+ state_color = "#FFFFFF"
else:
state = "Charge↗"
state_color = "#3AEE85"
- show_limit = str(int(self.charge_limit_percent_best[charge_window_n]))
+ show_limit = str(limit_percent)
if discharge_window_n >= 0:
limit = self.discharge_limits_best[discharge_window_n]
@@ -5269,6 +5290,10 @@ def publish_html_plan(self, pv_forecast_minute_step, load_minutes_step, end_reco
rate_str_import = "%02.02f ?" % (rate_value_import)
else:
rate_str_import = "%02.02f" % rate_value_import
+
+ if plan_debug:
+ rate_str_import += " (%02.02f)" % (rate_value_import / self.battery_loss / self.inverter_loss + self.metric_battery_cycle)
+
if charge_window_n >= 0:
rate_str_import = "" + rate_str_import + ""
@@ -5276,9 +5301,25 @@ def publish_html_plan(self, pv_forecast_minute_step, load_minutes_step, end_reco
rate_str_export = "%02.02f ?" % (rate_value_export)
else:
rate_str_export = "%2.02f" % (rate_value_export)
+
+ if plan_debug:
+ rate_str_export += " (%02.02f)" % (rate_value_export * self.battery_loss_discharge * self.inverter_loss - self.metric_battery_cycle)
+
if discharge_window_n >= 0:
rate_str_export = "" + rate_str_export + ""
+ # Cost
+ cost_str = "£%02.02f" % (metric_start / 100.0)
+ if metric_change >= 0.5:
+ cost_str += " ↗"
+ cost_color = '#F18261'
+ elif metric_change <= -0.5:
+ cost_str += " ↘"
+ cost_color = "#3AEE85"
+ else:
+ cost_str += " →"
+ cost_color = "#FFFFFF"
+
# Car charging?
if self.num_cars > 0:
car_charging_kwh = 0.0
@@ -5311,6 +5352,7 @@ def publish_html_plan(self, pv_forecast_minute_step, load_minutes_step, end_reco
if self.num_cars > 0: # Don't display car charging data if there's no car
html += "" + car_charging_str + " | "
html += "" + str(soc_percent) + soc_sym + " | "
+ html += "" + str(cost_str) + " | "
html += ""
html += "
"
self.dashboard_item(self.prefix + ".plan_html", state="", attributes={"html": html, "friendly_name": "Plan in HTML", "icon": "mdi:web-box"})
@@ -5761,6 +5803,7 @@ def reset(self):
self.end_record = 24 * 60 * 2
self.predict_soc = {}
self.predict_soc_best = {}
+ self.predict_metric_best = {}
self.metric_min_improvement = 0.0
self.metric_min_improvement_discharge = 0.0
self.metric_battery_cycle = 0.0
@@ -5901,6 +5944,7 @@ def optimise_charge_limit_price(
best_soc_min = self.reserve
best_cost = 0
best_price_charge = price_set[-1]
+ best_price_discharge = price_set[0]
tried_list = {}
# Do we loop on discharge?
@@ -5912,12 +5956,14 @@ def optimise_charge_limit_price(
# Most expensive first
all_prices = price_set[::] + [price_set[-1] - 1]
self.log("All prices {}".format(all_prices))
+ window_prices = {}
+ window_prices_discharge = {}
for loop_price in all_prices:
- window_prices = {}
for divide in [2, 4, 8, 16, 96]:
all_n = []
all_d = []
highest_price_charge = price_set[-1]
+ lowest_price_discharge = price_set[0]
divide_count_c = 0
divide_count_d = 0
first_charge = True
@@ -5929,8 +5975,8 @@ def optimise_charge_limit_price(
for key in links:
window_n = window_index[key]["id"]
typ = window_index[key]["type"]
- window_prices[window_n] = price
if typ == "c":
+ window_prices[window_n] = price
if first_charge:
if (int(divide_count_c / divide) % 2) == 0:
all_n.append(window_n)
@@ -5945,6 +5991,7 @@ def optimise_charge_limit_price(
typ = window_index[key]["type"]
window_n = window_index[key]["id"]
if typ == "d":
+ window_prices_discharge[window_n] = price
if first_discharge:
if (int(divide_count_d / divide) % 2) == 0:
all_d.append(window_n)
@@ -5975,6 +6022,8 @@ def optimise_charge_limit_price(
hit_charge = self.hit_charge_window(self.charge_window_best, self.discharge_window_best[window_n]["start"], self.discharge_window_best[window_n]["end"])
if not self.calculate_discharge_oncharge and hit_charge >= 0 and try_charge_limit[hit_charge] > 0.0:
continue
+ if window_prices_discharge[window_n] < lowest_price_discharge:
+ lowest_price_discharge = window_prices_discharge[window_n]
try_discharge[window_n] = 0
# Skip this one as it's the same as selected already
@@ -6028,6 +6077,7 @@ def optimise_charge_limit_price(
best_metric = metric
best_price = loop_price
best_price_charge = highest_price_charge
+ best_price_discharge = lowest_price_discharge
best_limits = try_charge_limit.copy()
best_discharge = try_discharge.copy()
best_soc_min = soc_min
@@ -6042,7 +6092,7 @@ def optimise_charge_limit_price(
self.dp2(best_price), self.dp2(best_price_charge), self.dp2(best_metric), self.dp2(best_cost), self.dp2(best_soc_min), best_limits, best_discharge
)
)
- return best_limits, best_discharge, best_price_charge
+ return best_limits, best_discharge, best_price_charge, best_price_discharge
def optimise_charge_limit(
self,
@@ -6445,7 +6495,7 @@ def sort_window_by_price_combined(self, charge_windows, discharge_windows, stand
if stand_alone:
average = self.dp2(window["average"])
else:
- average = self.dp2(window["average"] / self.inverter_loss / self.battery_loss)
+ average = self.dp2(window["average"] / self.inverter_loss / self.battery_loss + self.metric_battery_cycle)
sort_key = "%04.2f_%03d_c%02d" % (5000 - average, 999 - id, id)
window_sort.append(sort_key)
window_links[sort_key] = {}
@@ -6459,7 +6509,7 @@ def sort_window_by_price_combined(self, charge_windows, discharge_windows, stand
id = 0
for window in discharge_windows:
# Account for losses in average rate as it makes export value lower
- average = self.dp2(window["average"] * self.inverter_loss * self.battery_loss_discharge)
+ average = self.dp2(window["average"] * self.inverter_loss * self.battery_loss_discharge - self.metric_battery_cycle)
sort_key = "%04.2f_%03d_d%02d" % (5000 - average, 999 - id, id)
if not self.calculate_discharge_first:
# Push discharge last if first is not set
@@ -6818,7 +6868,7 @@ def optimise_all_windows(self, end_record, load_minutes_step, pv_forecast_minute
if price_set and self.calculate_best_charge and self.charge_window_best:
self.log("Optimise all windows, total charge {} discharge {}".format(record_charge_windows, record_discharge_windows))
self.optimise_charge_windows_reset(end_record, load_minutes_step, pv_forecast_minute_step, pv_forecast_minute10_step)
- self.charge_limit_best, ignore_discharge_limits, best_price = self.optimise_charge_limit_price(
+ self.charge_limit_best, ignore_discharge_limits, best_price, best_price_discharge = self.optimise_charge_limit_price(
price_set,
price_links,
window_index,
@@ -6833,8 +6883,8 @@ def optimise_all_windows(self, end_record, load_minutes_step, pv_forecast_minute
end_record=end_record,
)
- self.rate_best_cost_threshold_charge = best_price * self.inverter_loss * self.battery_loss
- self.rate_best_cost_threshold_discharge = best_price / self.inverter_loss / self.battery_loss
+ self.rate_best_cost_threshold_charge = best_price
+ self.rate_best_cost_threshold_discharge = best_price_discharge
# Optimise individual windows in the price band for charge/discharge
# First optimise those at or below threshold highest to lowest (to turn down values)
@@ -6858,6 +6908,10 @@ def optimise_all_windows(self, end_record, load_minutes_step, pv_forecast_minute
window_n = window_index[key]["id"]
if typ == "c":
+ # Store price set with window
+ self.charge_window_best[window_n]["set"] = price
+
+ # Skip those outside threshold
if not start_at_low and price > best_price:
continue
if start_at_low and price <= best_price:
@@ -6902,6 +6956,9 @@ def optimise_all_windows(self, end_record, load_minutes_step, pv_forecast_minute
)
)
else:
+ # Store price set with window
+ self.discharge_window_best[window_n]["set"] = price
+
# Do highest price first
if start_at_low:
continue