From 5daafe8d4004304b479d6ddaa0c6469dd403dd85 Mon Sep 17 00:00:00 2001 From: Trefor Southwell <48591903+springfall2008@users.noreply.github.com> Date: Sat, 3 Feb 2024 18:40:25 +0000 Subject: [PATCH 1/4] Doc strings --- apps/predbat/predbat.py | 48 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/apps/predbat/predbat.py b/apps/predbat/predbat.py index c5164b042..687072a88 100644 --- a/apps/predbat/predbat.py +++ b/apps/predbat/predbat.py @@ -10959,6 +10959,7 @@ def download_predbat_file_from_github(self, tag, filename, new_filename): Downloads a predbat source file from github and returns the contents Args: + tag (str): The tag to download from (e.g. v1.0.0) filename (str): The filename to download (e.g. predbat.py) new_filename (str): The new filename to save the file as Returns: @@ -11036,6 +11037,20 @@ def download_predbat_version(self, version): def select_event(self, event, data, kwargs): """ Catch HA Input select updates + + Parameters: + - event: The event triggered by the input select. + - data: The data associated with the event. + - kwargs: Additional keyword arguments. + + Returns: + None + + Description: + This method is used to handle Home Assistant input select updates. + It extracts the necessary information from the data and performs different actions based on the selected option. + The actions include calling update service, saving and restoring settings, performing manual selection, and exposing configuration. + After performing the actions, it triggers an update by setting update_pending flag to True and plan_valid flag to False. """ service_data = data.get("service_data", {}) value = service_data.get("option", None) @@ -11070,6 +11085,19 @@ def select_event(self, event, data, kwargs): def number_event(self, event, data, kwargs): """ Catch HA Input number updates + + This method is called when there is an update to a Home Assistant input number entity. + It extracts the value and entity ID from the event data and processes it accordingly. + If the entity ID matches any of the entities specified in the CONFIG_ITEMS list, + it logs the entity and value, exposes the configuration item, and updates the pending plan. + + Args: + event (str): The event name. + data (dict): The event data. + kwargs (dict): Additional keyword arguments. + + Returns: + None """ service_data = data.get("service_data", {}) value = service_data.get("value", None) @@ -11090,6 +11118,18 @@ def number_event(self, event, data, kwargs): def switch_event(self, event, data, kwargs): """ Catch HA Switch toggle + + This method is called when a Home Assistant switch is toggled. It handles the logic for updating the state of the switch + and triggering any necessary actions based on the switch state. + + Parameters: + - event (str): The event triggered by the switch toggle. + - data (dict): Additional data associated with the event. + - kwargs (dict): Additional keyword arguments. + + Returns: + - None + """ service = data.get("service", None) service_data = data.get("service_data", {}) @@ -11119,6 +11159,14 @@ def switch_event(self, event, data, kwargs): def get_ha_config(self, name, default): """ Get Home assistant config value, use default if not set + + Parameters: + name (str): The name of the config value to retrieve. + default: The default value to use if the config value is not set. + + Returns: + value: The value of the config if it is set, otherwise the default value. + default: The default value passed as an argument. """ item = self.config_index.get(name) if item and item["name"] == name: From 0ebcfaeaeb571d5c67f49eaacd39ecb7f68c428f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Sat, 3 Feb 2024 18:41:57 +0000 Subject: [PATCH 2/4] [pre-commit.ci lite] apply automatic fixes --- apps/predbat/predbat.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/predbat/predbat.py b/apps/predbat/predbat.py index 687072a88..fde8cd6de 100644 --- a/apps/predbat/predbat.py +++ b/apps/predbat/predbat.py @@ -11047,9 +11047,9 @@ def select_event(self, event, data, kwargs): None Description: - This method is used to handle Home Assistant input select updates. - It extracts the necessary information from the data and performs different actions based on the selected option. - The actions include calling update service, saving and restoring settings, performing manual selection, and exposing configuration. + This method is used to handle Home Assistant input select updates. + It extracts the necessary information from the data and performs different actions based on the selected option. + The actions include calling update service, saving and restoring settings, performing manual selection, and exposing configuration. After performing the actions, it triggers an update by setting update_pending flag to True and plan_valid flag to False. """ service_data = data.get("service_data", {}) From c80a9f2226fa9ac75458a24959031b1b021c5840 Mon Sep 17 00:00:00 2001 From: Trefor Southwell <48591903+springfall2008@users.noreply.github.com> Date: Sat, 3 Feb 2024 18:51:40 +0000 Subject: [PATCH 3/4] Minor refactoing --- apps/predbat/predbat.py | 102 +++++++++++++++++++--------------------- 1 file changed, 49 insertions(+), 53 deletions(-) diff --git a/apps/predbat/predbat.py b/apps/predbat/predbat.py index fde8cd6de..61a3d3754 100644 --- a/apps/predbat/predbat.py +++ b/apps/predbat/predbat.py @@ -33,21 +33,17 @@ # 240v x 100 amps x 3 phases / 1000 to kW / 60 minutes in an hour is the maximum kWh in a 1 minute period MAX_INCREMENT = 240 * 100 * 3 / 1000 / 60 +MINUTE_WATT = 60 * 1000 SIMULATE = False # Debug option, when set don't write to entities but simulate each 30 min period SIMULATE_LENGTH = 23 * 60 # How many periods to simulate, set to 0 for just current INVERTER_TEST = False # Run inverter control self test -""" -Create an array of times -""" -OPTIONS_TIME = [] +# Create an array of times in the day in 5-minute intervals BASE_TIME = datetime.strptime("00:00:00", "%H:%M:%S") -for minute in range(0, 24 * 60, 5): - timeobj = BASE_TIME + timedelta(seconds=minute * 60) - timestr = timeobj.strftime("%H:%M:%S") - OPTIONS_TIME.append(timestr) +OPTIONS_TIME = [((BASE_TIME + timedelta(seconds=minute * 60)).strftime("%H:%M:%S")) for minute in range(0, 24 * 60, 5)] +# List of supported inverters INVERTER_TYPES = {"GE": "GivEnergy", "GS": "Ginlong Solis", "SE": "SolarEdge", "SX4": "Solax Gen4 (Modbus Power Control)", "SF": "Sofar HYD"} # Inverter modes @@ -1096,11 +1092,11 @@ def __init__(self, base, id=0, quiet=False): raise ValueError # Battery rate max charge, discharge (all converted to kW/min) - self.battery_rate_max_charge = min(self.base.get_arg("inverter_limit_charge", self.battery_rate_max_raw, index=self.id), self.battery_rate_max_raw) / 60.0 / 1000.0 - self.battery_rate_max_discharge = min(self.base.get_arg("inverter_limit_discharge", self.battery_rate_max_raw, index=self.id), self.battery_rate_max_raw) / 60.0 / 1000.0 + self.battery_rate_max_charge = min(self.base.get_arg("inverter_limit_charge", self.battery_rate_max_raw, index=self.id), self.battery_rate_max_raw) / MINUTE_WATT + self.battery_rate_max_discharge = min(self.base.get_arg("inverter_limit_discharge", self.battery_rate_max_raw, index=self.id), self.battery_rate_max_raw) / MINUTE_WATT self.battery_rate_max_charge_scaled = self.battery_rate_max_charge * self.base.battery_rate_max_scaling self.battery_rate_max_discharge_scaled = self.battery_rate_max_discharge * self.base.battery_rate_max_scaling_discharge - self.battery_rate_min = min(self.base.get_arg("inverter_battery_rate_min", 0, index=self.id), self.battery_rate_max_raw) / 60.0 / 1000.0 + self.battery_rate_min = min(self.base.get_arg("inverter_battery_rate_min", 0, index=self.id), self.battery_rate_max_raw) / MINUTE_WATT # Convert inverter time into timestamp if ivtime: @@ -1164,9 +1160,9 @@ def __init__(self, base, id=0, quiet=False): # Max inverter rate override if "inverter_limit" in self.base.args: - self.inverter_limit = self.base.get_arg("inverter_limit", self.inverter_limit, index=self.id) / (1000 * 60.0) + self.inverter_limit = self.base.get_arg("inverter_limit", self.inverter_limit, index=self.id) / MINUTE_WATT if "export_limit" in self.base.args: - self.export_limit = self.base.get_arg("export_limit", self.inverter_limit, index=self.id) / (1000 * 60.0) + self.export_limit = self.base.get_arg("export_limit", self.inverter_limit, index=self.id) / MINUTE_WATT # Can't export more than the inverter limit self.export_limit = min(self.export_limit, self.inverter_limit) @@ -1180,7 +1176,7 @@ def __init__(self, base, id=0, quiet=False): self.base.dp2(self.battery_rate_max_raw), self.base.dp2(self.battery_rate_max_charge * 60.0), self.base.dp2(self.battery_rate_max_discharge * 60.0), - self.base.dp2(self.battery_rate_min * 60.0 * 1000.0), + self.base.dp2(self.battery_rate_min * MINUTE_WATT), self.base.dp2(self.inverter_limit * 60), self.base.dp2(self.export_limit * 60), self.reserve_percent, @@ -1204,8 +1200,8 @@ def __init__(self, base, id=0, quiet=False): self.base.args["charge_limit"] = self.create_entity("charge_limit", 100, device_class="battery", uom="%") if self.inv_output_charge_control != "power": - self.base.args["charge_rate"] = self.create_entity("charge_rate", int(self.battery_rate_max_charge * 60 * 1000), uom="W", device_class="power") - self.base.args["discharge_rate"] = self.create_entity("discharge_rate", int(self.battery_rate_max_discharge * 60 * 1000), uom="W", device_class="power") + self.base.args["charge_rate"] = self.create_entity("charge_rate", int(self.battery_rate_max_charge * MINUTE_WATT), uom="W", device_class="power") + self.base.args["discharge_rate"] = self.create_entity("discharge_rate", int(self.battery_rate_max_discharge * MINUTE_WATT), uom="W", device_class="power") if not self.inv_has_ge_inverter_mode: self.base.args["inverter_mode"] = self.create_entity("inverter_mode", "Eco") @@ -1234,9 +1230,9 @@ def find_charge_curve(self, discharge): final_curve_count = {} if discharge: - max_power = int(self.battery_rate_max_discharge * 1000.0 * 60.0) + max_power = int(self.battery_rate_max_discharge * MINUTE_WATT) else: - max_power = int(self.battery_rate_max_charge * 1000.0 * 60.0) + max_power = int(self.battery_rate_max_charge * MINUTE_WATT) if soc_kwh_sensor and charge_rate_sensor and battery_power_sensor and predbat_status_sensor: battery_power_sensor = battery_power_sensor.replace("number.", "sensor.") # Workaround as old template had number. @@ -1547,13 +1543,13 @@ def update_status(self, minutes_now, quiet=False): if self.rest_data: self.charge_enable_time = self.rest_data["Control"]["Enable_Charge_Schedule"] == "enable" self.discharge_enable_time = self.rest_data["Control"]["Enable_Discharge_Schedule"] == "enable" - self.charge_rate_now = self.rest_data["Control"]["Battery_Charge_Rate"] / 1000.0 / 60.0 - self.discharge_rate_now = self.rest_data["Control"]["Battery_Discharge_Rate"] / 1000.0 / 60.0 + self.charge_rate_now = self.rest_data["Control"]["Battery_Charge_Rate"] / MINUTE_WATT + self.discharge_rate_now = self.rest_data["Control"]["Battery_Discharge_Rate"] / MINUTE_WATT else: self.charge_enable_time = self.base.get_arg("scheduled_charge_enable", "on", index=self.id) == "on" self.discharge_enable_time = self.base.get_arg("scheduled_discharge_enable", "off", index=self.id) == "on" - self.charge_rate_now = self.base.get_arg("charge_rate", index=self.id, default=2600.0) / 1000.0 / 60.0 - self.discharge_rate_now = self.base.get_arg("discharge_rate", index=self.id, default=2600.0) / 1000.0 / 60.0 + self.charge_rate_now = self.base.get_arg("charge_rate", index=self.id, default=2600.0) / MINUTE_WATT + self.discharge_rate_now = self.base.get_arg("discharge_rate", index=self.id, default=2600.0) / MINUTE_WATT # Scale charge and discharge rates with battery scaling self.charge_rate_now = max(self.charge_rate_now * self.base.battery_rate_max_scaling, self.battery_rate_min) @@ -1598,8 +1594,8 @@ def update_status(self, minutes_now, quiet=False): self.id, self.base.dp2(self.soc_kw), self.soc_percent, - self.charge_rate_now * 60 * 1000, - self.discharge_rate_now * 60 * 1000.0, + self.charge_rate_now * MINUTE_WATT, + self.discharge_rate_now * MINUTE_WATT, self.battery_power, self.battery_voltage, ) @@ -1763,7 +1759,7 @@ def mimic_target_soc(self, current_charge_limit): # If we drop below the target, turn grid charging back on and make sure the charge current is correct self.alt_charge_discharge_enable("charge", True, grid=True, timed=False) if self.inv_output_charge_control == "current": - self.set_current_from_power("charge", self.battery_rate_max_charge * 60 * 1000) + self.set_current_from_power("charge", self.battery_rate_max_charge * MINUTE_WATT) self.base.log( f"Current SOC {self.soc_percent}% is less than Target SOC {current_charge_limit}. Grid charging enabled with charge current set to {self.base.get_arg('timed_charge_current', index=self.id, default=65):0.2f}" ) @@ -2374,9 +2370,9 @@ def alt_charge_discharge_enable(self, direction, enable, grid=True, timed=False) # MQTT if direction == "charge" and enable: - self.mqtt_message("set/charge", payload=int(self.battery_rate_max_charge * 60 * 1000)) + self.mqtt_message("set/charge", payload=int(self.battery_rate_max_charge * MINUTE_WATT)) elif direction == "discharge" and enable: - self.mqtt_message("set/discharge", payload=int(self.battery_rate_max_discharge * 60 * 1000)) + self.mqtt_message("set/discharge", payload=int(self.battery_rate_max_discharge * MINUTE_WATT)) else: self.mqtt_message("set/auto", payload="true") @@ -9126,12 +9122,12 @@ def balance_inverters(self): total_battery_power += inverter.battery_power total_pv_power += inverter.pv_power total_load_power += inverter.load_power - battery_max_rates.append(inverter.battery_rate_max_discharge * 60 * 1000.0) - total_max_rate += inverter.battery_rate_max_discharge * 60 * 1000.0 - charge_rates.append(inverter.charge_rate_now * 60 * 1000.0) - total_charge_rates += inverter.charge_rate_now * 60 * 1000.0 - discharge_rates.append(inverter.discharge_rate_now * 60 * 1000.0) - total_discharge_rates += inverter.discharge_rate_now * 60 * 1000.0 + battery_max_rates.append(inverter.battery_rate_max_discharge * MINUTE_WATT) + total_max_rate += inverter.battery_rate_max_discharge * MINUTE_WATT + charge_rates.append(inverter.charge_rate_now * MINUTE_WATT) + total_charge_rates += inverter.charge_rate_now * MINUTE_WATT + discharge_rates.append(inverter.discharge_rate_now * MINUTE_WATT) + total_discharge_rates += inverter.discharge_rate_now * MINUTE_WATT self.log( "BALANCE: socs {} reserves {} battery_powers {} total {} battery_max_rates {} charge_rates {} pv_power {} load_power {} total {} discharge_rates {} total {}".format( socs, @@ -9225,11 +9221,11 @@ def balance_inverters(self): for id in range(num_inverters): if not balance_reset_charge.get(id, False) and total_charge_rates != 0 and charge_rates[id] == 0: - self.log("BALANCE: Inverter {} reset charge rate to {} now balanced".format(id, inverter.battery_rate_max_charge * 60 * 1000)) - inverters[id].adjust_charge_rate(inverter.battery_rate_max_charge * 60 * 1000, notify=False) + self.log("BALANCE: Inverter {} reset charge rate to {} now balanced".format(id, inverter.battery_rate_max_charge * MINUTE_WATT)) + inverters[id].adjust_charge_rate(inverter.battery_rate_max_charge * MINUTE_WATT, notify=False) if not balance_reset_discharge.get(id, False) and total_discharge_rates != 0 and discharge_rates[id] == 0: - self.log("BALANCE: Inverter {} reset discharge rate to {} now balanced".format(id, inverter.battery_rate_max_discharge * 60 * 1000)) - inverters[id].adjust_discharge_rate(inverter.battery_rate_max_discharge * 60 * 1000, notify=False) + self.log("BALANCE: Inverter {} reset discharge rate to {} now balanced".format(id, inverter.battery_rate_max_discharge * MINUTE_WATT)) + inverters[id].adjust_discharge_rate(inverter.battery_rate_max_discharge * MINUTE_WATT, notify=False) self.log("BALANCE: Completed this run") @@ -9260,10 +9256,10 @@ def find_charge_rate(self, minutes_now, soc, window, target_soc, max_rate, quiet min_rate = charge_left / minutes_left # Apply the curve at each rate to pick one that works - rate_w = max_rate * 60.0 * 1000.0 + rate_w = max_rate * MINUTE_WATT best_rate = max_rate while rate_w >= 400: - rate = rate_w / 60.0 / 1000.0 + rate = rate_w / MINUTE_WATT if rate >= min_rate: charge_now = soc minute = 0 @@ -9278,7 +9274,7 @@ def find_charge_rate(self, minutes_now, soc, window, target_soc, max_rate, quiet if not quiet: self.log( "Find charge rate now {} soc {} window {} target_soc {} max_rate {} min_rate {} returns {}".format( - minutes_now, soc, window, target_soc, int(max_rate * 60.0 * 1000.0), int(min_rate * 60.0 * 1000.0), int(best_rate * 60.0 * 1000.0) + minutes_now, soc, window, target_soc, int(max_rate * MINUTE_WATT), int(min_rate * MINUTE_WATT), int(best_rate * MINUTE_WATT) ) ) return best_rate @@ -9591,7 +9587,7 @@ def reset_inverter(self): ) ) if self.set_charge_window or (self.inverter_needs_reset_force in ["set_read_only", "mode"]): - inverter.adjust_charge_rate(inverter.battery_rate_max_charge * 60.0 * 1000.0) + inverter.adjust_charge_rate(inverter.battery_rate_max_charge * MINUTE_WATT) if self.set_read_only and self.set_charge_window: # Only reset charge window if we are no longer controller charge window inverter.disable_charge_window() @@ -9599,7 +9595,7 @@ def reset_inverter(self): if self.set_charge_window or self.set_discharge_window or (self.inverter_needs_reset_force in ["set_read_only", "mode"]): inverter.adjust_reserve(0) if self.set_discharge_window or (self.inverter_needs_reset_force in ["set_read_only", "mode"]): - inverter.adjust_discharge_rate(inverter.battery_rate_max_discharge * 60 * 1000) + inverter.adjust_discharge_rate(inverter.battery_rate_max_discharge * MINUTE_WATT) inverter.adjust_force_discharge(False) self.inverter_needs_reset = False @@ -9675,7 +9671,7 @@ def execute_plan(self): charge_rate = self.find_charge_rate( self.minutes_now, inverter.soc_kw, window, self.charge_limit_percent_best[0] * inverter.soc_max / 100.0, inverter.battery_rate_max_charge, quiet=False ) - inverter.adjust_charge_rate(int(charge_rate * 60.0 * 1000.0)) + inverter.adjust_charge_rate(int(charge_rate * MINUTE_WATT)) # Do we disable discharge during charge? if not self.set_discharge_during_charge and (inverter.soc_percent >= self.charge_limit_percent_best[0]): @@ -9772,7 +9768,7 @@ def execute_plan(self): if (self.minutes_now >= minutes_start) and (self.minutes_now < minutes_end) and (self.discharge_limits_best[0] < 100.0): if not self.set_discharge_freeze_only and ((self.soc_kw - PREDICT_STEP * inverter.battery_rate_max_discharge_scaled) >= discharge_soc): self.log("Discharging now - current SOC {} and target {}".format(self.soc_kw, self.dp2(discharge_soc))) - inverter.adjust_discharge_rate(inverter.battery_rate_max_discharge * 60 * 1000) + inverter.adjust_discharge_rate(inverter.battery_rate_max_discharge * MINUTE_WATT) inverter.adjust_force_discharge(True, discharge_start_time, discharge_end_time) resetDischarge = False isDischarging = True @@ -9817,14 +9813,14 @@ def execute_plan(self): if self.set_discharge_freeze and not isCharging: # In discharge freeze mode we disable charging during discharge slots, so turn it back on otherwise - inverter.adjust_charge_rate(inverter.battery_rate_max_charge * 60 * 1000) + inverter.adjust_charge_rate(inverter.battery_rate_max_charge * MINUTE_WATT) elif self.set_discharge_window: self.log("Setting ECO mode as no discharge window planned") inverter.adjust_force_discharge(False) resetReserve = True if self.set_discharge_freeze and not isCharging: # In discharge freeze mode we disable charging during discharge slots, so turn it back on otherwise - inverter.adjust_charge_rate(inverter.battery_rate_max_charge * 60 * 1000) + inverter.adjust_charge_rate(inverter.battery_rate_max_charge * MINUTE_WATT) # Car charging from battery disable? if not self.car_charging_from_battery: @@ -9849,7 +9845,7 @@ def execute_plan(self): # Reset discharge rate? if resetDischarge: - inverter.adjust_discharge_rate(inverter.battery_rate_max_discharge * 60 * 1000) + inverter.adjust_discharge_rate(inverter.battery_rate_max_discharge * MINUTE_WATT) # Set the SOC just before or within the charge window if self.set_soc_enable: @@ -10512,7 +10508,7 @@ def fetch_inverter_data(self): self.soc_kw, self.charge_rate_now * 60, self.discharge_rate_now * 60, - self.battery_rate_min * 60 * 1000, + self.battery_rate_min * MINUTE_WATT, self.dp2(self.inverter_limit * 60), self.dp2(self.export_limit * 60), 100 - int(self.battery_loss * 100), @@ -10810,8 +10806,8 @@ def fetch_config_options(self): self.iboost_gas_scale = self.get_arg("iboost_gas_scale") self.iboost_max_energy = self.get_arg("iboost_max_energy") - self.iboost_max_power = self.get_arg("iboost_max_power") / 1000 / 60.0 - self.iboost_min_power = self.get_arg("iboost_min_power") / 1000 / 60.0 + self.iboost_max_power = self.get_arg("iboost_max_power") / MINUTE_WATT + 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_next = self.iboost_today @@ -11047,9 +11043,9 @@ def select_event(self, event, data, kwargs): None Description: - This method is used to handle Home Assistant input select updates. - It extracts the necessary information from the data and performs different actions based on the selected option. - The actions include calling update service, saving and restoring settings, performing manual selection, and exposing configuration. + This method is used to handle Home Assistant input select updates. + It extracts the necessary information from the data and performs different actions based on the selected option. + The actions include calling update service, saving and restoring settings, performing manual selection, and exposing configuration. After performing the actions, it triggers an update by setting update_pending flag to True and plan_valid flag to False. """ service_data = data.get("service_data", {}) From d2b492146ec0535831becb163d4495fbc06fb1e4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Sat, 3 Feb 2024 18:52:24 +0000 Subject: [PATCH 4/4] [pre-commit.ci lite] apply automatic fixes --- apps/predbat/predbat.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/predbat/predbat.py b/apps/predbat/predbat.py index 61a3d3754..66399a932 100644 --- a/apps/predbat/predbat.py +++ b/apps/predbat/predbat.py @@ -11043,9 +11043,9 @@ def select_event(self, event, data, kwargs): None Description: - This method is used to handle Home Assistant input select updates. - It extracts the necessary information from the data and performs different actions based on the selected option. - The actions include calling update service, saving and restoring settings, performing manual selection, and exposing configuration. + This method is used to handle Home Assistant input select updates. + It extracts the necessary information from the data and performs different actions based on the selected option. + The actions include calling update service, saving and restoring settings, performing manual selection, and exposing configuration. After performing the actions, it triggers an update by setting update_pending flag to True and plan_valid flag to False. """ service_data = data.get("service_data", {})