From ad3caf43f3b270715e4c95330a32a39bd3824d15 Mon Sep 17 00:00:00 2001 From: Trefor Southwell <48591903+springfall2008@users.noreply.github.com> Date: Thu, 1 Feb 2024 08:55:52 +0000 Subject: [PATCH 1/3] Charge curve averaging and auto mode --- apps/predbat/predbat.py | 135 +++++++++++++++++++++++----------------- 1 file changed, 77 insertions(+), 58 deletions(-) diff --git a/apps/predbat/predbat.py b/apps/predbat/predbat.py index b9ba74d2a..c5c0f2adc 100644 --- a/apps/predbat/predbat.py +++ b/apps/predbat/predbat.py @@ -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: @@ -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 @@ -1108,19 +1115,39 @@ 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 maxium 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: @@ -1133,6 +1160,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"): """ @@ -3114,17 +3142,7 @@ def minute_data_import_export(self, now_utc, key, scale=1.0, required_unit=None) if history: import_today = self.minute_data( - history[0], - self.max_days_previous, - now_utc, - "state", - "last_updated", - backwards=True, - smoothing=True, - scale=scale, - clean_increment=True, - accumulate=import_today, - required_unit=required_unit, + history[0], self.max_days_previous, now_utc, "state", "last_updated", backwards=True, smoothing=True, scale=scale, clean_increment=True, accumulate=import_today, required_unit=required_unit ) else: self.log("Error: Unable to fetch history for {}".format(entity_id)) @@ -3299,18 +3317,18 @@ def minute_data( continue # Find and converter units - if required_unit and ("attributes" in item): - if "unit_of_measurement" in item["attributes"]: - unit = item["attributes"]["unit_of_measurement"] + if required_unit and ('attributes' in item): + if 'unit_of_measurement' in item['attributes']: + unit = item['attributes']['unit_of_measurement'] if unit != required_unit: - if required_unit in ["kW", "kWh"] and unit in ["W", "Wh"]: + if required_unit in ['kW', 'kWh'] and unit in ['W', 'Wh']: state = state / 1000.0 - elif required_unit in ["W", "Wh"] and unit in ["kW", "kWh"]: + elif required_unit in ['W', 'Wh'] and unit in ['kW', 'kWh']: state = state * 1000.0 else: # Ignore data in wrong units if we can't converter - continue - + continue + # Divide down the state if required if divide_by: state /= divide_by @@ -6985,8 +7003,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, @@ -8658,17 +8677,7 @@ def fetch_extra_load_forecast(self, now_utc): data = None load_forecast = self.minute_data( - data, - self.forecast_days, - self.midnight_utc, - "energy", - "last_updated", - backwards=False, - clean_increment=False, - smoothing=True, - divide_by=1.0, - scale=1.0, - required_unit="kWh", + data, self.forecast_days, self.midnight_utc, "energy", "last_updated", backwards=False, clean_increment=False, smoothing=True, divide_by=1.0, scale=1.0, required_unit="kWh" ) return load_forecast @@ -10127,8 +10136,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 @@ -10354,12 +10366,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") From 28a34f44b69e3625e0cab9d7a2bb94e6f5c90cc5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Thu, 1 Feb 2024 08:57:17 +0000 Subject: [PATCH 2/3] [pre-commit.ci lite] apply automatic fixes --- apps/predbat/predbat.py | 46 ++++++++++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/apps/predbat/predbat.py b/apps/predbat/predbat.py index c5c0f2adc..0c1f2f096 100644 --- a/apps/predbat/predbat.py +++ b/apps/predbat/predbat.py @@ -1129,7 +1129,7 @@ def find_charge_curve(self): if rate_scaling > 0: for index in final_curve: final_curve[index] = round(final_curve[index] / rate_scaling, 2) - + # If we have the correct data then output it if rate_scaling > 0: text = " battery_charge_power_curve:\n" @@ -1144,7 +1144,9 @@ def find_charge_curve(self): 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)) + 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.") @@ -3142,7 +3144,17 @@ def minute_data_import_export(self, now_utc, key, scale=1.0, required_unit=None) if history: import_today = self.minute_data( - history[0], self.max_days_previous, now_utc, "state", "last_updated", backwards=True, smoothing=True, scale=scale, clean_increment=True, accumulate=import_today, required_unit=required_unit + history[0], + self.max_days_previous, + now_utc, + "state", + "last_updated", + backwards=True, + smoothing=True, + scale=scale, + clean_increment=True, + accumulate=import_today, + required_unit=required_unit, ) else: self.log("Error: Unable to fetch history for {}".format(entity_id)) @@ -3317,18 +3329,18 @@ def minute_data( continue # Find and converter units - if required_unit and ('attributes' in item): - if 'unit_of_measurement' in item['attributes']: - unit = item['attributes']['unit_of_measurement'] + if required_unit and ("attributes" in item): + if "unit_of_measurement" in item["attributes"]: + unit = item["attributes"]["unit_of_measurement"] if unit != required_unit: - if required_unit in ['kW', 'kWh'] and unit in ['W', 'Wh']: + if required_unit in ["kW", "kWh"] and unit in ["W", "Wh"]: state = state / 1000.0 - elif required_unit in ['W', 'Wh'] and unit in ['kW', 'kWh']: + elif required_unit in ["W", "Wh"] and unit in ["kW", "kWh"]: state = state * 1000.0 else: # Ignore data in wrong units if we can't converter - continue - + continue + # Divide down the state if required if divide_by: state /= divide_by @@ -8677,7 +8689,17 @@ def fetch_extra_load_forecast(self, now_utc): data = None load_forecast = self.minute_data( - data, self.forecast_days, self.midnight_utc, "energy", "last_updated", backwards=False, clean_increment=False, smoothing=True, divide_by=1.0, scale=1.0, required_unit="kWh" + data, + self.forecast_days, + self.midnight_utc, + "energy", + "last_updated", + backwards=False, + clean_increment=False, + smoothing=True, + divide_by=1.0, + scale=1.0, + required_unit="kWh", ) return load_forecast @@ -10368,7 +10390,7 @@ def fetch_config_options(self): self.battery_scaling = self.get_arg("battery_scaling", 1.0) # Charge curve - if self.args.get("battery_charge_power_curve", '') == 'auto': + if self.args.get("battery_charge_power_curve", "") == "auto": self.battery_charge_power_curve_auto = True else: self.battery_charge_power_curve_auto = False From d97a65113454806d4b9088bcfd9a678a98930f70 Mon Sep 17 00:00:00 2001 From: Trefor Southwell <48591903+springfall2008@users.noreply.github.com> Date: Thu, 1 Feb 2024 08:59:23 +0000 Subject: [PATCH 3/3] Update predbat.py - typo --- apps/predbat/predbat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/predbat/predbat.py b/apps/predbat/predbat.py index 0c1f2f096..1f7096c38 100644 --- a/apps/predbat/predbat.py +++ b/apps/predbat/predbat.py @@ -1121,7 +1121,7 @@ def find_charge_curve(self): if final_curve_count[index] > 0: final_curve[index] = final_curve[index] / final_curve_count[index] - # Work out the maxium power to use as a scale factor + # Work out the maximum power to use as a scale factor for index in final_curve: rate_scaling = max(final_curve[index], rate_scaling)