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 diff --git a/apps/predbat/predbat.py b/apps/predbat/predbat.py index 3dc13a4da..50b9e419d 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: @@ -4918,6 +4933,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 +4994,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 +5152,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 +6648,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 +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 + 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 +9289,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 +9807,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 +9824,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 +9836,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 +9924,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 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_ diff --git a/docs/energy-rates.md b/docs/energy-rates.md index 24205cc26..87be876bd 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. @@ -184,7 +187,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