Skip to content

Add optional plan_at to plan_once() to plan at a caller-chosen instant - #5811

Merged
springfall2008 merged 2 commits into
feat/plan-execute-splitfrom
feat/plan-at
Oct 28, 2026
Merged

springfall2008 merged 2 commits into
feat/plan-execute-splitfrom
feat/plan-at

Conversation

@mgazza

@mgazza mgazza commented Oct 28, 2026

Copy link
Copy Markdown
Collaborator

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 at plan_at instead of the wall clock.
  • update_time() gains an optional now= argument. now_utc_real, now_utc, midnight_utc, minutes_now and related values are derived from it exactly as from the clock, including rounding down to PREDICT_STEP.
  • The clock_skew debug setting is not applied to a given instant (the caller chose it); a warning is logged if skew is set.
  • A naive datetime raises ValueError before the run changes any state.
  • With plan_at left as None, 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_real is set to the exact plan_at, because the dynamic-load car logic in plan.py reads it during planning. It keeps that value until the next update_time().
  • Remaining direct 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_at drives now_utc/minutes_now/midnight_utc in the configured timezone, a time between boundaries is rounded like the clock, clock_skew is ignored, and a naive datetime is rejected. unit_test.py --quick passes.

🤖 Generated with Claude Code

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>
@github-actions github-actions Bot added the BOT_REVIEW Trigger an autotriage label Oct 28, 2026

@springfall2008 springfall2008 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread apps/predbat/predbat.py
@springfall2008 springfall2008 removed the BOT_REVIEW Trigger an autotriage label Oct 28, 2026
…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>
@springfall2008
springfall2008 merged commit af012f4 into feat/plan-execute-split Oct 28, 2026
2 checks passed
@springfall2008
springfall2008 deleted the feat/plan-at branch October 28, 2026 19:16
springfall2008 pushed a commit that referenced this pull request Oct 29, 2026
* 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants