diff --git a/apps/predbat/execute.py b/apps/predbat/execute.py
index 2d276b7f2..11a0a32a7 100644
--- a/apps/predbat/execute.py
+++ b/apps/predbat/execute.py
@@ -79,10 +79,9 @@ def execute_plan(self):
self.log("Include original charge start {}, keeping this instead of new start {}".format(self.time_abs_str(inverter.charge_start_time_minutes), self.time_abs_str(minutes_start)))
minutes_start = inverter.charge_start_time_minutes
- plan_interval_minutes = getattr(self, "plan_interval_minutes", 30)
# Avoid having too long a period to configure as registers only support 24-hours
if (minutes_start < self.minutes_now) and ((minutes_end - minutes_start) >= 24 * 60):
- minutes_start = int(self.minutes_now / plan_interval_minutes) * plan_interval_minutes
+ minutes_start = int(self.minutes_now / self.plan_interval_minutes) * self.plan_interval_minutes
self.log("Move on charge window start time to avoid wrap - new start {}".format(self.time_abs_str(minutes_start)))
# Span midnight allowed?
@@ -280,10 +279,9 @@ def execute_plan(self):
)
)
- plan_interval_minutes = getattr(self, "plan_interval_minutes", 30)
# Avoid having too long a period to configure as registers only support 24-hours
if (minutes_start < self.minutes_now) and ((minutes_end - minutes_start) >= 24 * 60):
- minutes_start = int(self.minutes_now / plan_interval_minutes) * plan_interval_minutes
+ minutes_start = int(self.minutes_now / self.plan_interval_minutes) * self.plan_interval_minutes
self.log("Move on export window start time to avoid wrap - new start {}".format(self.time_abs_str(minutes_start)))
export_adjust = 1
diff --git a/apps/predbat/fetch.py b/apps/predbat/fetch.py
index 9f0b78d46..3065687ca 100644
--- a/apps/predbat/fetch.py
+++ b/apps/predbat/fetch.py
@@ -95,8 +95,7 @@ def step_data_history(
values = {}
cloud_diff = 0
- plan_interval_minutes = getattr(self, "plan_interval_minutes", 30)
- for minute in range(0, self.forecast_minutes + plan_interval_minutes, step):
+ for minute in range(0, self.forecast_minutes + self.plan_interval_minutes, step):
value = 0
minute_absolute = minute + minutes_now
@@ -130,7 +129,7 @@ def step_data_history(
for offset in range(step):
load_extra += self.get_from_incrementing(load_forecast, minute_absolute, backwards=False)
if load_adjust:
- load_extra += load_adjust.get(minute_absolute, 0) * step / float(plan_interval_minutes) # The kWh figure is for the plan interval period, so divide by plan_interval_minutes and times by step
+ load_extra += load_adjust.get(minute_absolute, 0) * step / float(self.plan_interval_minutes) # The kWh figure is for the plan interval period, so divide by plan_interval_minutes and times by step
load_extra = max(load_extra, -value) # Don't allow going to negative load values
values[minute] = dp4((value + load_extra) * scaling_dynamic * scale_today * scale_fixed)
@@ -236,9 +235,8 @@ def previous_days_modal_filter(self, data):
del self.days_previous[min_sum_day_idx]
del self.days_previous_weight[min_sum_day_idx]
- plan_interval_minutes = getattr(self, "plan_interval_minutes", 30)
# Gap filling
- gap_size = max(self.get_arg("load_filter_threshold", plan_interval_minutes), 5)
+ gap_size = max(self.get_arg("load_filter_threshold", self.plan_interval_minutes), 5)
for days in days_list:
use_days = max(min(days, self.load_minutes_age), 1)
num_gaps = 0
@@ -1055,7 +1053,6 @@ def find_charge_window(self, rates, minute, threshold_rate, find_high, alt_rates
rate_low_count = 0
alternate_rate_boundary = False
alt_rate_last = None
- plan_interval_minutes = getattr(self, "plan_interval_minutes", 30)
# Work out alternate rate threshold
alt_rate_max = max(alt_rates.values()) if alt_rates else 0
@@ -1085,11 +1082,11 @@ def find_charge_window(self, rates, minute, threshold_rate, find_high, alt_rates
# 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 (rate_low_start in self.manual_all_times or minute in self.manual_all_times) and (rate_low_start >= 0) and ((minute - rate_low_start) >= plan_interval_minutes):
+ if (rate_low_start in self.manual_all_times or minute in self.manual_all_times) and (rate_low_start >= 0) and ((minute - rate_low_start) >= self.plan_interval_minutes):
# Manual slot
rate_low_end = minute
break
- if find_high and (rate_low_start >= 0) and (((minute - rate_low_start) >= 60 * 24) or (((minute - rate_low_start) >= plan_interval_minutes) and alternate_rate_boundary)):
+ if find_high and (rate_low_start >= 0) and (((minute - rate_low_start) >= 60 * 24) or (((minute - rate_low_start) >= self.plan_interval_minutes) and alternate_rate_boundary)):
# Export slot can never be bigger than 4 hours
rate_low_end = minute
break
@@ -1735,9 +1732,8 @@ def fetch_config_options(self):
self.best_soc_max = self.get_arg("best_soc_max")
self.best_soc_keep = self.get_arg("best_soc_keep")
self.best_soc_keep_weight = self.get_arg("best_soc_keep_weight")
- plan_interval_minutes = getattr(self, "plan_interval_minutes", 30)
- self.set_soc_minutes = plan_interval_minutes
- self.set_window_minutes = plan_interval_minutes
+ self.set_soc_minutes = self.plan_interval_minutes
+ self.set_window_minutes = self.plan_interval_minutes
self.inverter_set_charge_before = self.get_arg("inverter_set_charge_before")
if not self.inverter_set_charge_before:
self.set_soc_minutes = 0
@@ -1751,8 +1747,8 @@ def fetch_config_options(self):
self.combine_rate_threshold = self.get_arg("combine_rate_threshold")
self.combine_export_slots = self.get_arg("combine_export_slots")
self.combine_charge_slots = self.get_arg("combine_charge_slots")
- self.charge_slot_split = plan_interval_minutes
- self.export_slot_split = plan_interval_minutes
+ self.charge_slot_split = self.plan_interval_minutes
+ self.export_slot_split = self.plan_interval_minutes
self.calculate_best = True
self.set_read_only = self.get_arg("set_read_only")
diff --git a/apps/predbat/futurerate.py b/apps/predbat/futurerate.py
index 70b6c29d6..73308f2d4 100644
--- a/apps/predbat/futurerate.py
+++ b/apps/predbat/futurerate.py
@@ -13,6 +13,7 @@ class FutureRate:
def __init__(self, base):
self.base = base
self.record_status = base.record_status
+ self.plan_interval_minutes = base.plan_interval_minutes
self.log = base.log
self.get_arg = base.get_arg
self.midnight = base.midnight
@@ -212,11 +213,10 @@ def futurerate_analysis_new(self, url_template, rate_import_real, rate_export_re
item["to"] = time_date_end.strftime(TIME_FORMAT)
item["rate_import"] = dp2(rate_import)
item["rate_export"] = dp2(rate_export)
- plan_interval_minutes = getattr(self, "plan_interval_minutes", 30)
# Create intermediate 30 minute data points
if prev_time_date_end == time_date_start and (minutes_end - minutes_start) == 60:
- time_end_intermediate = time_date_start + timedelta(minutes=plan_interval_minutes)
+ time_end_intermediate = time_date_start + timedelta(minutes=self.plan_interval_minutes)
item["to"] = time_end_intermediate.strftime(TIME_FORMAT)
item["rate_import"] = dp2((rate_import + prev_rate_import) / 2)
item["rate_export"] = dp2((rate_export + prev_rate_export) / 2)
diff --git a/apps/predbat/octopus.py b/apps/predbat/octopus.py
index 0ca075c6a..da9bb8b02 100644
--- a/apps/predbat/octopus.py
+++ b/apps/predbat/octopus.py
@@ -320,6 +320,8 @@ def __init__(self, api_key, account_id, automatic, base):
self.api_key = api_key
self.base = base
self.log = base.log
+ self.local_tz = base.local_tz
+ self.plan_interval_minutes = base.plan_interval_minutes
self.api = OctopusEnergyApiClient(api_key, self.log)
self.stop_api = False
self.account_id = account_id
@@ -400,7 +402,7 @@ async def start(self):
try:
# Update time every minute
self.now = datetime.now()
- self.now_utc = datetime.now(timezone.utc).astimezone()
+ self.now_utc = datetime.now(self.local_tz)
count_minutes = self.now_utc.minute + self.now_utc.hour * 60
if first or (count_minutes % 30) == 0:
@@ -956,13 +958,12 @@ async def fetch_tariffs(self, tariffs):
standing = self.get_octopus_rates_direct(tariff, standingCharge=True)
rates_stamp = []
- plan_interval_minutes = getattr(self, "plan_interval_minutes", 30)
- for minute in range(0, 60 * 24 * 2, plan_interval_minutes):
+ for minute in range(0, 60 * 24 * 2, self.plan_interval_minutes):
time_now = self.midnight_utc + timedelta(minutes=minute)
rate_value = rates.get(minute, None)
if rate_value is not None:
start_time = time_now.strftime(TIME_FORMAT)
- end_time = (time_now + timedelta(minutes=plan_interval_minutes)).strftime(TIME_FORMAT)
+ end_time = (time_now + timedelta(minutes=self.plan_interval_minutes)).strftime(TIME_FORMAT)
rates_stamp.append({"start": start_time, "end": end_time, "value_inc_vat": dp4(rate_value / 100)})
rate_now = rates.get(self.now_utc.minute + self.now_utc.hour * 60, None)
if rate_now:
diff --git a/apps/predbat/output.py b/apps/predbat/output.py
index 0ff9eba7c..18b445d74 100644
--- a/apps/predbat/output.py
+++ b/apps/predbat/output.py
@@ -766,10 +766,9 @@ def get_export_type(self, export_limit, current=False):
def get_pv_forecast_slots(self, pv_forecast_minute_step):
pv_forecast_slots = []
- plan_interval_minutes = self.plan_interval_minutes
- for minute_relative in range(0, self.forecast_minutes, plan_interval_minutes):
+ for minute_relative in range(0, self.forecast_minutes, self.plan_interval_minutes):
minute_relative_start = minute_relative
- minute_relative_slot_end = minute_relative + plan_interval_minutes
+ minute_relative_slot_end = minute_relative + self.plan_interval_minutes
pv_forecast = 0.0
for offset in range(minute_relative_start, minute_relative_slot_end, PREDICT_STEP):
@@ -949,8 +948,7 @@ def publish_html_plan(self, pv_forecast_minute_step, pv_forecast_minute_step10,
html += "
"
html += self.get_html_plan_header(plan_debug)
# Use plan_interval_minutes instead of hardcoded 30
- plan_interval_minutes = self.plan_interval_minutes
- minute_now_align = int(self.minutes_now / plan_interval_minutes) * plan_interval_minutes
+ minute_now_align = int(self.minutes_now / self.plan_interval_minutes) * self.plan_interval_minutes
end_plan = min(end_record, self.forecast_minutes) + minute_now_align
rowspan = 0
in_span = False
@@ -988,11 +986,11 @@ def publish_html_plan(self, pv_forecast_minute_step, pv_forecast_minute_step10,
raw_plan["carbon_enable"] = self.carbon_enable
rate_start = self.midnight_utc
- for minute in range(minute_now_align, end_plan, plan_interval_minutes):
+ for minute in range(minute_now_align, end_plan, self.plan_interval_minutes):
minute_relative = minute - self.minutes_now
minute_relative_start = max(minute_relative, 0)
minute_start = minute_relative_start + self.minutes_now
- minute_relative_end = minute_relative + plan_interval_minutes
+ minute_relative_end = minute_relative + self.plan_interval_minutes
minute_end = minute_relative_end + self.minutes_now
minute_relative_slot_end = minute_relative_end
minute_timestamp = self.midnight_utc + timedelta(minutes=(minute_relative_start + self.minutes_now))
@@ -1003,7 +1001,7 @@ def publish_html_plan(self, pv_forecast_minute_step, pv_forecast_minute_step10,
charge_window_n = -1
export_window_n = -1
in_alert = True if self.alert_active_keep.get(minute, 0) > 0 else False
- periods_left = int((end_plan - minute + plan_interval_minutes - 1) / plan_interval_minutes)
+ periods_left = int((end_plan - minute + self.plan_interval_minutes - 1) / self.plan_interval_minutes)
show_limit = ""
@@ -1039,7 +1037,7 @@ def publish_html_plan(self, pv_forecast_minute_step, pv_forecast_minute_step10,
if discharge_intersect >= 0:
charge_end_minute = min(charge_end_minute, self.export_window_best[discharge_intersect]["start"])
- rowspan = min(int((charge_end_minute - minute) / plan_interval_minutes), periods_left)
+ rowspan = min(int((charge_end_minute - minute) / self.plan_interval_minutes), periods_left)
if rowspan > 1 and (export_window_n < 0):
in_span = True
start_span = True
@@ -1049,7 +1047,7 @@ def publish_html_plan(self, pv_forecast_minute_step, pv_forecast_minute_step10,
if export_window_n >= 0 and not in_span:
export_end_minute = self.export_window_best[export_window_n]["end"]
- rowspan = min(int((export_end_minute - minute) / plan_interval_minutes), periods_left)
+ rowspan = min(int((export_end_minute - minute) / self.plan_interval_minutes), periods_left)
start = self.export_window_best[export_window_n]["start"]
if start <= minute and rowspan > 1 and (charge_window_n < 0):
in_span = True
@@ -1574,8 +1572,7 @@ def publish_rates(self, rates, export, gas=False):
Create rates/time every plan_interval_minutes
"""
rates_time = {}
- plan_interval_minutes = self.plan_interval_minutes
- for minute in range(-24 * 60, self.minutes_now + self.forecast_minutes + 24 * 60, plan_interval_minutes):
+ for minute in range(-24 * 60, self.minutes_now + self.forecast_minutes + 24 * 60, self.plan_interval_minutes):
minute_timestamp = self.midnight_utc + timedelta(minutes=minute)
stamp = minute_timestamp.strftime(TIME_FORMAT)
rates_time[stamp] = dp2(rates[minute])
diff --git a/apps/predbat/plan.py b/apps/predbat/plan.py
index b2ef72115..008016b3b 100644
--- a/apps/predbat/plan.py
+++ b/apps/predbat/plan.py
@@ -534,9 +534,8 @@ def launch_run_prediction_export(self, this_export_limit, start, window_n, try_c
def scenario_summary_title(self, record_time):
txt = ""
- plan_interval_minutes = self.plan_interval_minutes
- minute_start = self.minutes_now - self.minutes_now % plan_interval_minutes
- for minute_absolute in range(minute_start, self.forecast_minutes + minute_start, plan_interval_minutes):
+ minute_start = self.minutes_now - self.minutes_now % self.plan_interval_minutes
+ for minute_absolute in range(minute_start, self.forecast_minutes + minute_start, self.plan_interval_minutes):
this_minute_absolute = max(minute_absolute, self.minutes_now)
minute_timestamp = self.midnight_utc + timedelta(seconds=60 * this_minute_absolute)
dstamp = minute_timestamp.strftime(TIME_FORMAT)
@@ -550,9 +549,8 @@ def scenario_summary_title(self, record_time):
def scenario_summary(self, record_time, datap):
txt = ""
- plan_interval_minutes = self.plan_interval_minutes
- minute_start = self.minutes_now - self.minutes_now % plan_interval_minutes
- for minute_absolute in range(minute_start, self.forecast_minutes + minute_start, plan_interval_minutes):
+ minute_start = self.minutes_now - self.minutes_now % self.plan_interval_minutes
+ for minute_absolute in range(minute_start, self.forecast_minutes + minute_start, self.plan_interval_minutes):
this_minute_absolute = max(minute_absolute, self.minutes_now)
minute_timestamp = self.midnight_utc + timedelta(seconds=60 * this_minute_absolute)
stamp = minute_timestamp.strftime(TIME_FORMAT)
@@ -570,18 +568,17 @@ def scenario_summary(self, record_time, datap):
def scenario_summary_state(self, record_time):
txt = ""
- plan_interval_minutes = self.plan_interval_minutes
- minute_start = self.minutes_now - self.minutes_now % plan_interval_minutes
- for minute_absolute in range(minute_start, self.forecast_minutes + minute_start, plan_interval_minutes):
+ minute_start = self.minutes_now - self.minutes_now % self.plan_interval_minutes
+ for minute_absolute in range(minute_start, self.forecast_minutes + minute_start, self.plan_interval_minutes):
minute_relative_start = max(minute_absolute - self.minutes_now, 0)
- minute_relative_end = minute_relative_start + plan_interval_minutes
+ minute_relative_end = minute_relative_start + self.plan_interval_minutes
this_minute_absolute = max(minute_absolute, self.minutes_now)
minute_timestamp = self.midnight_utc + timedelta(seconds=60 * this_minute_absolute)
stamp = minute_timestamp.strftime(TIME_FORMAT)
value = ""
charge_window_n = -1
- for try_minute in range(this_minute_absolute, minute_absolute + plan_interval_minutes, 5):
+ for try_minute in range(this_minute_absolute, minute_absolute + self.plan_interval_minutes, 5):
charge_window_n = self.in_charge_window(self.charge_window_best, try_minute)
if charge_window_n >= 0 and self.charge_limit_best[charge_window_n] == 0:
charge_window_n = -1
@@ -589,7 +586,7 @@ def scenario_summary_state(self, record_time):
break
export_window_n = -1
- for try_minute in range(this_minute_absolute, minute_absolute + plan_interval_minutes, 5):
+ for try_minute in range(this_minute_absolute, minute_absolute + self.plan_interval_minutes, 5):
export_window_n = self.in_charge_window(self.export_window_best, try_minute)
if export_window_n >= 0 and self.export_limits_best[export_window_n] == 100.0:
export_window_n = -1
diff --git a/apps/predbat/predbat.py b/apps/predbat/predbat.py
index 32f773991..cf01dd606 100644
--- a/apps/predbat/predbat.py
+++ b/apps/predbat/predbat.py
@@ -25,7 +25,7 @@
import requests
import asyncio
-THIS_VERSION = "v8.27.4"
+THIS_VERSION = "v8.27.5"
# fmt: off
PREDBAT_FILES = ["predbat.py", "config.py", "prediction.py", "gecloud.py","utils.py", "inverter.py", "ha.py", "download.py", "unit_test.py", "web.py", "web_helper.py", "predheat.py", "futurerate.py", "octopus.py", "solcast.py","execute.py", "plan.py", "fetch.py", "output.py", "userinterface.py", "energydataservice.py", "alertfeed.py", "compare.py", "db_manager.py", "db_engine.py", "plugin_system.py", "ohme.py", "components.py", "fox.py", "carbon.py", "web_mcp.py"]
@@ -334,6 +334,7 @@ def reset(self):
self.text_plan = "Computing please wait..."
self.prediction_cache_enable = True
self.base_load = 0
+ self.plan_interval_minutes = self.args.get("plan_interval_minutes", 30)
self.db_manager = None
self.plan_debug = False
self.arg_errors = {}
@@ -624,7 +625,6 @@ def reset(self):
self.config_root = "./"
self.inverter_can_charge_during_export = True
self.octopus_last_joined_try = None
- self.plan_interval_minutes = self.args.get("plan_interval_minutes", 30)
for root in CONFIG_ROOTS:
if os.path.exists(root):
diff --git a/apps/predbat/prediction.py b/apps/predbat/prediction.py
index 4085c1679..8a30773f1 100644
--- a/apps/predbat/prediction.py
+++ b/apps/predbat/prediction.py
@@ -179,6 +179,7 @@ def __init__(self, base=None, pv_forecast_minute_step=None, pv_forecast_minute10
self.inverter_can_charge_during_export = base.inverter_can_charge_during_export
self.prediction_cache_enable = base.prediction_cache_enable
self.prediction_cache = {}
+ self.plan_interval_minutes = base.plan_interval_minutes
# Store this dictionary in global so we can reconstruct it in the thread without passing the data
PRED_GLOBAL["dict"] = self.__dict__.copy()
@@ -1055,8 +1056,7 @@ def run_prediction(self, charge_limit, charge_window, export_window, export_limi
predict_battery_power[stamp] = round(battery_draw * (60 / step), 3)
predict_battery_cycle[stamp] = round(battery_cycle, 3)
# Use plan_interval_minutes instead of hardcoded 30 for scaling
- plan_interval_minutes = getattr(self, "plan_interval_minutes", 30)
- predict_pv_power[stamp] = round((pv_forecast_minute_step[minute] + pv_forecast_minute_step.get(minute + step, 0)) * (plan_interval_minutes / step), 3)
+ predict_pv_power[stamp] = round((pv_forecast_minute_step[minute] + pv_forecast_minute_step.get(minute + step, 0)) * (self.plan_interval_minutes / step), 3)
predict_grid_power[stamp] = round(diff * (60 / step), 3)
predict_load_power[stamp] = round(load_yesterday * (60 / step), 3)
if carbon_enable:
diff --git a/apps/predbat/solcast.py b/apps/predbat/solcast.py
index a12a8048d..5c0e39f1c 100644
--- a/apps/predbat/solcast.py
+++ b/apps/predbat/solcast.py
@@ -58,16 +58,16 @@ def __init__(self, prefix, solcast_host, solcast_api_key, solcast_sites, solcast
self.forecast_solar_failures_total = 0
self.config_root = base.config_root
self.forecast_days = 4
+ self.local_tz = self.base.local_tz
+ self.plan_interval_minutes = base.plan_interval_minutes
async def start(self):
"""
Start the Solar API
"""
seconds = 0
- plan_interval_minutes = getattr(self.base, "plan_interval_minutes", 30)
while not self.api_stop:
- plan_interval_minutes = getattr(self.base, "plan_interval_minutes", 30)
- if seconds % (plan_interval_minutes * 60) == 0: # Every plan_interval_minutes
+ if seconds % (self.plan_interval_minutes * 60) == 0: # Every plan_interval_minutes
try:
self.fetch_pv_forecast()
except Exception as e:
@@ -240,7 +240,6 @@ def download_forecast_solar_data(self):
period_data = {}
max_kwh = 0
- plan_interval_minutes = getattr(self.base, "plan_interval_minutes", 30)
for config in configs:
lat = config.get("latitude", 51.5072)
lon = config.get("longitude", -0.1276)
@@ -306,11 +305,11 @@ def download_forecast_solar_data(self):
period_start_stamp = period_end_stamp
- for minute in range(0, days_data * 24 * 60, plan_interval_minutes):
+ for minute in range(0, days_data * 24 * 60, self.plan_interval_minutes):
pv50 = 0
- for offset in range(0, plan_interval_minutes, 1):
+ for offset in range(0, self.plan_interval_minutes, 1):
pv50 += dp4(forecast_watt_data.get(minute + offset, 0) / 1000.0)
- pv50 /= plan_interval_minutes
+ pv50 /= self.plan_interval_minutes
period_start_stamp = self.midnight_utc.replace(tzinfo=pytz.utc) + timedelta(minutes=minute)
data_item = {"period_start": period_start_stamp.strftime(TIME_FORMAT), "pv_estimate": pv50}
if period_start_stamp in period_data:
@@ -698,14 +697,12 @@ def pv_calibration(self, pv_forecast_minute, pv_forecast_minute10, pv_forecast_d
pv_forecast_by_slot_count = {}
past_day_forecast = {}
past_day_actual = {}
- plan_interval_minutes = getattr(self.base, "plan_interval_minutes", 30)
-
for minute in pv_power_hist:
minute_absolute = self.minutes_now - minute
if minute_absolute < 0:
days_prev = int(abs(minute_absolute) / (24 * 60)) + 1
slot_abs = minute_absolute % (24 * 60)
- slot = int(slot_abs / plan_interval_minutes) * plan_interval_minutes
+ slot = int(slot_abs / self.plan_interval_minutes) * self.plan_interval_minutes
pv_power_hist_by_slot[slot] = pv_power_hist_by_slot.get(slot, 0) + pv_power_hist[minute]
pv_power_hist_by_slot_count[slot] = pv_power_hist_by_slot_count.get(slot, 0) + 1
past_day_actual[days_prev] = past_day_actual.get(days_prev, 0) + pv_power_hist[minute]
@@ -718,7 +715,7 @@ def pv_calibration(self, pv_forecast_minute, pv_forecast_minute10, pv_forecast_d
minute_absolute = self.minutes_now - minute
if minute_absolute < 0:
slot_abs = minute_absolute % (24 * 60)
- slot = int(slot_abs / plan_interval_minutes) * plan_interval_minutes
+ slot = int(slot_abs / self.plan_interval_minutes) * self.plan_interval_minutes
pv_forecast_by_slot[slot] = pv_forecast_by_slot.get(slot, 0) + pv_forecast[minute]
pv_forecast_by_slot_count[slot] = pv_forecast_by_slot_count.get(slot, 0) + 1
@@ -748,17 +745,17 @@ def pv_calibration(self, pv_forecast_minute, pv_forecast_minute10, pv_forecast_d
pv_forecast_by_slot[slot] = dp4(pv_forecast_by_slot[slot] / pv_forecast_by_slot_count[slot])
total_production = 0
- for slot in range(0, 24 * 60, plan_interval_minutes):
+ for slot in range(0, 24 * 60, self.plan_interval_minutes):
total_production += pv_power_hist_by_slot.get(slot, 0)
total_forecast = 0
- for slot in range(0, 24 * 60, plan_interval_minutes):
+ for slot in range(0, 24 * 60, self.plan_interval_minutes):
total_forecast += pv_forecast_by_slot.get(slot, 0)
pv_distribution = {}
forecast_distribution = {}
slot_adjustment = {}
- for slot in range(0, 24 * 60, plan_interval_minutes):
+ for slot in range(0, 24 * 60, self.plan_interval_minutes):
pv_distribution[slot] = dp4((pv_power_hist_by_slot.get(slot, 0)) / total_production if total_production > 0 else 0)
forecast_distribution[slot] = dp4((pv_forecast_by_slot.get(slot, 0)) / total_forecast if total_forecast > 0 else 0)
@@ -779,15 +776,15 @@ def pv_calibration(self, pv_forecast_minute, pv_forecast_minute10, pv_forecast_d
pv_forecast_minute_adjusted = {}
for minute in range(0, max(pv_forecast_minute.keys()) + 1):
pv_value = pv_forecast_minute.get(minute, 0)
- slot = (int(minute / plan_interval_minutes) * plan_interval_minutes) % (24 * 60)
+ slot = (int(minute / self.plan_interval_minutes) * self.plan_interval_minutes) % (24 * 60)
pv_forecast_minute_adjusted[minute] = pv_value * slot_adjustment.get(slot, 1.0)
pv_estimateCL = {}
pv_estimate10 = {}
pv_estimate90 = {}
- for minute in range(0, max(pv_forecast_minute.keys()) + 1, plan_interval_minutes):
+ for minute in range(0, max(pv_forecast_minute.keys()) + 1, self.plan_interval_minutes):
pv_value = 0
- for offset in range(0, plan_interval_minutes, 1):
+ for offset in range(0, self.plan_interval_minutes, 1):
pv_value += pv_forecast_minute_adjusted.get(minute + offset, 0)
# Force timezone to UTC
pv_estimateCL[minute] = min(dp4(pv_value), max_kwh / 2) # Clamp to max_kwh, divide max by 2 due to plan_interval_minutes slots
@@ -798,7 +795,7 @@ def pv_calibration(self, pv_forecast_minute, pv_forecast_minute10, pv_forecast_d
period_start = entry.get("period_start", "")
if period_start:
minutes_since_midnight = (datetime.strptime(period_start, TIME_FORMAT) - self.midnight_utc).total_seconds() / 60
- slot = int(minutes_since_midnight / plan_interval_minutes) * plan_interval_minutes
+ slot = int(minutes_since_midnight / self.plan_interval_minutes) * self.plan_interval_minutes
calibrated = pv_estimateCL.get(slot, None)
calibrated10 = pv_estimate10.get(slot, None)
calibrated90 = pv_estimate90.get(slot, None)
@@ -832,7 +829,6 @@ def pack_and_store_forecast(self, pv_forecast_minute, pv_forecast_minute10):
prev_value = -1
prev_value10 = -1
- plan_interval_minutes = getattr(self.base, "plan_interval_minutes", 30)
for minute in range(0, self.forecast_days * 24 * 60):
current_value = dp4(pv_forecast_minute.get(minute, 0))
@@ -865,7 +861,7 @@ def fetch_pv_forecast(self):
create_pv10 = False
max_kwh = 9999
- self.now_utc = datetime.now(timezone.utc)
+ self.now_utc = datetime.now(self.local_tz)
self.midnight_utc = self.now_utc.replace(hour=0, minute=0, second=0, microsecond=0)
self.minutes_now = int((self.now_utc - self.midnight_utc).seconds / 60 / PREDICT_STEP) * PREDICT_STEP
diff --git a/apps/predbat/web.py b/apps/predbat/web.py
index 2a1d8a73f..597525c5e 100644
--- a/apps/predbat/web.py
+++ b/apps/predbat/web.py
@@ -51,6 +51,7 @@ class WebInterface:
def __init__(self, web_port, base) -> None:
self.abort = False
self.base = base
+ self.plan_interval_minutes = base.plan_interval_minutes
self.log = base.log
self.default_page = "./dash"
self.pv_power_hist = {}
@@ -1359,8 +1360,7 @@ def add_button_to_time(match):
dropdown_counter += 1
now_utc = self.base.now_utc
- plan_interval_minutes = getattr(self.base, "plan_interval_minutes", 30)
- time_stamp = get_override_time_from_string(now_utc, time_text, plan_interval_minutes)
+ time_stamp = get_override_time_from_string(now_utc, time_text, self.plan_interval_minutes)
if time_stamp is None:
return match.group(0)
@@ -1822,8 +1822,7 @@ def get_chart(self, chart):
soc_kw_h0 = {}
if self.base.soc_kwh_history:
hist = self.base.soc_kwh_history
- plan_interval_minutes = getattr(self.base, "plan_interval_minutes", 30)
- for minute in range(0, self.base.minutes_now, plan_interval_minutes):
+ for minute in range(0, self.base.minutes_now, self.plan_interval_minutes):
minute_timestamp = self.base.midnight_utc + timedelta(minutes=minute)
stamp = minute_timestamp.strftime(TIME_FORMAT)
soc_kw_h0[stamp] = hist.get(self.base.minutes_now - minute, 0)
@@ -2804,8 +2803,7 @@ async def html_rate_override(self, request):
return web.json_response({"success": False, "message": "Missing required parameters"}, status=400)
now_utc = self.base.now_utc
- plan_interval_minutes = getattr(self.base, "plan_interval_minutes", 30)
- override_time = get_override_time_from_string(now_utc, time_str, plan_interval_minutes)
+ override_time = get_override_time_from_string(now_utc, time_str, self.plan_interval_minutes)
minutes_from_now = (override_time - now_utc).total_seconds() / 60
if minutes_from_now >= 17 * 60:
@@ -2865,8 +2863,7 @@ async def html_plan_override(self, request):
return web.json_response({"success": False, "message": "Missing required parameters"}, status=400)
now_utc = self.base.now_utc
- plan_interval_minutes = getattr(self.base, "plan_interval_minutes", 30)
- override_time = get_override_time_from_string(now_utc, time_str, plan_interval_minutes)
+ override_time = get_override_time_from_string(now_utc, time_str, self.plan_interval_minutes)
if not override_time:
return web.json_response({"success": False, "message": "Invalid time format"}, status=400)
diff --git a/apps/predbat/web_mcp.py b/apps/predbat/web_mcp.py
index e25479db3..4270ad994 100644
--- a/apps/predbat/web_mcp.py
+++ b/apps/predbat/web_mcp.py
@@ -949,6 +949,7 @@ def __init__(self, base, log_func=None):
self.base = base
self.log = log_func or print
self.is_running = False
+ self.plan_interval_minutes = base.plan_interval_minutes
if log_func:
log_func("Creating HTTP MCP Server with Predbat integration")
@@ -1037,8 +1038,7 @@ async def _execute_set_plan_override(self, arguments: Dict[str, Any]) -> Dict[st
action = action.replace(" ", "_")
now_utc = self.base.now_utc
- plan_interval_minutes = getattr(self.base, "plan_interval_minutes", 30)
- override_time = get_override_time_from_string(now_utc, time_str, plan_interval_minutes)
+ override_time = get_override_time_from_string(now_utc, time_str, self.plan_interval_minutes)
if not override_time:
return {"success": False, "error": "Invalid time format. Use 'Day HH:MM' format e.g. Sat 14:30", "data": None}