Skip to content

fix(component_base): stop components reading calculate_yesterday's faked minutes_now - #5086

Merged
springfall2008 merged 2 commits into
mainfrom
fix/component-minutes-now
Sep 14, 2026
Merged

springfall2008 merged 2 commits into
mainfrom
fix/component-minutes-now

Conversation

@springfall2008

Copy link
Copy Markdown
Owner

Posted by Claude on behalf of @springfall2008.

Stacked on #5084 — merge that first. Base is set to its branch, so this diff shows only the change itself.

The other half of #4804. #5054 stopped components seeing the midnight_utc that calculate_yesterday() rewinds. It fakes minutes_now to 0 in the same breath (output.py:3291), and that passthrough was deliberately left alone at the time: base.minutes_now was measured from a different clock than now_utc, so deriving it would have made components disagree with the planner wherever the container's timezone differed from the timezone: setting. #5084 removes that second clock, so the derivation is now exactly the calculation update_time() makes — through the same helper.

Why this one matters more than the date

A rewound date is obviously wrong. 0 reads as a legitimate "just after midnight", so it fails quietly. It reaches 39 component-side call sites:

File Reads Effect of minutes_now = 0
octopus.py 19 end_minutes <= self.minutes_now stops dropping expired dispatch slots; start_offset = start_minutes - self.minutes_now shifts every published offset by the real time of day
load_ml_component.py 9 minutes_now % 30 slot alignment, history windows, ML feature payload
solcast.py 5 minute_absolute = self.minutes_now - minute, today's actual-PV alignment
web.py 2 SOC history chart range
alphaess.py, deye.py, sunsynk.py 1 each _now_minutes() / _window_active() pick the live TOU slot while writing to a real inverter — 0 reads as midnight

axle.py also reads base.minutes_now, but load_axle_slot() is called from fetch.py on the base's own thread — it should see the faked values, and this does not touch it.

Tests

ComponentBase gets a test that compares the derived value against update_time()'s on the real base rather than restating the formula, so the two cannot drift, plus the rewind case itself.

The AlphaESS doubles set base.minutes_now on a MagicMock base, so they were answering arithmetic with more mocks rather than failing. They get a real clock and a set_mock_clock() helper that moves it — writing minutes_now directly no longer does anything, which is the point.

Mutation-tested: reverting the property fails with Faked base.minutes_now leaked into the component: 0.

  • ./run_all --quick green (314 tests) under both a mismatched host timezone and TZ=Europe/London
  • ./run_pre_commit green

🤖 Generated with Claude Code

…ked minutes_now

The other half of GH#4804. #5054 stopped components seeing the midnight_utc that
calculate_yesterday() (output.py) rewinds; it fakes minutes_now to 0 in the same
breath, and that passthrough was left alone because it could not be fixed safely
at the time - base.minutes_now was measured from a different clock than now_utc,
so deriving it would have made components disagree with the planner wherever the
container's timezone differed from the timezone: setting. #5084 removed that
second clock, so the derivation is now exactly the calculation update_time()
makes - through the same helper.

0 is the nastier of the two fakes: a rewound date is obviously wrong, while 0
reads as a legitimate "just after midnight". It reaches 39 component-side call
sites - octopus.py's dispatch-slot arithmetic (which stops dropping expired slots
and shifts every published offset), solcast.py's actual-PV alignment,
load_ml_component.py's slot alignment, web.py's SOC history, and the
AlphaESS/Deye/Sunsynk adapters that pick the live TOU slot by time of day while
writing to a real inverter.

The AlphaESS test doubles set base.minutes_now directly on a MagicMock base, so
they answered arithmetic with more mocks rather than failing; they get a real
clock and a set_mock_clock() helper that moves it.

Mutation-tested: reverting the property fails the new ComponentBase test with
"Faked base.minutes_now leaked into the component: 0".

./run_all --quick green (314 tests) under both a mismatched host timezone and
TZ=Europe/London; pre-commit clean.

