Repository navigation
Add optional plan_at to plan_once() to plan at a caller-chosen instant - #5811
Conversation
plan_once(scheduled=True, plan_at=None) now accepts a timezone-aware datetime that replaces the wall clock for that run, via a new optional update_time(now=...) argument. now_utc_real, now_utc, midnight_utc and minutes_now are derived from it exactly as from the clock, including the round-down to PREDICT_STEP. The clock_skew debug setting is not applied to a given instant. A naive datetime raises ValueError. With plan_at left as None behaviour is unchanged. 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.
Review at head commit 77c976add (base branch feat/plan-execute-split, verified to contain #4186's current tip 49a5333): 1 finding posted inline (low); the rest of the change looked sound. Checked and found no issue: the plan_at plumbing is confined to plan_once()/update_time() with no other callers affected (update_pred, annual.py and the unit-test helper all call without plan_at, and no mid-plan caller of update_time() re-derives the clock); the naive-datetime guard in plan_once is its first statement, so the ValueError precedes reset_run_status() and any other state change, and the one in update_time() backs up the direct call path; the default path is unchanged, skew warning included; astimezone() converts plan_at correctly for pytz zones on both a BST (June) and GMT (January) instant, which the new tests pin; the seven new test save/restore attributes are exactly the seven attributes update_time() writes, so no fixture leak; the scenarios sit in the registered, quick-eligible TEST_REGISTRY entry; no docs obligation is triggered (plan_once is a developer-facing API, not a documented setting or service) and no CONFIG_ITEMS/APPS_SCHEMA change is needed as clock_skew already exists; and the PR body's claim that the remaining datetime.now() call sites (cache freshness, storage expiry, component health, dynamic-load poll, component threads) intentionally stay on the real clock matches what I found by reading the call sites.
…ck is not a day old timedelta.seconds wraps a negative age to ~1440 minutes, so a plan stamped at a plan_at boundary ahead of the wall clock made a later wall-clock update_pred take the aged-plan recompute path. Document in plan_once that plan_at should be at or behind the wall clock, since calculate_plan's start-of-day check still treats a future stamp as a day wrap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Split update_pred into plan_once()/execute_once() entry points plan_once() refreshes inputs and recomputes the plan if required without executing it, returning a plan artifact summary (recomputed, plan_valid, plan_version, plan_last_updated), or None when a pre-condition fails. execute_once() executes the current plan, re-reading the inverter first. update_pred() is now their composition plus _post_run_bookkeeping(), so a host can plan without the timer loop. A monotonic plan_version is persisted with the saved plan and bumped on every recompute, including the age-triggered one. Rebased onto main: reset_run_status(), the control ledger's begin_cycle() before the first inverter read, and the replay logging stay in the plan phase, in their original order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Add plan/execute split and update_pred characterisation tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Add optional plan_at to plan_once() to plan at a caller-chosen instant (#5811) * Add optional plan_at to plan_once() to plan at a caller-chosen instant plan_once(scheduled=True, plan_at=None) now accepts a timezone-aware datetime that replaces the wall clock for that run, via a new optional update_time(now=...) argument. now_utc_real, now_utc, midnight_utc and minutes_now are derived from it exactly as from the clock, including the round-down to PREDICT_STEP. The clock_skew debug setting is not applied to a given instant. A naive datetime raises ValueError. With plan_at left as None behaviour is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Read plan age with total_seconds() so a plan stamped ahead of the clock is not a day old timedelta.seconds wraps a negative age to ~1440 minutes, so a plan stamped at a plan_at boundary ahead of the wall clock made a later wall-clock update_pred take the aged-plan recompute path. Document in plan_once that plan_at should be at or behind the wall clock, since calculate_plan's start-of-day check still treats a future stamp as a day wrap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: CI <ci@example.com> * Only re-version and re-save the plan when it changes; restore a stale plan plan_version now advances, and the plan is saved, only when the plan's content differs from the plan that last took a version: a new plan, or a head charge/export window dropped as time moved on. That is checked on every run with a valid plan, recompute or not. A recompute that lands on the same plan, or a run that reuses it, writes nothing. As an unchanged plan is no longer re-saved, the saved plan has no expiry. load_plan() restores it however old it is and treats it as the version baseline. One made on an earlier day is restored invalid, since its window minutes count from that day's midnight, so the first run recomputes before anything executes it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: CI <ci@example.com>
Depends on #4186 (plan_once()/execute_once() split) and is based on its branch; please merge that first.
Summary
plan_once(scheduled=True, plan_at=None)takes an optional timezone-aware datetime. When given, that run plans atplan_atinstead of the wall clock.update_time()gains an optionalnow=argument.now_utc_real,now_utc,midnight_utc,minutes_nowand related values are derived from it exactly as from the clock, including rounding down toPREDICT_STEP.clock_skewdebug setting is not applied to a given instant (the caller chose it); a warning is logged if skew is set.ValueErrorbefore the run changes any state.plan_atleft asNone, behaviour is unchanged.Why
A host with several processes gathering inputs for the same site needs them to agree on one planning instant (for example the current 5-minute boundary). Today the plan instant is always read from the clock inside
update_time().Notes
now_utc_realis set to the exactplan_at, because the dynamic-load car logic inplan.pyreads it during planning. It keeps that value until the nextupdate_time().datetime.now()calls are cache freshness, storage expiry, inverter clock checks and loop scheduling; they intentionally stay on the real clock.Tests
Extended
tests/test_plan_execute_split.py: default unchanged,plan_atdrivesnow_utc/minutes_now/midnight_utcin the configured timezone, a time between boundaries is rounded like the clock,clock_skewis ignored, and a naive datetime is rejected.unit_test.py --quickpasses.🤖 Generated with Claude Code