Skip to content

fix(octopus): free session window must be measured from minutes_now - #4935

Merged
springfall2008 merged 3 commits into
mainfrom
fix/free-session-window-minutes-now
Sep 5, 2026
Merged

fix(octopus): free session window must be measured from minutes_now#4935
springfall2008 merged 3 commits into
mainfrom
fix/free-session-window-minutes-now

Conversation

@mgazza

@mgazza mgazza commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Fixes #4931.

Root cause

load_free_slot() bounded the session against self.forecast_minutes on its own:

end_minutes = min(minutes_to_time(end, self.midnight_utc), self.forecast_minutes)

if start_minutes >= 0 and end_minutes != start_minutes and start_minutes < self.forecast_minutes:

self.forecast_minutes is a duration from minutes_now (forecast_hours * 60, fetch.py), while start_minutes / end_minutes are absolute minutes from midnight_utc. The two are not comparable. The effective cut-off was 48 hours from midnight today rather than 48 hours from now — so the accepted window shrank as the day went on.

The two sibling loaders in the same file already get this right:

loader bound
load_saving_slot self.forecast_minutes + self.minutes_now
load_axle_slot self.forecast_minutes + self.minutes_now
load_free_slot self.forecast_minutes

Why it presents as "recognised but not planned"

fetch_octopus_sessions() logs the event when it fetches it, so the session shows up in the sensor and in the log. The Setting Octopus free session in range ... line sits inside the failing if, so when the slot is dropped there is no second log line and nothing to indicate it was discarded. From the outside the rates just never change — the symptom reported in #4931.

Worked example from that report — Friday 18:00, session Sunday 11:00–12:00:

  • start_minutes = 3540 (Sunday 11:00 measured from Friday midnight)
  • forecast_minutes = 2880
  • 3540 < 2880 → false → session discarded

…even though the plan runs to minutes_now + forecast_minutes = 1080 + 2880 = 4320 and covers minute 3540 comfortably. Anyone told about a free hour more than a day or so ahead loses it, and the further into the day they look, the worse it gets.

rate_replicate() fills to forecast_minutes + 48 * 60, and runs before load_free_slot() in fetch.py, so the extended range is already populated — no new keys are needed.

Tests

test_load_free_slot previously never set minutes_now, so it inherited the wall clock and its boundary cases meant something different depending on when they ran. It now pins minutes_now to 18:00 and sizes the rate dicts the way rate_replicate does.

  • New Test 6b — the two-day-ahead session from Sunday's Power Up events are recognised but not being added to the plan #4931. Fails without this change: minute 3540 stays at 20.0p.
  • Test 6 — previously used a day-3 session, which is inside the corrected window; moved to a session genuinely past minutes_now + forecast_minutes.
  • Test 7 — previously asserted the cap landed on forecast_minutes; now asserts it lands on minutes_now + forecast_minutes, and that the minute after the window is untouched.

Tests 6 and 7 encoded the old boundary, so they had to move with it — flagging that explicitly rather than burying it.

unit_test.py -k load_free_slot / octopus_free / saving_session / octopus_slots / savings_stability / fetch_octopus_rates all pass.

Noted, not changed

rate_add_io_slots() clamps end_minutes with a bare self.forecast_minutes too. It is left alone here — IO dispatch slots are near-term and changing that behaviour deserves its own change with its own reasoning — but it looks like the same latent mismatch and may be worth a separate look.

Related

#4930 raises the manual rate-override horizon from 48 hours to 7 days. Different code path, same user-facing complaint: a free hour announced several days ahead does not make it into the plan. Regional Power Ups are announced per-customer and are not on the national free-electricity page, so the manual override is the only route for them — which is why both halves matter.

…4931)

load_free_slot() bounded the session against self.forecast_minutes alone.
That is a DURATION from minutes_now (forecast_hours * 60), while
start_minutes/end_minutes are ABSOLUTE minutes from midnight_utc, so the two
are not comparable. The effective cut-off was 48 hours from midnight today
rather than 48 hours from now, and it shrank as the day went on.

Result: a free session Predbat had already fetched and logged was dropped
before it reached the rates, with no further log line - the "Setting Octopus
free session in range" message sits inside the same if. So the session is
visible in the sensor and in the log, and simply absent from the plan, which
is exactly what #4931 describes.

Worked example from that report: Friday 18:00, session Sunday 11:00-12:00.
start_minutes is 3540 (from Friday midnight) and forecast_minutes is 2880, so
3540 < 2880 is false and the session is discarded - even though the plan runs
to minutes_now + forecast_minutes = 4320 and covers it comfortably. Anyone
told about a free hour more than a day or so ahead loses it.

load_saving_slot() and load_axle_slot(), the two sibling loaders in this file,
already bound themselves with forecast_minutes + minutes_now; only the free
session path was missing it. rate_replicate() fills to forecast_minutes +
48 * 60, so the extended range is populated before this runs.

Tests: test_load_free_slot now pins minutes_now (18:00) instead of inheriting
the wall clock, so the boundary cases mean the same thing on every run, and
sizes the rate dicts the way rate_replicate does. New Test 6b covers the
two-day-ahead session from #4931 and fails without this change (minute 3540
stays at 20.0p). Tests 6 and 7 encoded the old boundary and have been moved
onto the corrected one - Test 6 now uses a session genuinely past the window,
and Test 7 asserts the cap lands on minutes_now + forecast_minutes.

Note rate_add_io_slots() clamps with a bare forecast_minutes too. It is left
alone here - IO dispatch slots are near-term and changing them deserves its
own change - but it looks like the same latent mismatch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

There is a correctness issue in load_free_slot() when a later slot has an invalid start/end (stale minute range can be reused with a new rate), and Test 7 currently doesn’t actually exercise the intended capping behavior with the pinned minutes_now.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR fixes Octopus “free session” (Power Up / Happy Hour) handling so the free-session window is bounded against the plan horizon measured from minutes_now (consistent with other Octopus slot loaders), preventing sessions a couple of days ahead from being silently dropped as the day progresses.

Changes:

  • Fix load_free_slot() horizon comparisons by bounding against self.forecast_minutes + self.minutes_now instead of self.forecast_minutes.
  • Update test_load_free_slot to pin minutes_now for deterministic boundary behavior and add coverage for the “two days ahead” scenario from #4931.
File summaries
File Description
apps/predbat/octopus.py Corrects free-session bounding to use a horizon relative to minutes_now.
apps/predbat/tests/test_load_free_slot.py Pins minutes_now and adjusts/adds boundary tests for free-session application within the plan window.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread apps/predbat/octopus.py Outdated
Comment thread apps/predbat/tests/test_load_free_slot.py Outdated
@springfall2008 springfall2008 added the BOT_CLEANUP Trigger: bot should address PR review feedback and CI failures, then commit and push label Sep 5, 2026
CI and others added 2 commits September 5, 2026 12:23
…enuine window-straddle

Co-Authored-By: Claude Code <noreply@anthropic.com>
@springfall2008 springfall2008 removed the BOT_CLEANUP Trigger: bot should address PR review feedback and CI failures, then commit and push label Sep 5, 2026
@springfall2008
springfall2008 requested a lite review from Copilot September 5, 2026 12:11

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

The change is narrowly scoped, aligns load_free_slot() with established horizon semantics used elsewhere, and is backed by targeted regression tests covering the reported failure case.

Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@springfall2008
springfall2008 merged commit 3bacc6e into main Sep 5, 2026
3 checks passed
@springfall2008
springfall2008 deleted the fix/free-session-window-minutes-now branch September 5, 2026 12:18
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.

Sunday's Power Up events are recognised but not being added to the plan

3 participants