Repository navigation
Commit f8c71d8
fix(octopus): make IOG slot trust independent of entry and car order
The trust gating added by this PR decided a 30-minute block from whichever
dispatch entry the Octopus feed happened to list first, and whichever car
fetch.py happened to loop over first. Both are arbitrary, and both could price
a genuine dispatch at the day rate. Found by review on #5110 and by successive
cold reviews of the fix itself.
Entry order. Several entries can cover one block - Octopus lists a planned
dispatch and, later, a completed record for it - and `_confirmed` is the only
per-ENTRY input to `trusted`, since car_charging_now_confirmed_slot_starts is
keyed on slot_start and therefore shared. slots_decided made the first entry
visited decide the block permanently, and saved_slots then skipped the later
entry's minutes before it could be consulted. So an unconfirmed planned entry
rejected a block its own completed record proved had happened. Confirmation is
now resolved across all entries before any is applied.
Car order. rate_add_io_slots() runs once per car into one shared rates dict,
and its reject path rewrites the rate AND drops the io_adjusted marker. The
guard against undoing another car's dispatch read trusted_dynamic_minutes,
which that same loop builds as it goes - so a rejecting car running first saw
it empty, popped the marker, and nothing anywhere restores one. The rate was
later restored but the marker was not, dropping the block out of plan.py's IOG
skew and prediction.py's PV10 hedge. resolve_protected_dispatch_minutes() now
resolves the shield across every car before the loop begins.
One decision, one implementation. car_trusted_dispatch_blocks() is the single
per-car answer to "which blocks does this car have a genuine dispatch for",
used by both the shield and the loop's own gates. An earlier version of the
shield re-derived a looser variant alongside the loop - it dropped the `needed`
gate at "planned" and skipped the per-car degrade to "none" for a car with no
real car_charging_now sensor - shielding blocks the loop itself rejects, which
would have kept a rescindable slot cheap. That is the #4516 failure this PR
exists to prevent.
assumed_price reads rate_import_no_io, the snapshot fetch takes before the
per-car loop, rather than the working rates dict this function mutates. On main
nothing ever wrote rate_max_base into rates so the read-back was safe; this
PR's restore path made it reachable, letting one car's rejection be read back
by the next car as though it were the tariff price.
trusted_dynamic_minutes records the whole block when `needed and trusted` held,
regardless of the octopus_slot_max cap. The cap budgets what Predbat PLANS -
it is documented per-car while the rates dict is install-wide - not what it
believes the price to be. Gating the record on the cap meant
exclude_dynamic_io_slots() stripped the very minutes the #4483 cap-only
exemption had just protected, on any install whose feed delivers the discount.
Test 39 asserted that bookkeeping directly and is rewritten to assert the
user-visible outcome, with io_adjusted markers it previously lacked.
Also corrects an end_minutes clamp that used forecast_minutes where its three
siblings use forecast_minutes + minutes_now. forecast_minutes is a duration,
so the clamp truncated roughly minutes_now of the horizon; pre-existing, but
this PR added the consumer that makes it strip a real dispatch's discount.
Tests 45-51 cover entry order, cap accounting under duplication, the tariff
read-back, car-order equivalence including the io_adjusted marker, shield
breadth in both directions, and the reject path against a discount
distinguishable from rate_max_base - most existing tests run on a baseline
equal to rate_max_base, so the restore was invisible to them. Each was
confirmed to fail against the behaviour it pins.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>1 parent 8575ffa commit f8c71d8
4 files changed
Lines changed: 522 additions & 39 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
987 | 987 | | |
988 | 988 | | |
989 | 989 | | |
| 990 | + | |
| 991 | + | |
| 992 | + | |
| 993 | + | |
| 994 | + | |
990 | 995 | | |
991 | 996 | | |
992 | 997 | | |
| |||
1248 | 1253 | | |
1249 | 1254 | | |
1250 | 1255 | | |
| 1256 | + | |
| 1257 | + | |
| 1258 | + | |
| 1259 | + | |
| 1260 | + | |
| 1261 | + | |
| 1262 | + | |
| 1263 | + | |
1251 | 1264 | | |
1252 | 1265 | | |
1253 | 1266 | | |
| |||
0 commit comments