Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 70 additions & 29 deletions apps/predbat/predbat.py
Original file line number Diff line number Diff line change
Expand Up @@ -990,6 +990,7 @@ def find_charge_curve(self):
predbat_status_sensor = "predbat.status"
battery_power_sensor = self.base.get_arg("battery_power", indirect=False, index=self.id)
final_curve = {}
final_curve_count = {}

max_power = int(self.battery_rate_max_charge * 1000.0 * 60.0)
if soc_kwh_sensor and charge_rate_sensor and battery_power_sensor and predbat_status_sensor:
Expand Down Expand Up @@ -1077,27 +1078,33 @@ def find_charge_curve(self):
soc_charged = from_soc - to_soc
average_power = total_power / time_diff
charge_curve = round(min(average_power / max_power / self.base.battery_loss, 1.0), 2)
self.log(
"Charge Curve Percent: {}-{} at {} took {} minutes charged {} curve {} average_power {}".format(
data_point,
this_soc + 1,
self.base.time_abs_str(self.base.minutes_now - minute),
time_diff,
round(soc_charged, 2),
charge_curve,
average_power,
if self.base.debug_enable:
self.log(
"Charge Curve Percent: {}-{} at {} took {} minutes charged {} curve {} average_power {}".format(
data_point,
this_soc + 1,
self.base.time_abs_str(self.base.minutes_now - minute),
time_diff,
round(soc_charged, 2),
charge_curve,
average_power,
)
)
)
# Store data points
for point in range(this_soc + 1, 101):
if point not in final_curve:
final_curve[point] = charge_curve
final_curve_count[point] = 1
elif point <= data_point:
# Don't double count those above this point that already have data
final_curve[point] += charge_curve
final_curve_count[point] += 1

found = True
break
# Found this data point so stop looking
# If a data point was found, skip over this charge and look for more of them
if found:
break
minute = target_minute
if final_curve:
self.log("Charge curve before adjustment is: {}".format(final_curve))
found_required = True
Expand All @@ -1108,19 +1115,41 @@ def find_charge_curve(self):
if found_required:
# Scale curve to 1.0
rate_scaling = 0

# Average the data points
for index in final_curve:
if final_curve_count[index] > 0:
final_curve[index] = final_curve[index] / final_curve_count[index]

# Work out the maximum power to use as a scale factor
for index in final_curve:
rate_scaling = max(final_curve[index], rate_scaling)

# Scale the curve
if rate_scaling > 0:
for index in final_curve:
final_curve[index] = round(final_curve[index] / rate_scaling, 2)
text = " battery_charge_power_curve:\n"
keys = sorted(final_curve.keys())
keys.reverse()
for key in keys:
if final_curve[key] < 1.0:
text += " {} : {}\n".format(key, final_curve[key])
self.log("Charge curve can be entered into apps.yaml:\n" + text)
self.log("Consider setting in HA: input_number.battery_max_rate_scaling: {}".format(round(rate_scaling, 2)))

# If we have the correct data then output it
if rate_scaling > 0:
text = " battery_charge_power_curve:\n"
keys = sorted(final_curve.keys())
keys.reverse()
for key in keys:
if final_curve[key] < 1.0 and final_curve[key] > 0.0:
text += " {} : {}\n".format(key, final_curve[key])
if self.base.battery_charge_power_curve_auto:
self.log("Charge automatically computed as:\n" + text)
else:
self.log("Charge curve can be entered into apps.yaml or set to auto:\n" + text)
rate_scaling = round(rate_scaling, 2)
if rate_scaling != self.base.battery_rate_max_scaling:
self.log(
"Consider setting in HA: input_number.battery_max_rate_scaling: {} - currently {}".format(rate_scaling, self.base.battery_rate_max_scaling)
)
return final_curve
else:
self.log("Note: Found incorrect battery charging curve, maybe try again when you have more data.")
else:
self.log("Note: Found incomplete battery charging curve, maybe try again when you have more data.")
else:
Expand All @@ -1133,6 +1162,7 @@ def find_charge_curve(self):
)
else:
self.log("Note: Can not find battery charge curve, one of the required settings for soc_kw, battery_power and charge_rate are missing from apps.yaml")
return {}

def create_entity(self, entity_name, value, uom=None, device_class="None"):
"""
Expand Down Expand Up @@ -6985,8 +7015,9 @@ def reset(self):
self.future_energy_rates_import = {}
self.future_energy_rates_export = {}
self.load_scaling_dynamic = {}
self.computed_charge_curve = False
self.battery_charge_power_curve = {}
self.battery_charge_power_curve_auto = False
self.computed_charge_curve = False

def optimise_charge_limit_price(
self,
Expand Down Expand Up @@ -10127,8 +10158,11 @@ def fetch_inverter_data(self):
inverter = Inverter(self, id)
inverter.update_status(self.minutes_now)

if id == 0 and not self.computed_charge_curve and not self.battery_charge_power_curve:
inverter.find_charge_curve()
if id == 0 and (not self.computed_charge_curve or self.battery_charge_power_curve_auto) and not self.battery_charge_power_curve:
curve = inverter.find_charge_curve()
if curve:
self.log("Saved computed battery power curve")
self.battery_charge_power_curve = curve
self.computed_charge_curve = True

# As the inverters will run in lockstep, we will initially look at the programming of the first enabled one for the current window setting
Expand Down Expand Up @@ -10354,12 +10388,19 @@ def fetch_config_options(self):
self.inverter_loss = 1.0 - self.get_arg("inverter_loss")
self.inverter_hybrid = self.get_arg("inverter_hybrid")
self.battery_scaling = self.get_arg("battery_scaling", 1.0)
self.battery_charge_power_curve = self.args.get("battery_charge_power_curve", {})
# Check power curve is a dictionary
if not isinstance(self.battery_charge_power_curve, dict):
self.battery_charge_power_curve = {}
self.log("WARN: battery_power_curve is incorrectly configured - ignoring")
self.record_status("battery_power_curve is incorrectly configured - ignoring", had_errors=True)

# Charge curve
if self.args.get("battery_charge_power_curve", "") == "auto":
self.battery_charge_power_curve_auto = True
else:
self.battery_charge_power_curve_auto = False
self.battery_charge_power_curve = self.args.get("battery_charge_power_curve", {})
# Check power curve is a dictionary
if not isinstance(self.battery_charge_power_curve, dict):
self.battery_charge_power_curve = {}
self.log("WARN: battery_power_curve is incorrectly configured - ignoring")
self.record_status("battery_power_curve is incorrectly configured - ignoring", had_errors=True)

self.import_export_scaling = self.get_arg("import_export_scaling", 1.0)
self.best_soc_margin = 0.0
self.best_soc_min = self.get_arg("best_soc_min")
Expand Down