From 0a4b37d026f1395cecef82ab98d503890058012e Mon Sep 17 00:00:00 2001 From: Trefor Southwell <48591903+springfall2008@users.noreply.github.com> Date: Sat, 20 Jan 2024 16:29:42 +0000 Subject: [PATCH 1/7] New saving session scaling feature - Saving session load scaling feature - Load scaling in manual rates - Change manual override to 18 hours --- apps/predbat/predbat.py | 63 ++++++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 29 deletions(-) diff --git a/apps/predbat/predbat.py b/apps/predbat/predbat.py index 3dc13a4da..41247f6ac 100644 --- a/apps/predbat/predbat.py +++ b/apps/predbat/predbat.py @@ -114,6 +114,17 @@ "icon": "mdi:multiplication", "default": 1.1, }, + { + "name": "load_scaling_saving", + "friendly_name": "Load Scaling for saving sessions", + "type": "input_number", + "min": 0, + "max": 2.0, + "step": 0.01, + "unit": "multiple", + "icon": "mdi:multiplication", + "default": 1.0, + }, { "name": "battery_rate_max_scaling", "friendly_name": "Battery rate max scaling", @@ -3639,7 +3650,7 @@ def get_cloud_factor(self, minutes_now, pv_data, pv_data10): else: return None - def step_data_history(self, item, minutes_now, forward, step=PREDICT_STEP, scale_today=1.0, type_load=False, load_forecast={}, cloud_factor=None): + def step_data_history(self, item, minutes_now, forward, step=PREDICT_STEP, scale_today=1.0, type_load=False, load_forecast={}, cloud_factor=None, load_scaling_dynamic=None): """ Create cached step data for historical array """ @@ -3649,6 +3660,10 @@ def step_data_history(self, item, minutes_now, forward, step=PREDICT_STEP, scale value = 0 minute_absolute = minute + minutes_now + scaling_dynamic = 1.0 + if load_scaling_dynamic: + scaling_dynamic = load_scaling_dynamic.get(minute_absolute, scaling_dynamic) + # Reset in-day adjustment for tomorrow if (minute + minutes_now) > 24 * 60: scale_today = 1.0 @@ -3668,7 +3683,7 @@ def step_data_history(self, item, minutes_now, forward, step=PREDICT_STEP, scale if load_forecast: for offset in range(0, step): load_extra += self.get_from_incrementing(load_forecast, minute_absolute, backwards=False) - values[minute] = value * scale_today + load_extra + values[minute] = (value * scale_today + load_extra) * scaling_dynamic # Simple cloud model keeps the same generation but brings PV generation up and down every 5 minutes if cloud_factor and cloud_factor > 0: @@ -4841,31 +4856,11 @@ def find_charge_window(self, rates, minute, threshold_rate, find_high): # If combine is disabled, for import slots make them all N minutes so we can select some not all rate_low_end = minute break - if ( - (not find_high) - and ( - rate_low_start in self.manual_charge_times - or rate_low_start in self.manual_idle_times - or minute in self.manual_charge_times - or minute in self.manual_idle_times - ) - and (rate_low_start >= 0) - and ((minute - rate_low_start) >= 30) - ): + if (not find_high) and (rate_low_start in self.manual_charge_times or rate_low_start in self.manual_idle_times or minute in self.manual_charge_times or minute in self.manual_idle_times) and (rate_low_start >= 0) and ((minute - rate_low_start) >= 30): # Manual import slot rate_low_end = minute break - if ( - find_high - and ( - rate_low_start in self.manual_discharge_times - or rate_low_start in self.manual_idle_times - or minute in self.manual_discharge_times - or minute in self.manual_idle_times - ) - and (rate_low_start >= 0) - and ((minute - rate_low_start) >= 30) - ): + if find_high and (rate_low_start in self.manual_discharge_times or rate_low_start in self.manual_idle_times or minute in self.manual_discharge_times or minute in self.manual_idle_times) and (rate_low_start >= 0) and ((minute - rate_low_start) >= 30): # Manual export slot rate_low_end = minute break @@ -4918,6 +4913,7 @@ def basic_rates(self, info, rtype, prev=None): start_str = self.resolve_arg("start", start_str, "00:00:00") end_str = this_rate.get("end", "00:00:00") end_str = self.resolve_arg("end", end_str, "00:00:00") + load_scaling = this_rate.get("load_scaling", None) if start_str.count(":") < 2: start_str += ":00" @@ -4978,8 +4974,12 @@ def basic_rates(self, info, rtype, prev=None): for minute in range(start_minutes, end_minutes): if (not date) or (minute >= 0 and minute < max_minute): rates[minute % max_minute] = rate + if load_scaling is not None: + self.load_scaling_dynamic[minute % max_minute] = load_scaling if not date and not prev: rates[(minute % max_minute) + max_minute] = rate + if load_scaling is not None: + self.load_scaling_dynamic[(minute % max_minute) + max_minute] = load_scaling return rates @@ -5132,6 +5132,7 @@ def load_saving_slot(self, octopus_saving_slots, export=False): self.rate_export[minute] += rate else: self.rate_import[minute] += rate + self.load_scaling_dynamic[minute] = self.load_scaling_saving def load_octopus_slots(self, octopus_slots): """ @@ -6627,6 +6628,7 @@ def reset(self): self.metric_cloud_coverage = 0.0 self.future_energy_rates_import = {} self.future_energy_rates_export = {} + self.load_scaling_dynamic = {} def optimise_charge_limit_price( self, @@ -8627,10 +8629,10 @@ def calculate_plan(self, recompute=True): # Created optimised step data self.metric_cloud_coverage = self.get_cloud_factor(self.minutes_now, self.pv_forecast_minute, self.pv_forecast_minute10) load_minutes_step = self.step_data_history( - self.load_minutes, self.minutes_now, forward=False, scale_today=self.load_inday_adjustment, type_load=True, load_forecast=self.load_forecast + self.load_minutes, self.minutes_now, forward=False, scale_today=self.load_inday_adjustment, type_load=True, load_forecast=self.load_forecast, load_scaling_dynamic=self.load_scaling_dynamic ) load_minutes_step10 = self.step_data_history( - self.load_minutes, self.minutes_now, forward=False, scale_today=self.load_inday_adjustment * self.load_scaling10, type_load=True, load_forecast=self.load_forecast + self.load_minutes, self.minutes_now, forward=False, scale_today=self.load_inday_adjustment * self.load_scaling10, type_load=True, load_forecast=self.load_forecast, load_scaling_dynamic=self.load_scaling_dynamic ) pv_forecast_minute_step = self.step_data_history(self.pv_forecast_minute, self.minutes_now, forward=True, cloud_factor=self.metric_cloud_coverage) pv_forecast_minute10_step = self.step_data_history(self.pv_forecast_minute10, self.minutes_now, forward=True, cloud_factor=self.metric_cloud_coverage) @@ -9255,6 +9257,7 @@ def fetch_sensor_data(self): self.load_forecast = {} self.pv_forecast_minute = {} self.pv_forecast_minute10 = {} + self.load_scaling_dynamic = {} # Iboost load data if self.iboost_enable: @@ -9772,6 +9775,7 @@ def manual_times(self, config_item): """ time_overrides = [] minutes_now = int(self.minutes_now / 30) * 30 + manual_time_max = 18 * 60 # Deconstruct the value into a list of minutes item = self.config_index.get(config_item) @@ -9788,7 +9792,7 @@ def manual_times(self, config_item): minutes = start_time.hour * 60 + start_time.minute if minutes < minutes_now: minutes += 24 * 60 - if (minutes - minutes_now) < 12 * 60: + if (minutes - minutes_now) < manual_time_max: time_overrides.append(minutes) # Reconstruct the list in order based on minutes @@ -9800,7 +9804,7 @@ def manual_times(self, config_item): # Create the new dropdown time_values = [] - for minute in range(minutes_now, minutes_now + 12 * 60, 30): + for minute in range(minutes_now, minutes_now + manual_time_max, 30): minute_str = (self.midnight + timedelta(minutes=minute)).strftime("%H:%M:%S") if minute in time_overrides: minute_str = "[" + minute_str + "]" @@ -9888,6 +9892,7 @@ def fetch_config_options(self): self.pv_metric10_weight = self.get_arg("pv_metric10_weight") self.load_scaling = self.get_arg("load_scaling") self.load_scaling10 = self.get_arg("load_scaling10") + self.load_scaling_saving = self.get_arg("load_scaling_saving") self.battery_rate_max_scaling = self.get_arg("battery_rate_max_scaling") self.best_soc_step = 0.25 @@ -10936,4 +10941,4 @@ def run_time_loop_balance(self, cb_args): except Exception as e: self.log("ERROR: Exception raised {}".format(e)) self.record_status("ERROR: Exception raised {}".format(e)) - raise + raise \ No newline at end of file From 922912882877a691fff857bd3c2b0ef57e02bdb6 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, 20 Jan 2024 16:31:10 +0000 Subject: [PATCH 2/7] [pre-commit.ci lite] apply automatic fixes --- apps/predbat/predbat.py | 42 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/apps/predbat/predbat.py b/apps/predbat/predbat.py index 41247f6ac..50b9e419d 100644 --- a/apps/predbat/predbat.py +++ b/apps/predbat/predbat.py @@ -4856,11 +4856,31 @@ def find_charge_window(self, rates, minute, threshold_rate, find_high): # If combine is disabled, for import slots make them all N minutes so we can select some not all rate_low_end = minute break - if (not find_high) and (rate_low_start in self.manual_charge_times or rate_low_start in self.manual_idle_times or minute in self.manual_charge_times or minute in self.manual_idle_times) and (rate_low_start >= 0) and ((minute - rate_low_start) >= 30): + if ( + (not find_high) + and ( + rate_low_start in self.manual_charge_times + or rate_low_start in self.manual_idle_times + or minute in self.manual_charge_times + or minute in self.manual_idle_times + ) + and (rate_low_start >= 0) + and ((minute - rate_low_start) >= 30) + ): # Manual import slot rate_low_end = minute break - if find_high and (rate_low_start in self.manual_discharge_times or rate_low_start in self.manual_idle_times or minute in self.manual_discharge_times or minute in self.manual_idle_times) and (rate_low_start >= 0) and ((minute - rate_low_start) >= 30): + if ( + find_high + and ( + rate_low_start in self.manual_discharge_times + or rate_low_start in self.manual_idle_times + or minute in self.manual_discharge_times + or minute in self.manual_idle_times + ) + and (rate_low_start >= 0) + and ((minute - rate_low_start) >= 30) + ): # Manual export slot rate_low_end = minute break @@ -8629,10 +8649,22 @@ def calculate_plan(self, recompute=True): # Created optimised step data self.metric_cloud_coverage = self.get_cloud_factor(self.minutes_now, self.pv_forecast_minute, self.pv_forecast_minute10) load_minutes_step = self.step_data_history( - self.load_minutes, self.minutes_now, forward=False, scale_today=self.load_inday_adjustment, type_load=True, load_forecast=self.load_forecast, load_scaling_dynamic=self.load_scaling_dynamic + self.load_minutes, + self.minutes_now, + forward=False, + scale_today=self.load_inday_adjustment, + type_load=True, + load_forecast=self.load_forecast, + load_scaling_dynamic=self.load_scaling_dynamic, ) load_minutes_step10 = self.step_data_history( - self.load_minutes, self.minutes_now, forward=False, scale_today=self.load_inday_adjustment * self.load_scaling10, type_load=True, load_forecast=self.load_forecast, load_scaling_dynamic=self.load_scaling_dynamic + self.load_minutes, + self.minutes_now, + forward=False, + scale_today=self.load_inday_adjustment * self.load_scaling10, + type_load=True, + load_forecast=self.load_forecast, + load_scaling_dynamic=self.load_scaling_dynamic, ) pv_forecast_minute_step = self.step_data_history(self.pv_forecast_minute, self.minutes_now, forward=True, cloud_factor=self.metric_cloud_coverage) pv_forecast_minute10_step = self.step_data_history(self.pv_forecast_minute10, self.minutes_now, forward=True, cloud_factor=self.metric_cloud_coverage) @@ -10941,4 +10973,4 @@ def run_time_loop_balance(self, cb_args): except Exception as e: self.log("ERROR: Exception raised {}".format(e)) self.record_status("ERROR: Exception raised {}".format(e)) - raise \ No newline at end of file + raise From b59c30d2238b68e687fd232cd2728b734b2c322b Mon Sep 17 00:00:00 2001 From: Trefor Southwell <48591903+springfall2008@users.noreply.github.com> Date: Sat, 20 Jan 2024 16:31:16 +0000 Subject: [PATCH 3/7] Update customisation.md --- docs/customisation.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/customisation.md b/docs/customisation.md index 3a2fb4744..e3822ecf9 100644 --- a/docs/customisation.md +++ b/docs/customisation.md @@ -122,6 +122,9 @@ Use 1.0 to use exactly previous load data (1.1 would add 10% to load) This can be used to make the 10% scenario take into account extra load usage and hence be more pessimistic while leaving the central scenario unchanged. The default is 1.1 meaning an extra 10% load is added. This will only have an impact if the PV 10% weighting is non-zero. +**input_number.load_scaling_saving** is a Scaling factor applied to historical load only during Octopus Saving sessions. This can be used to model +your household cutting down on energy use only inside a session (e.g. turning off a heat pump). + **input_number.pv_scaling** is a scaling factor applied to PV data, tune down if you want to be more pessimistic on PV production vs Solcast Use 1.0 to use exactly the Solcast data (0.9 would remove 10% from forecast) @@ -405,7 +408,7 @@ manual force charge feature. The force discharge takes priority over force charg The **select.predbat_manual_idle** selector is used to force Predbat to be idle during a 30 minute slot, this implies no charging or discharging and thus the battery will cover the house load (if there is enough charge). -When you use the manual override features you can only select times in the next 12 hours, the overrides will be removed once their time +When you use the manual override features you can only select times in the next 18 hours, the overrides will be removed once their time slot expires (they do not repeat). _CAUTION: If you leave Predbat turned off for a long period of time then the override timeslots could end up repeating when you restart_ From cb10ed148460acc8ed4d06f6eea194452add28b2 Mon Sep 17 00:00:00 2001 From: Trefor Southwell <48591903+springfall2008@users.noreply.github.com> Date: Sat, 20 Jan 2024 16:34:46 +0000 Subject: [PATCH 4/7] Update energy-rates.md --- docs/energy-rates.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/energy-rates.md b/docs/energy-rates.md index 24205cc26..57af18926 100644 --- a/docs/energy-rates.md +++ b/docs/energy-rates.md @@ -184,7 +184,21 @@ rates_export_override: rate : pence ``` +Optionally you can add a predicted load scaling in these periods using **load_scaling** for example: + +```yaml +rates_import_override: + - date: '2024-01-21' + start: '17:30:00' + end: '18:30:00' + rate: 150 + load_scaling: 0.8 +``` + +Would say that during a 1 hour period at 5:30-6:30pm on 21st of Jan set the import rate to 150p and assume our load will be 80% of normal (20% lower). + **date** is in the date format of "YYYY-MM-DD" e.g. "2023-09-09", **start** and **end** in "HH:MM:SS" time format e.g. "12:30:00", and **rate** in pence. +**load_scaling** is a factor, where 1.0 would be no change, 0.8 is 80% of nominal. ## Rate offsets From 263262b5621adc191b70dd652486de53195b69d4 Mon Sep 17 00:00:00 2001 From: Trefor Southwell <48591903+springfall2008@users.noreply.github.com> Date: Sat, 20 Jan 2024 16:35:36 +0000 Subject: [PATCH 5/7] Update apps.yaml --- apps/predbat/config/apps.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/predbat/config/apps.yaml b/apps/predbat/config/apps.yaml index 4c391e9b9..cc571e86a 100644 --- a/apps/predbat/config/apps.yaml +++ b/apps/predbat/config/apps.yaml @@ -310,7 +310,8 @@ pred_bat: # - date: '2023-09-10' # start: '14:00:00' # end: '14:30:00' - # rate: 5 + # rate: 112 + # load_scaling: 0.8 # For pv estimate, leave blank for central estimate, or add 10 for 10% curve (worst case) or 90 or 90% curve (best case) # If you use 10 then disable pv_metric10_weight below From 753eb58c146c9ede39f956b4597636969e9cba55 Mon Sep 17 00:00:00 2001 From: Trefor Southwell <48591903+springfall2008@users.noreply.github.com> Date: Sat, 20 Jan 2024 16:39:16 +0000 Subject: [PATCH 6/7] Update energy-rates.md --- docs/energy-rates.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/energy-rates.md b/docs/energy-rates.md index 57af18926..b2a5318b2 100644 --- a/docs/energy-rates.md +++ b/docs/energy-rates.md @@ -100,6 +100,9 @@ In the Predbat plan, for joined saving sessions the energy rates for import and The assumed rate will be taken from the Octopus Energy integration and converted into pence using the **octopus_saving_session_octopoints_per_penny** configuration item in apps.yaml (default is 8). +If you normally cut back your house usage during a saving session then you can change **input_number.predbat_load_scaling_saving** to allow Predbat to assume an energy +reduction in this period. E.g. setting to a value of 0.8 would indicate you will use 80% of the normal consumption in that period (a 20% reduction). + As the saving session import and export rates are very high compared to normal Predbat will plan additional export during the saving session period. If necessary, a pre-charge may happen at some point during the day to maintain the battery right level for the session. From 834a62012fc3fc0e6aefa3f595fdbcbc70716771 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, 20 Jan 2024 16:39:53 +0000 Subject: [PATCH 7/7] [pre-commit.ci lite] apply automatic fixes --- docs/energy-rates.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/energy-rates.md b/docs/energy-rates.md index b2a5318b2..87be876bd 100644 --- a/docs/energy-rates.md +++ b/docs/energy-rates.md @@ -101,7 +101,7 @@ The assumed rate will be taken from the Octopus Energy integration and converted using the **octopus_saving_session_octopoints_per_penny** configuration item in apps.yaml (default is 8). If you normally cut back your house usage during a saving session then you can change **input_number.predbat_load_scaling_saving** to allow Predbat to assume an energy -reduction in this period. E.g. setting to a value of 0.8 would indicate you will use 80% of the normal consumption in that period (a 20% reduction). +reduction in this period. E.g. setting to a value of 0.8 would indicate you will use 80% of the normal consumption in that period (a 20% reduction). As the saving session import and export rates are very high compared to normal Predbat will plan additional export during the saving session period. If necessary, a pre-charge may happen at some point during the day to maintain the battery right level for the session.