From 44ed6ce412e88c8b8ac27e31e3f3995d2ff2516e Mon Sep 17 00:00:00 2001 From: Trefor Southwell <48591903+springfall2008@users.noreply.github.com> Date: Tue, 30 Jan 2024 17:28:57 +0000 Subject: [PATCH 1/4] Fix issue with charge curve calculation and battery scaling --- apps/predbat/predbat.py | 92 ++++++++++++++++++++++++++++------------- 1 file changed, 63 insertions(+), 29 deletions(-) diff --git a/apps/predbat/predbat.py b/apps/predbat/predbat.py index e1f72ca84..e9a7ec387 100644 --- a/apps/predbat/predbat.py +++ b/apps/predbat/predbat.py @@ -18,7 +18,7 @@ import os import yaml -THIS_VERSION = "v7.15.5" +THIS_VERSION = "v7.15.6" 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" @@ -1011,7 +1011,7 @@ def find_charge_curve(self): clean_increment=False, smoothing=False, divide_by=1.0, - scale=1.0, + scale=self.base.battery_scaling, ) charge_rate = self.base.minute_data( charge_rate_data[0], @@ -1386,6 +1386,12 @@ def update_status(self, minutes_now, quiet=False): def mimic_target_soc(self, current_charge_limit): """ Function to turn on/off charging based on the current SOC and the set charge limit + + Parameters: + current_charge_limit (float): The target SOC (State of Charge) limit for charging. + + Returns: + None """ if self.soc_percent >= float(current_charge_limit): # If current SOC is above Target SOC, turn Grid Charging off @@ -2221,6 +2227,9 @@ def press_and_poll_button(self, entity_id): def rest_readData(self, api="readData"): """ Get inverter status + + :param api: The API endpoint to retrieve data from (default is "readData") + :return: The JSON response containing the inverter status, or None if there was an error """ url = self.rest_api + "/" + api try: @@ -3040,9 +3049,9 @@ async def get_history_async_hook(self, result, entity_id, days): Async function to get history from HA """ if days: - result["data"] = await self.get_history(entity_id=entity_id, days=days) + result['data'] = await self.get_history(entity_id=entity_id, days=days) else: - result["data"] = await self.get_history(entity_id=entity_id) + result['data'] = await self.get_history(entity_id=entity_id) def get_history_async(self, entity_id, days=None): """ @@ -3052,11 +3061,11 @@ def get_history_async(self, entity_id, days=None): task = self.create_task(self.get_history_async_hook(result, entity_id=entity_id, days=days)) cnt = 0 while not task.done() and (cnt < 120): - time.sleep(0.05) + time.sleep(0.05) cnt += 0.05 - if "data" in result: - return result["data"] + if 'data' in result: + return result['data'] else: self.log("Failure to fetch history for {}".format(entity_id)) raise ValueError @@ -3397,24 +3406,35 @@ def dp1(self, value): """ Round to 1 decimal place """ - return round(value * 10) / 10 + return round(value, 1) def dp2(self, value): """ Round to 2 decimal places """ - return round(value * 100) / 100 + return round(value, 2) def dp3(self, value): """ Round to 3 decimal places """ - return round(value * 1000) / 1000 + return round(value, 3) def hit_charge_window(self, charge_window, start, end): + """ + Determines if the given start and end time falls within any of the charge windows. + + Parameters: + charge_window (list): A list of dictionaries representing charge windows, each containing "start" and "end" keys. + start (int): The start time of the interval to check. + end (int): The end time of the interval to check. + + Returns: + int: The index of the charge window that the interval falls within, or -1 if it doesn't fall within any charge window. + """ window_n = 0 for window in charge_window: - if end > window["start"] and start <= window["end"]: + if end > window["start"] and start < window["end"]: return window_n window_n += 1 return -1 @@ -3422,11 +3442,25 @@ def hit_charge_window(self, charge_window, start, end): def in_charge_window(self, charge_window, minute_abs): """ Work out if this minute is within the a charge window + + Parameters: + charge_window (list): A sorted list of dictionaries representing charge windows. + Each dictionary should have "start" and "end" keys + representing the start and end minutes of the window. + + minute_abs (int): The absolute minute value to check. + + Returns: + int: The index of the charge window if the minute is within a window, + otherwise -1. """ window_n = 0 for window in charge_window: if minute_abs >= window["start"] and minute_abs < window["end"]: return window_n + elif window["start"] > minute_abs: + # As windows are sorted, we can stop searching once we've passed the minute + break window_n += 1 return -1 @@ -8363,17 +8397,23 @@ def optimise_charge_windows_manual(self): self.discharge_limits_best[window_n] = 0 def optimise_charge_windows_reset(self, reset_all): - """ - Reset the charge windows to min - """ - if self.charge_window_best and self.calculate_best_charge: - # Set all to max - for window_n in range(0, len(self.charge_window_best)): - if self.charge_window_best[window_n]["start"] < (self.minutes_now + self.end_record): - if reset_all: - self.charge_limit_best[window_n] = 0.0 - else: - self.charge_limit_best[window_n] = self.soc_max + """ + Reset the charge windows to min + + Parameters: + - reset_all (bool): If True, reset all charge windows. If False, reset only the charge windows that are in the record window. + + Returns: + None + """ + if self.charge_window_best and self.calculate_best_charge: + # Set all to max + for window_n in range(0, len(self.charge_window_best)): + if self.charge_window_best[window_n]["start"] < (self.minutes_now + self.end_record): + if reset_all: + self.charge_limit_best[window_n] = 0.0 + else: + self.charge_limit_best[window_n] = self.soc_max def window_as_text(self, windows, percents, ignore_min=False, ignore_max=False): """ @@ -9216,13 +9256,7 @@ def execute_plan(self): status = "Freeze charging" status_extra = " target {}%".format(inverter.soc_percent) else: - if ( - self.set_soc_enable - and self.set_reserve_enable - and self.set_reserve_hold - and ((inverter.soc_percent + 1) >= self.charge_limit_percent_best[0]) - and (inverter.reserve_max >= inverter.soc_percent) - ): + if self.set_soc_enable and self.set_reserve_enable and self.set_reserve_hold and ((inverter.soc_percent + 1) >= self.charge_limit_percent_best[0]) and (inverter.reserve_max >= inverter.soc_percent): status = "Hold charging" inverter.disable_charge_window() disabled_charge_window = True From ec9c9f23991e245c5b69591983d232e89cc27326 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Tue, 30 Jan 2024 17:30:27 +0000 Subject: [PATCH 2/4] [pre-commit.ci lite] apply automatic fixes --- apps/predbat/predbat.py | 54 +++++++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/apps/predbat/predbat.py b/apps/predbat/predbat.py index e9a7ec387..22581d3d5 100644 --- a/apps/predbat/predbat.py +++ b/apps/predbat/predbat.py @@ -1386,10 +1386,10 @@ def update_status(self, minutes_now, quiet=False): def mimic_target_soc(self, current_charge_limit): """ Function to turn on/off charging based on the current SOC and the set charge limit - + Parameters: current_charge_limit (float): The target SOC (State of Charge) limit for charging. - + Returns: None """ @@ -2227,7 +2227,7 @@ def press_and_poll_button(self, entity_id): def rest_readData(self, api="readData"): """ Get inverter status - + :param api: The API endpoint to retrieve data from (default is "readData") :return: The JSON response containing the inverter status, or None if there was an error """ @@ -3049,9 +3049,9 @@ async def get_history_async_hook(self, result, entity_id, days): Async function to get history from HA """ if days: - result['data'] = await self.get_history(entity_id=entity_id, days=days) + result["data"] = await self.get_history(entity_id=entity_id, days=days) else: - result['data'] = await self.get_history(entity_id=entity_id) + result["data"] = await self.get_history(entity_id=entity_id) def get_history_async(self, entity_id, days=None): """ @@ -3061,11 +3061,11 @@ def get_history_async(self, entity_id, days=None): task = self.create_task(self.get_history_async_hook(result, entity_id=entity_id, days=days)) cnt = 0 while not task.done() and (cnt < 120): - time.sleep(0.05) + time.sleep(0.05) cnt += 0.05 - if 'data' in result: - return result['data'] + if "data" in result: + return result["data"] else: self.log("Failure to fetch history for {}".format(entity_id)) raise ValueError @@ -8397,23 +8397,23 @@ def optimise_charge_windows_manual(self): self.discharge_limits_best[window_n] = 0 def optimise_charge_windows_reset(self, reset_all): - """ - Reset the charge windows to min + """ + Reset the charge windows to min - Parameters: - - reset_all (bool): If True, reset all charge windows. If False, reset only the charge windows that are in the record window. + Parameters: + - reset_all (bool): If True, reset all charge windows. If False, reset only the charge windows that are in the record window. - Returns: - None - """ - if self.charge_window_best and self.calculate_best_charge: - # Set all to max - for window_n in range(0, len(self.charge_window_best)): - if self.charge_window_best[window_n]["start"] < (self.minutes_now + self.end_record): - if reset_all: - self.charge_limit_best[window_n] = 0.0 - else: - self.charge_limit_best[window_n] = self.soc_max + Returns: + None + """ + if self.charge_window_best and self.calculate_best_charge: + # Set all to max + for window_n in range(0, len(self.charge_window_best)): + if self.charge_window_best[window_n]["start"] < (self.minutes_now + self.end_record): + if reset_all: + self.charge_limit_best[window_n] = 0.0 + else: + self.charge_limit_best[window_n] = self.soc_max def window_as_text(self, windows, percents, ignore_min=False, ignore_max=False): """ @@ -9256,7 +9256,13 @@ def execute_plan(self): status = "Freeze charging" status_extra = " target {}%".format(inverter.soc_percent) else: - if self.set_soc_enable and self.set_reserve_enable and self.set_reserve_hold and ((inverter.soc_percent + 1) >= self.charge_limit_percent_best[0]) and (inverter.reserve_max >= inverter.soc_percent): + if ( + self.set_soc_enable + and self.set_reserve_enable + and self.set_reserve_hold + and ((inverter.soc_percent + 1) >= self.charge_limit_percent_best[0]) + and (inverter.reserve_max >= inverter.soc_percent) + ): status = "Hold charging" inverter.disable_charge_window() disabled_charge_window = True From b5441c4aecac89fb8aad05e9f6a55bd28ae493c7 Mon Sep 17 00:00:00 2001 From: Trefor Southwell <48591903+springfall2008@users.noreply.github.com> Date: Tue, 30 Jan 2024 17:30:53 +0000 Subject: [PATCH 3/4] Fix format --- apps/predbat/predbat.py | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/apps/predbat/predbat.py b/apps/predbat/predbat.py index 22581d3d5..81fbaa6bf 100644 --- a/apps/predbat/predbat.py +++ b/apps/predbat/predbat.py @@ -1386,10 +1386,10 @@ def update_status(self, minutes_now, quiet=False): def mimic_target_soc(self, current_charge_limit): """ Function to turn on/off charging based on the current SOC and the set charge limit - + Parameters: current_charge_limit (float): The target SOC (State of Charge) limit for charging. - + Returns: None """ @@ -2227,7 +2227,7 @@ def press_and_poll_button(self, entity_id): def rest_readData(self, api="readData"): """ Get inverter status - + :param api: The API endpoint to retrieve data from (default is "readData") :return: The JSON response containing the inverter status, or None if there was an error """ @@ -3049,9 +3049,9 @@ async def get_history_async_hook(self, result, entity_id, days): Async function to get history from HA """ if days: - result["data"] = await self.get_history(entity_id=entity_id, days=days) + result['data'] = await self.get_history(entity_id=entity_id, days=days) else: - result["data"] = await self.get_history(entity_id=entity_id) + result['data'] = await self.get_history(entity_id=entity_id) def get_history_async(self, entity_id, days=None): """ @@ -3061,11 +3061,11 @@ def get_history_async(self, entity_id, days=None): task = self.create_task(self.get_history_async_hook(result, entity_id=entity_id, days=days)) cnt = 0 while not task.done() and (cnt < 120): - time.sleep(0.05) + time.sleep(0.05) cnt += 0.05 - if "data" in result: - return result["data"] + if 'data' in result: + return result['data'] else: self.log("Failure to fetch history for {}".format(entity_id)) raise ValueError @@ -9256,13 +9256,7 @@ def execute_plan(self): status = "Freeze charging" status_extra = " target {}%".format(inverter.soc_percent) else: - if ( - self.set_soc_enable - and self.set_reserve_enable - and self.set_reserve_hold - and ((inverter.soc_percent + 1) >= self.charge_limit_percent_best[0]) - and (inverter.reserve_max >= inverter.soc_percent) - ): + if self.set_soc_enable and self.set_reserve_enable and self.set_reserve_hold and ((inverter.soc_percent + 1) >= self.charge_limit_percent_best[0]) and (inverter.reserve_max >= inverter.soc_percent): status = "Hold charging" inverter.disable_charge_window() disabled_charge_window = True From a3e46a84f9463a9c681e4bc3692733e3eeb809ee Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Tue, 30 Jan 2024 17:32:32 +0000 Subject: [PATCH 4/4] [pre-commit.ci lite] apply automatic fixes --- apps/predbat/predbat.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/apps/predbat/predbat.py b/apps/predbat/predbat.py index 81fbaa6bf..22581d3d5 100644 --- a/apps/predbat/predbat.py +++ b/apps/predbat/predbat.py @@ -1386,10 +1386,10 @@ def update_status(self, minutes_now, quiet=False): def mimic_target_soc(self, current_charge_limit): """ Function to turn on/off charging based on the current SOC and the set charge limit - + Parameters: current_charge_limit (float): The target SOC (State of Charge) limit for charging. - + Returns: None """ @@ -2227,7 +2227,7 @@ def press_and_poll_button(self, entity_id): def rest_readData(self, api="readData"): """ Get inverter status - + :param api: The API endpoint to retrieve data from (default is "readData") :return: The JSON response containing the inverter status, or None if there was an error """ @@ -3049,9 +3049,9 @@ async def get_history_async_hook(self, result, entity_id, days): Async function to get history from HA """ if days: - result['data'] = await self.get_history(entity_id=entity_id, days=days) + result["data"] = await self.get_history(entity_id=entity_id, days=days) else: - result['data'] = await self.get_history(entity_id=entity_id) + result["data"] = await self.get_history(entity_id=entity_id) def get_history_async(self, entity_id, days=None): """ @@ -3061,11 +3061,11 @@ def get_history_async(self, entity_id, days=None): task = self.create_task(self.get_history_async_hook(result, entity_id=entity_id, days=days)) cnt = 0 while not task.done() and (cnt < 120): - time.sleep(0.05) + time.sleep(0.05) cnt += 0.05 - if 'data' in result: - return result['data'] + if "data" in result: + return result["data"] else: self.log("Failure to fetch history for {}".format(entity_id)) raise ValueError @@ -9256,7 +9256,13 @@ def execute_plan(self): status = "Freeze charging" status_extra = " target {}%".format(inverter.soc_percent) else: - if self.set_soc_enable and self.set_reserve_enable and self.set_reserve_hold and ((inverter.soc_percent + 1) >= self.charge_limit_percent_best[0]) and (inverter.reserve_max >= inverter.soc_percent): + if ( + self.set_soc_enable + and self.set_reserve_enable + and self.set_reserve_hold + and ((inverter.soc_percent + 1) >= self.charge_limit_percent_best[0]) + and (inverter.reserve_max >= inverter.soc_percent) + ): status = "Hold charging" inverter.disable_charge_window() disabled_charge_window = True