diff --git a/apps/predbat/config/apps.yaml b/apps/predbat/config/apps.yaml index 7c55ec3f..712891ee 100644 --- a/apps/predbat/config/apps.yaml +++ b/apps/predbat/config/apps.yaml @@ -12,6 +12,10 @@ pred_bat: # Timezone to work in timezone: Europe/London + # Number of threads to use in plan calculation + # Can be auto for automatic, 0 for off or values 1-N for a fixed number + threads: auto + # XXX: This is a configuration template, delete this line once you edit your configuration template: True diff --git a/apps/predbat/predbat.py b/apps/predbat/predbat.py index 23dc5bff..dff94447 100644 --- a/apps/predbat/predbat.py +++ b/apps/predbat/predbat.py @@ -20,7 +20,7 @@ import pytz import requests import yaml -from multiprocessing import Pool +from multiprocessing import Pool, cpu_count # Only assign globals once to avoid re-creating them with processes are forked if not "PRED_GLOBAL" in globals(): @@ -1129,6 +1129,25 @@ def find_charge_rate(model, minutes_now, soc, window, target_soc, max_rate, quie return max_rate +""" +Used to mimic threads when they are disabled +""" + + +class DummyThread: + def __init__(self, result): + """ + Store the data into the class + """ + self.result = result + + def get(self): + """ + Return the result + """ + return self.result + + class Prediction: """ Class to hold prediction input and output data and the run function @@ -8159,6 +8178,37 @@ def optimise_charge_limit_price( ) return best_limits, best_discharge, best_price_charge, best_price_discharge, best_metric, best_cost + def launch_run_prediction_charge(self, loop_soc, window_n, charge_limit, charge_window, discharge_window, discharge_limits, pv10, all_n, end_record): + """ + Launch a thread to run a prediction + """ + if self.pool: + han = self.pool.apply_async( + self.prediction.thread_run_prediction_charge, (loop_soc, window_n, charge_limit, charge_window, discharge_window, discharge_limits, pv10, all_n, end_record) + ) + else: + han = DummyThread( + self.prediction.thread_run_prediction_charge(loop_soc, window_n, charge_limit, charge_window, discharge_window, discharge_limits, pv10, all_n, end_record) + ) + return han + + def launch_run_prediction_discharge(self, this_discharge_limit, start, window_n, try_charge_limit, charge_window, try_discharge_window, try_discharge, pv10, all_n, end_record): + """ + Launch a thread to run a prediction + """ + if self.pool: + han = self.pool.apply_async( + self.prediction.thread_run_prediction_discharge, + (this_discharge_limit, start, window_n, try_charge_limit, charge_window, try_discharge_window, try_discharge, pv10, all_n, end_record), + ) + else: + han = DummyThread( + self.prediction.thread_run_prediction_discharge( + this_discharge_limit, start, window_n, try_charge_limit, charge_window, try_discharge_window, try_discharge, pv10, all_n, end_record + ) + ) + return han + def optimise_charge_limit( self, window_n, @@ -8204,27 +8254,10 @@ def optimise_charge_limit( hans = [] all_max_soc = 0 all_min_soc = self.soc_max - hans.append( - self.pool.apply_async( - self.prediction.thread_run_prediction_charge, (loop_soc, window_n, charge_limit, charge_window, discharge_window, discharge_limits, False, all_n, end_record) - ) - ) - hans.append( - self.pool.apply_async( - self.prediction.thread_run_prediction_charge, (loop_soc, window_n, charge_limit, charge_window, discharge_window, discharge_limits, True, all_n, end_record) - ) - ) - hans.append( - self.pool.apply_async( - self.prediction.thread_run_prediction_charge, - (best_soc_min, window_n, charge_limit, charge_window, discharge_window, discharge_limits, False, all_n, end_record), - ) - ) - hans.append( - self.pool.apply_async( - self.prediction.thread_run_prediction_charge, (best_soc_min, window_n, charge_limit, charge_window, discharge_window, discharge_limits, True, all_n, end_record) - ) - ) + hans.append(self.launch_run_prediction_charge(loop_soc, window_n, charge_limit, charge_window, discharge_window, discharge_limits, False, all_n, end_record)) + hans.append(self.launch_run_prediction_charge(loop_soc, window_n, charge_limit, charge_window, discharge_window, discharge_limits, True, all_n, end_record)) + hans.append(self.launch_run_prediction_charge(best_soc_min, window_n, charge_limit, charge_window, discharge_window, discharge_limits, False, all_n, end_record)) + hans.append(self.launch_run_prediction_charge(best_soc_min, window_n, charge_limit, charge_window, discharge_window, discharge_limits, True, all_n, end_record)) id = 0 for han in hans: metricmid, import_kwh_battery, import_kwh_house, export_kwh, soc_min, soc, soc_min_minute, battery_cycle, metric_keep, final_iboost, min_soc, max_soc = han.get() @@ -8323,13 +8356,9 @@ def optimise_charge_limit( results10 = [] for try_soc in try_socs: if try_soc not in resultmid: - hanres = self.pool.apply_async( - self.prediction.thread_run_prediction_charge, (try_soc, window_n, charge_limit, charge_window, discharge_window, discharge_limits, False, all_n, end_record) - ) + hanres = self.launch_run_prediction_charge(try_soc, window_n, charge_limit, charge_window, discharge_window, discharge_limits, False, all_n, end_record) results.append(hanres) - hanres10 = self.pool.apply_async( - self.prediction.thread_run_prediction_charge, (try_soc, window_n, charge_limit, charge_window, discharge_window, discharge_limits, True, all_n, end_record) - ) + hanres10 = self.launch_run_prediction_charge(try_soc, window_n, charge_limit, charge_window, discharge_window, discharge_limits, True, all_n, end_record) results10.append(hanres10) # Get results from sims if we simulated them @@ -8517,16 +8546,16 @@ def optimise_discharge( this_discharge_limit = max(calc_percent_limit(max(self.best_soc_min, self.reserve), self.soc_max), this_discharge_limit) try_options.append([start, this_discharge_limit]) - hanres = self.pool.apply_async( - self.prediction.thread_run_prediction_discharge, - (this_discharge_limit, start, window_n, try_charge_limit, charge_window, try_discharge_window, try_discharge, False, all_n, end_record), + results.append( + self.launch_run_prediction_discharge( + this_discharge_limit, start, window_n, try_charge_limit, charge_window, try_discharge_window, try_discharge, False, all_n, end_record + ) ) - hanres10 = self.pool.apply_async( - self.prediction.thread_run_prediction_discharge, - (this_discharge_limit, start, window_n, try_charge_limit, charge_window, try_discharge_window, try_discharge, True, all_n, end_record), + results10.append( + self.launch_run_prediction_discharge( + this_discharge_limit, start, window_n, try_charge_limit, charge_window, try_discharge_window, try_discharge, True, all_n, end_record + ) ) - results.append(hanres) - results10.append(hanres10) # Get results from sims try_results = [] @@ -9918,7 +9947,15 @@ def calculate_plan(self, recompute=True): # Create pool if not self.pool: - self.pool = Pool(processes=8) + threads = self.get_arg("threads", "auto") + if threads == "auto": + self.log("Creating pool of {} processes to match your CPU count".format(cpu_count())) + self.pool = Pool(processes=cpu_count()) + elif threads: + self.log("Creating pool of {} processes as per apps.yaml".format(int(threads))) + self.pool = Pool(processes=int(threads)) + else: + self.log("Not using threading as threads is set to 0 in apps.yaml") # Simulate current settings to get initial data metric, import_kwh_battery, import_kwh_house, export_kwh, soc_min, soc, soc_min_minute, battery_cycle, metric_keep, final_iboost = self.run_prediction( diff --git a/docs/apps-yaml.md b/docs/apps-yaml.md index 5e64d6fa..cd1899af 100644 --- a/docs/apps-yaml.md +++ b/docs/apps-yaml.md @@ -44,6 +44,13 @@ Basic configuration items - **template** - Initially set to True, this is used to stop Predbat from operating until you have finished configuring your apps.yaml. Once you have made all other required changes to apps.yaml this line should be deleted or commented out. +- **threads** - If defined sets the number of threads to use during plan calculation, the default is 'auto' which will use the same number of threads as +you have CPUs in your system. +Valid values are: + - 'auto' - Use the same number of threads as your CPU count + - '0' - Don't use threads - disabled + - 'N' - Use N threads, recommended values are between 2 and 8 + - **notify_devices** - A list of device names to notify when Predbat sends a notification. The default is just 'notify' which contacts all mobile devices - **days_previous** - Predbat needs to know what your likely future house load will be to set and manage the battery level to support it. diff --git a/docs/customisation.md b/docs/customisation.md index e7fc001f..9a681430 100644 --- a/docs/customisation.md +++ b/docs/customisation.md @@ -92,6 +92,9 @@ quite CPU intensive and provides very little improvement for most systems. If you have performance problems turn **switch.predbat_calculate_fast_plan** (_expert mode_) On to help reduce your CPU load. +The number of threads you use can change your performance, you can set **threads** in apps.yaml to 0 to disable threading +if you don't have multiple CPUs available or set it to 'auto' (the default) to use one thread per CPU. + ## Battery loss options **input_number.predbat_battery_loss** is an assumed percentage figure for energy lost when charging the battery, the default 0.05 is 5%.