Repository navigation
feat(predbat): update_pred can plan at a given instant - #5875
Conversation
update_pred takes an optional timezone-aware plan_at, passed to plan_once with the same rules. The late recompute of an ageing plan runs at that same instant. The default None keeps today's behaviour. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he passthrough Assert now_utc, plan_last_updated and the logged age in the fresh-plan reuse test, and state in the update_pred docstring exactly what plan_at moves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
springfall2008
left a comment
There was a problem hiding this comment.
Automated comment from the triage bot.
Reviewed at head 0fe097c6 (CI green: pre-commit and kernel-binaries both SUCCESS). No issues found — 0 inline, 0 in body.
The change is add-only: update_pred(scheduled=True, plan_at=None) passes plan_at through to plan_once, which already owns all the semantics from PR #5811. What I checked against the head:
- Default path unchanged. Both production call sites (
apps/predbat/predbat.py:2468initial run,:2547the 5-minute tick) pass noplan_at; withNonethe new call is identical to the old one. - PR Notes verified. The naive-
plan_atcheck isplan_once's first statement (apps/predbat/predbat.py:1305), soValueErrorprecedes any state change. The late-recompute branch readsself.now_utc, whichplan_oncesets fromplan_at(:1311), and nothing mid-cycle re-derives it —update_time()is called only fromplan_onceandinitialize(:2315,:2321), so the inverter re-read inexecute_onceleaves the clock alone. The plan is stamped atnow_utc(apps/predbat/plan.py:2443), which the reuse/recompute tests pin. - Wall-clock bookkeeping claim verified. Control ledger pruning and inverter ledger stamps use
time.time()(apps/predbat/predbat.py:1510,apps/predbat/inverter.py:2457+), the inverter clock-skew check usesdatetime.now()(apps/predbat/inverter.py:755), and the IOG latency monitor usestime.time()(apps/predbat/execute.py:768) — all independent ofplan_at. - Tests fail without the change. Each new scenario calls
update_pred(plan_at=...), which is aTypeErroronmain; the reuse scenario'snow_utc == T+5and logged'5.0 minutes old'assertions also fail if the parameter existed but were not passed through. The scenarios run against the realupdate_time(real_time=True), soplan_last_updated == plan_attests real clock plumbing, not the mockedcalculate_plan. Mutated fixture state (calculate_plan_every = 30,clock_skew, the clock attributes) is restored in thefinallyvia_restore_state/_STATE_ATTRS; the module is already registered in TEST_REGISTRY (apps/predbat/unit_test.py:580).
One pre-existing, non-blocking note: calculate_plan's start-of-day check (apps/predbat/plan.py:2073, plan_last_updated_minutes > minutes_now) reads a plan stamp ahead of the current run's clock as a day wrap and forces one spurious recompute. That is only reachable if a host passes a plan_at ahead of the wall clock — an input plan_once's docstring already rules out — and the cost is one extra recompute, never a wrong plan. No action needed for this PR.
Summary
update_pred(scheduled=True, plan_at=None)now takes the same optionalplan_atasplan_once()(#5811) and passes it through.Why
A host that drives the engine on its own schedule can already plan at a chosen instant with
plan_once(plan_at=...), but to do so it has to callplan_once,execute_onceand the post-run bookkeeping itself, andplan_once(scheduled=False)forces a recompute every time. Withplan_atonupdate_pred, such a host gets the normal cycle with its planning clock at that instant, including reuse of a fresh plan untilcalculate_plan_everyruns out and the late recompute of an ageing plan.Notes
plan_atfollowsplan_once's rules: timezone-aware, at or behind the wall clock; a naive datetime raisesValueErrorbefore any state changes.plan_oncesetsnow_utcfromplan_at, the inverter re-read does not reset the clock, and the recomputed plan is stamped atnow_utc.plan_atsets the planning clock (now_utc,midnight_utc,minutes_now). Wall-clock bookkeeping (control ledger pruning, inverter fetch timestamps, latency monitors) stays on the real clock.RUN_EVERYminutes later, as it does today; a host calling on another cadence should keep that in mind.plan_at=Noneis today's behaviour exactly.Tests
Added to
tests/test_plan_execute_split.py:test_update_pred_plan_at_stamps_plan: a forced run at T leavesplan_last_updated == T.test_update_pred_scheduled_reuses_fresh_plan: a scheduled run at T + 5 min withcalculate_plan_every = 30does not recompute, and the clock and logged plan age are those of T + 5 min.test_update_pred_scheduled_recomputes_old_plan: the same at T + 30 min recomputes.test_update_pred_rejects_naive_plan_at: raisesValueError.unit_test.py -k plan_execute_splitpasses.--quickstops attest_data_age_metrics_round_trip, which fails the same way onmainwithout this change.🤖 Generated with Claude Code