Skip to content

Commit 1d916cc

Browse files
fix(octopus): keep a held half hour while Smart Control is off and a charger's own slots overlap it (#5339)
octopus_held_dispatches() (#5520) drops a held half hour once any completed or planned slot overlaps it. While Smart Control is off, the slots from octopus_intelligent_planned_slot are re-tagged as a charger schedule, which is never priced off-peak or counted against the daily slot cap - yet they still counted as cover. So with an Ohme on Octopus Intelligent, turning Smart Control off mid-dispatch put the half hour Octopus had already dispatched back to the day rate until the slot sensor listed it as completed, the #5520 symptom the hold exists to stop. A charger-schedule slot no longer covers a held half hour. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 53d4c65 commit 1d916cc

2 files changed

Lines changed: 35 additions & 5 deletions

File tree

‎apps/predbat/octopus.py‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4228,17 +4228,20 @@ def octopus_held_dispatches(self, car_n, completed, planned):
42284228
A half hour is covered from when it was seen running (octopus_held_update()) until a completed
42294229
dispatch overlaps it - which is then the record, including one of no energy, which is treated as
42304230
never having happened - or the hold runs out. It is left out while a planned dispatch still reaches
4231-
it, so it is not counted twice against the daily slot cap. The one in progress ends at now: it has
4232-
finished, so load_octopus_slots() plans no car charging in it, and rate_add_io_slots() prices the
4233-
rest of the half hour as Octopus bills it once a dispatch has started in it (GH#5316).
4231+
it, so it is not counted twice against the daily slot cap. A charger's own schedule does neither:
4232+
it is never priced off-peak or counted against the cap, and while Smart Control is off a charger's
4233+
own slots are re-tagged as one (#5339), though Octopus did dispatch the half hour it ran in before
4234+
then. The one in progress ends at now: it has finished, so load_octopus_slots() plans no car
4235+
charging in it, and rate_add_io_slots() prices the rest of the half hour as Octopus bills it once a
4236+
dispatch has started in it (GH#5316).
42344237
"""
42354238
held = self.octopus_slots_held.get(car_n, {})
42364239
if not held:
42374240
return []
42384241
covered = []
42394242
for slot in list(completed) + list(planned):
4240-
start_minutes, end_minutes, _, _, _ = self.decode_octopus_slot(car_n, slot, raw=True, boundaries_only=True)
4241-
if start_minutes != end_minutes:
4243+
start_minutes, end_minutes, _, source, _ = self.decode_octopus_slot(car_n, slot, raw=True, boundaries_only=True)
4244+
if start_minutes != end_minutes and source != DISPATCH_SOURCE_CHARGER_SCHEDULE:
42424245
covered.append(round_out_to_period(start_minutes, end_minutes))
42434246

42444247
dispatches = []

‎apps/predbat/tests/test_octopus_dispatch_hold.py‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,15 @@ def _dispatch(my_predbat, start_minute, end_minute, kwh=2.0, source="smart-charg
7171
return {"start": start.strftime(TIME_FORMAT), "end": end.strftime(TIME_FORMAT), "charge_in_kwh": kwh, "source": source, "location": location}
7272

7373

74+
def _untagged(my_predbat, start_minute, end_minute):
75+
"""
76+
A dispatch from a charger's own sensor (octopus_intelligent_planned_slot), which carries no source.
77+
"""
78+
slot = _dispatch(my_predbat, start_minute, end_minute)
79+
del slot["source"]
80+
return slot
81+
82+
7483
def _reset(my_predbat):
7584
"""
7685
One Octopus Intelligent car on a 6.57p / 27.7p tariff, with nothing held.
@@ -258,6 +267,24 @@ def _run(my_predbat):
258267
rates = _cycle(my_predbat, 1100)
259268
failed |= _check("t12 day rate once the entry has gone", _day(rates, 1080, 1110), "rate {}".format(rates[1080]))
260269

270+
print("Test 13: Smart Control off - a charger's own slots, re-tagged as its schedule, do not end a hold (#5339)")
271+
_reset(my_predbat)
272+
# 18:10, Smart Control on: the charger's sensor (octopus_intelligent_planned_slot) follows Octopus's dispatch
273+
_cycle(my_predbat, 1090, planned=[_untagged(my_predbat, 1080, 1200)])
274+
failed |= _check("t13 half hour recorded", 0 in my_predbat.octopus_slots_held and len(my_predbat.octopus_slots_held[0]) == 1, "held {}".format(my_predbat.octopus_slots_held))
275+
# 18:25, Smart Control off: the charger's plan and what it has seen finish are kept, re-tagged at the normal rate
276+
planned = my_predbat.smart_control_off_slots([_untagged(my_predbat, 1103, 1170)], True)
277+
completed = my_predbat.smart_control_off_slots([_untagged(my_predbat, 1080, 1103)], True)
278+
rates = _cycle(my_predbat, 1105, planned=planned, completed=completed)
279+
failed |= _check("t13 half hour Octopus dispatched still cheap", _low(rates, 1080, 1105), "rate {}".format(rates[1080]))
280+
failed |= _check("t13 held", len(_held(my_predbat)) == 1, "held {}".format(_held(my_predbat)))
281+
failed |= _check("t13 charger's plan at the day rate", _day(rates, 1110, 1170), "rate {}".format(rates[1110]))
282+
# Octopus lists the dispatch as completed: that is the record, so the hold ends
283+
completed = completed + [_dispatch(my_predbat, 1080, 1103, kwh=2.5)]
284+
rates = _cycle(my_predbat, 1125, planned=planned, completed=completed)
285+
failed |= _check("t13 hold ends once the completed list has it", _held(my_predbat) == [], "held {}".format(_held(my_predbat)))
286+
failed |= _check("t13 completed dispatch cheap", _low(rates, 1080, 1110), "rate {}".format(rates[1080]))
287+
261288
return failed
262289

263290

0 commit comments

Comments
 (0)