Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .cspell/custom-dictionary-workspace.txt
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,8 @@ randn
rarr
recp
Redownload
refetched
refetches
regionid
regname
remainings
Expand Down
77 changes: 62 additions & 15 deletions apps/predbat/predbat.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,7 @@ def reset(self):
self.plan_valid = False
self.plan_last_updated = None
self.plan_last_updated_minutes = 0
self.plan_version = 0
self.plugin_system = None
self.calculate_plan_every = 5
self.prediction_started = False
Expand Down Expand Up @@ -687,6 +688,7 @@ def save_plan(self):
"export_limits_best": self.export_limits_best,
"plan_last_updated": self.plan_last_updated.isoformat() if self.plan_last_updated else None,
"plan_last_updated_minutes": self.plan_last_updated_minutes,
"plan_version": self.plan_version,
}
try:
expiry = self.now_utc + timedelta(hours=8)
Expand Down Expand Up @@ -738,16 +740,20 @@ def load_plan(self):
self.export_limits_best = plan_data.get("export_limits_best", [])
self.plan_last_updated = saved_dt
self.plan_last_updated_minutes = plan_data.get("plan_last_updated_minutes", 0)
self.plan_version = plan_data.get("plan_version", 0)
self.plan_valid = True
self.log("Restored saved plan from {:.0f} minutes ago: {} charge windows, {} export windows".format(age_minutes, len(self.charge_window_best), len(self.export_window_best)))

def update_pred(self, scheduled=True):
def plan_once(self, scheduled=True):
"""
Update the prediction state, everything is called from here right now
Refresh input data and recompute the plan if required, without executing it.

Returns None when a pre-condition fails (template configuration, inverter data
unavailable or import rates all zero) - the error will already have been recorded.
Otherwise returns a plan artifact summary dict with keys recomputed, plan_valid,
plan_version and plan_last_updated. Passing scheduled=False forces a recompute.
"""
recompute = False
status_extra = ""
self.had_errors = False

self.update_time()
self.save_current_config()
Expand All @@ -760,7 +766,7 @@ def update_pred(self, scheduled=True):
if self.get_arg("template", False):
self.log("Error: You have not completed editing the apps.yaml template, Predbat cannot run. Please comment out 'Template: True' line in apps.yaml to start Predbat running")
self.record_status("Error: Template Configuration, remove 'Template: True' line in apps.yaml to start predbat running", had_errors=True)
return
return None

self.expose_config("active", True)
self.fetch_config_options()
Expand All @@ -775,13 +781,13 @@ def update_pred(self, scheduled=True):
if not self.fetch_inverter_data():
self.log("Error: Failed to fetch inverter data, not able to compute a plan")
self.record_status("Error: Failed to fetch inverter data, not able to compute a plan", had_errors=True)
return
return None

# Check if we have valid import rates
if self.rate_min == self.rate_max == 0:
self.log("Error: Import rates are all zero, not able to compute a plan")
self.record_status("Error: Import rates are all zero, not able to compute a plan", had_errors=True)
return
return None

if self.dynamic_load():
self.log("Dynamic load adjustment changed, will recompute the plan")
Expand All @@ -800,18 +806,52 @@ def update_pred(self, scheduled=True):

# Persist the plan so it can be restored immediately on next startup
if recompute and self.plan_valid:
self.plan_version += 1
self.save_plan()

# Publish rate data
self.publish_rate_and_threshold()

# Execute the plan, re-read the inverter first if we had to calculate (as time passes during calculations)
if recompute:
return {
"recomputed": recompute,
"plan_valid": self.plan_valid,
"plan_last_updated": self.plan_last_updated,
"plan_version": self.plan_version,
}

def execute_once(self, refetch_inverter=True):
"""
Execute the current plan against the inverters.

When refetch_inverter is True the inverter data is re-read first, as time passes
during plan calculations. Returns None if the inverter data could not be fetched
(the error will already have been recorded), otherwise the (status, status_extra)
tuple from execute_plan().
"""
if refetch_inverter:
if not self.fetch_inverter_data():
self.log("Error: Failed to fetch inverter data, not able to execute the plan")
self.record_status("Error: Failed to fetch inverter data, not able to execute the plan", had_errors=True)
return
status, status_extra = self.execute_plan()
return None
return self.execute_plan()

def update_pred(self, scheduled=True):
"""
Update the prediction state, everything is called from here right now
"""
status_extra = ""
self.had_errors = False

plan = self.plan_once(scheduled=scheduled)
if plan is None:
return
recompute = plan["recomputed"]

# Execute the plan, re-read the inverter first if we had to calculate (as time passes during calculations)
executed = self.execute_once(refetch_inverter=recompute)
if executed is None:
return
status, status_extra = executed

# If the plan was not updated, and the time has expired lets update it now
if not recompute:
Expand All @@ -828,14 +868,21 @@ def update_pred(self, scheduled=True):
time.sleep(delay)
# Calculate an updated plan, fetch the inverter data again and execute the plan
self.calculate_plan(recompute=True)
if not self.fetch_inverter_data():
self.log("Error: Failed to fetch inverter data, not able to execute the plan")
self.record_status("Error: Failed to fetch inverter data, not able to execute the plan", had_errors=True)
executed = self.execute_once(refetch_inverter=True)
if executed is None:
return
status, status_extra = self.execute_plan()
status, status_extra = executed
else:
self.log("Will not recompute the plan, it is {} minutes old and max age is {} minutes".format(dp1(plan_age_minutes), self.calculate_plan_every))

self._post_run_bookkeeping(status, status_extra, scheduled, recompute)

def _post_run_bookkeeping(self, status, status_extra, scheduled, recompute):
"""
Post-run bookkeeping shared by every successful update cycle: plugin hooks, iBoost model
state, register-write counters, savings totals, car SoC, holiday countdown, status
recording, component health, tariff comparison and memory cleanup.
"""
# Notify listeners that plan has been executed (consumed by gateway, plugins, etc.)
if self.plugin_system:
self.plugin_system.call_hooks(
Expand Down
Loading
Loading