Stacked on #5084 - merge that first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 14, 2026 13:15

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

Three moderate findings remain unresolved.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

This follow-up prevents components from reading calculate_yesterday()’s temporary minutes_now = 0 by deriving time from the stable clock.

Changes:

  • Derives component minutes from shared time state.
  • Adds rewind regression coverage.
  • Updates AlphaESS tests with coherent mock clocks.
File summaries
File Review summary
apps/predbat/component_base.py Two moderate findings: snapshot now_utc consistently (3 votes) and update Deye/Sunsynk test doubles (2 votes).
apps/predbat/tests/test_component_base.py Moderate finding (3 votes): restore all fields mutated by update_time().
apps/predbat/tests/test_alphaess_control.py Reviewed mock-clock updates.
apps/predbat/tests/test_alphaess_api.py Reviewed coherent mock-clock handling.
Review details
  • Files reviewed: 4/4 changed files
  • Comments generated: 3
  • 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/component_base.py Outdated
Comment thread apps/predbat/component_base.py Outdated
Comment thread apps/predbat/tests/test_component_base.py Outdated
Base automatically changed from fix/unify-clock-on-tz-aware to main September 14, 2026 13:37
…e mock bases

All three review findings held up.

- minutes_now read base.now_utc twice: once directly and once through
  self.midnight_utc, which goes back to the same field. update_time() runs on the
  main thread while components run on theirs, so the two reads can straddle it -
  at a day boundary that subtracts the new day's midnight from the old day's
  timestamp and collapses 23:59 to 0, reading as midnight rather than as an
  error. Snapshot it once instead. A base whose clock rolls over between reads
  reproduces that deterministically, with no threads (mutation-tested).

- The Deye and Sunsynk doubles carried the same MagicMock base AlphaESS did, so
  the derivation got arithmetic of its own rather than failing: their comment
  promised "tests set this for time-aware control" while a test setting
  base.minutes_now would have been silently ignored. Both get a real clock and
  the same set_mock_clock() helper.

- The new ComponentBase test restored three of the seven fields update_time()
  writes, leaking this test's wall-clock state into later ones. It restores the
  full set now, as tests/test_clock.py already does.

./run_all --quick green (314 tests), pre-commit clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@springfall2008

Copy link
Copy Markdown
Owner Author

Posted by Claude on behalf of @springfall2008.

All three findings held up this time. Fixed in d4eb4b6.

1. minutes_now read base.now_utc twice — real, and the sharpest of the three.

Once directly, once through self.midnight_utc, which goes back to the same field. update_time() runs on the main thread while components run on theirs, so the two reads can straddle it. At a day boundary that subtracts the new day's midnight from the old day's timestamp: (23:59 day 1) - (00:00 day 2) is -60s, which int() truncates toward zero, so it returns 0 — reading as midnight rather than as an error. Exactly the failure mode this PR exists to remove, reintroduced by a second read.

Snapshotted once now. A base whose now_utc rolls into the next day between reads reproduces it deterministically without needing two threads to interleave; mutation-tested (now_utc should be read once per call, was read 2 times).

2. Deye and Sunsynk doubles — real, same shape as AlphaESS.

Confirmed: MagicMock answers (mock - mock).total_seconds() / 60 / 5 with arithmetic of its own — int() of it is 1, so _now_minutes() silently returns 5. Their comment promised "tests set this for time-aware control" while a test setting base.minutes_now would have been ignored. Both doubles get a real clock and the same set_mock_clock() helper AlphaESS got.

3. Incomplete restore in the new test — real. It saved three of the seven fields update_time() writes, leaking this test's wall-clock state into later tests through the shared PredBat. Restores the full set now, matching what tests/test_clock.py already does.

./run_all --quick green (314 tests), pre-commit clean.

@springfall2008
springfall2008 merged commit 84c9380 into main Sep 14, 2026
2 checks passed
@springfall2008
springfall2008 deleted the fix/component-minutes-now branch September 14, 2026 13:50
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