Skip to content
Open
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
38 changes: 29 additions & 9 deletions apps/predbat/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -1930,6 +1930,8 @@ def import_rate_color(rate):
raw_plan["timestamp"] = self.now_utc_real.isoformat()

if publish:
# The live plan is republished on every re-plan; the web page's stale check reads this
raw_plan["refresh_minutes"] = self.calculate_plan_every
self.dashboard_item(self.prefix + ".plan_html", state="", attributes={"text": self.text_plan, "html": html, "raw": raw_plan, "friendly_name": "Plan in HTML", "icon": "mdi:web-box"})

return html, raw_plan
Expand Down Expand Up @@ -3184,26 +3186,42 @@ def plan_row_holding_for_car(self, minute_start, minute_end, car_hold_minutes=No
held = sum(PREDICT_STEP for minute in range(minute_start, minute_end, PREDICT_STEP) if (minute - self.minutes_now) in self.predict_car_hold_best)
return held * 2 >= minute_end - minute_start

def history_slot(self, when):
"""Which plan slot the plan history rebuild at when counts towards, as (date, slot number from midnight).

Slots are plan_interval_minutes long, shifted one run (PREDICT_STEP) later: the run at a slot boundary
writes predbat.cost_today seconds before calculate_yesterday() reads its history back from Home
Assistant, so the slot just finished is rebuilt on the next run, once that value is recorded. This only
helps when that next run re-plans; with calculate_plan_every at or above the slot length, re-plans can
fall on boundaries and the rebuild then happens there.
"""
when = when - timedelta(minutes=PREDICT_STEP)
return when.date(), (when.hour * 60 + when.minute) // self.plan_interval_minutes

def history_refresh_minutes(self):
"""The longest the plan history should go between rebuilds: a slot, the run it waits, and up to one re-plan interval."""
return self.plan_interval_minutes + PREDICT_STEP + self.calculate_plan_every

def calculate_yesterday(self):
"""
Calculate the base plan for yesterday
"""

# Check savings_last_updated timestamp, we don't need to re-compute this one every iteration, once an hour or when the day rolls over is enough
if self.savings_last_updated:
# savings_last_update is a timestamp object, compare age and date
age = self.now_utc - self.savings_last_updated
if age < timedelta(minutes=59) and self.savings_last_updated.date() == self.now_utc.date():
# Less than an hour old and already updated today
return
# Recompute once per completed plan slot rather than every iteration: the plan history (the web page's
# History view) shows each slot from what actually happened, so rebuilding as soon as a slot ends keeps the
# last finished slot on show - the live Plan view starts at the current slot, so an hourly rebuild left the
# slots since it in neither view. A day rolling over is a new slot too. Each rebuild takes under a second,
# and it waits one run past the boundary (see history_slot) so the boundary run's own writes are recorded.
if self.savings_last_updated and self.history_slot(self.savings_last_updated) == self.history_slot(self.now_utc):
return

# Everything below is anchored on yesterday's recorded cost, so fetch that before doing any of
# the expensive work - when Home Assistant isn't recording predbat.cost_today there is nothing
# to compute and the step data and rate scans below would only be thrown away again
cost_today_data = self.get_history_wrapper(entity_id=self.prefix + ".cost_today", days=2, required=False)
if not cost_today_data:
self.log("Warn: Calculate yesterday: No history for {}.cost_today, so the savings and plan history can not be computed - check that Home Assistant is recording this entity (see the recorder notes in the FAQ)".format(self.prefix))
# Record the attempt so this is retried on the normal hourly cadence rather than every cycle
# Record the attempt so this is retried in the next slot rather than every cycle
self.savings_last_updated = self.now_utc
return

Expand Down Expand Up @@ -3247,7 +3265,7 @@ def calculate_yesterday(self):
# Assume user might charge at the lowest rate only, for fixed tariff
# Only use yesterday's rate range (k < end_record) for the threshold to prevent today's rates
# (which are added progressively as minutes_now increases) from changing the baseline charge
# windows on each hourly recalculation and causing savings_yesterday to fluctuate.
# windows on each recalculation and causing savings_yesterday to fluctuate.
charge_window_best = []
rate_low = self.compute_rate_low_for_yesterday(past_rates, end_record)
combine_charge = self.combine_charge_slots
Expand Down Expand Up @@ -3438,6 +3456,7 @@ def calculate_yesterday(self):
self.export_limits_best = []
self.export_window_best = []
plan_html_baseline, plan_json_baseline = self.plan_write_debug(True, None, yesterday_pv_step, yesterday_pv_step, yesterday_load_step, yesterday_load_step, end_record, prediction=self.prediction)
plan_json_baseline["refresh_minutes"] = self.history_refresh_minutes()

# Now try to show what really happened yesterday
self.charge_limit_best = []
Expand Down Expand Up @@ -3568,6 +3587,7 @@ def calculate_yesterday(self):
plan_html_yesterday, plan_json_yesterday = self.publish_html_plan(
yesterday_pv_step, yesterday_pv_step, yesterday_load_step, yesterday_load_step, end_record + minutes_now, publish=False, prediction=self.prediction, car_hold_minutes=car_hold_minutes
)
plan_json_yesterday["refresh_minutes"] = self.history_refresh_minutes()
self.forecast_minutes = end_record

# Restore state
Expand Down
Loading
Loading