Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/predbat/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
DYNAMIC_LOAD_CAR_SENSOR_MINUTES = 2
DYNAMIC_LOAD_CAR_START_MINUTES = 3
DYNAMIC_LOAD_CAR_LOAD_MINUTES = 5
# A car_charging_now reading only confirms the half hour it is in (keeping that half hour cheap once the car
# stops, GH#5316) from this many minutes into it: the sensor can lag a car that stopped just before the boundary
DYNAMIC_LOAD_CAR_CONFIRM_MINUTES = 2

# Extra cloud divergence applied to the PV10 scenario on top of the computed cloud factor, so the
# downside case diverges harder than the central one
Expand Down
26 changes: 15 additions & 11 deletions apps/predbat/octopus.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from datetime import datetime, timedelta, timezone
from predbat_metrics import record_api_call
from const import TIME_FORMAT, TIME_FORMAT_OCTOPUS
from utils import str2time, minutes_to_time, dp1, dp2, dp4, minute_data, filter_payment_method, is_edge_block_body, token_mint_backoff_seconds, TOKEN_MINT_BACKOFF_LOG_INTERVAL_SECONDS
from utils import str2time, minutes_to_time, dp1, dp2, dp4, minute_data, round_out_to_period, filter_payment_method, is_edge_block_body, token_mint_backoff_seconds, TOKEN_MINT_BACKOFF_LOG_INTERVAL_SECONDS
from component_base import ComponentBase
from mock_base import MockBase as SharedMockBase
import aiohttp
Expand Down Expand Up @@ -3688,8 +3688,8 @@ def rate_add_io_slots(self, car_n, rates, octopus_slots):
saved_slots = set() # For logging purposes, track which slots we actually applied as low rate
# Dynamic load has seen this car in its slot but not charging: none of its dispatches from now
# on get the cheap rate (see dynamic_load_car_check()). Elapsed minutes keep theirs - they record
# what the tariff charged, and today's cost is built from them.
car_cancelled = self.dynamic_load_car_effective.get(car_n, False)
# what the tariff charged, and today's cost is built from them. None when the car is not cancelled.
strip_from = self.dynamic_load_car_strip_from(car_n)

if octopus_slots:
# Add in IO slots
Expand All @@ -3712,8 +3712,7 @@ def rate_add_io_slots(self, car_n, rates, octopus_slots):
# Round slots to 30 minute boundary
# Floor the start (round down) and ceiling the end (round up)
# This ensures any partial overlap with a 30-min slot marks the entire slot as off-peak
start_minutes = (start_minutes // plan_interval_minutes) * plan_interval_minutes
end_minutes = ((end_minutes + plan_interval_minutes - 1) // plan_interval_minutes) * plan_interval_minutes
start_minutes, end_minutes = round_out_to_period(start_minutes, end_minutes, plan_interval_minutes)
start_minutes = max(start_minutes, -96 * 60) # Allow for previous 2 days
end_minutes = min(end_minutes, self.forecast_minutes)

Expand All @@ -3727,7 +3726,7 @@ def rate_add_io_slots(self, car_n, rates, octopus_slots):
# is behaviour-preserving there and simply avoids the staleness this PR adds.)
assumed_price = rates.get(start_minutes, self.rate_min)

if car_cancelled and minute >= self.minutes_now:
if strip_from is not None and minute >= strip_from:
continue # Car is not charging, its dispatch is not trusted as cheap

if minute in saved_slots:
Expand Down Expand Up @@ -3802,7 +3801,8 @@ def dynamic_load_car_strip_feed_rates(self, rates):
A minute is set back to rate_max_base, and loses its io_adjusted marker, only when it is from now
onwards, outside the fixed 23:30-05:30 window (cheap by tariff, not by dispatch), covered by a
cancelled car's dispatch and not covered by a dispatch of any car still trusted - the rates are
shared by every car.
shared by every car. The rest of a half hour a cancelled car was seen charging in is still trusted
(see dynamic_load_car_strip_from()), whatever another cancelled car's dispatch covers.
"""
cancelled_cars = [car_n for car_n in range(self.num_cars) if self.dynamic_load_car_effective.get(car_n, False)]
if not cancelled_cars or not self.io_adjusted:
Expand All @@ -3814,14 +3814,18 @@ def dynamic_load_car_strip_feed_rates(self, rates):
cancelled_minutes = set()
trusted_minutes = set()
for car_n in range(min(self.num_cars, len(self.octopus_slots))):
covered = cancelled_minutes if car_n in cancelled_cars else trusted_minutes
strip_from = self.dynamic_load_car_strip_from(car_n)
for slot in self.octopus_slots[car_n]:
start_minutes, end_minutes, _, _, _ = self.decode_octopus_slot(car_n, slot, raw=True, boundaries_only=True)
if start_minutes == end_minutes:
continue
start_minutes = (start_minutes // 30) * 30
end_minutes = ((end_minutes + 29) // 30) * 30
covered.update(range(max(start_minutes, self.minutes_now), end_minutes))
start_minutes, end_minutes = round_out_to_period(start_minutes, end_minutes)
start_minutes = max(start_minutes, self.minutes_now)
if strip_from is None:
trusted_minutes.update(range(start_minutes, end_minutes))
else:
trusted_minutes.update(range(start_minutes, min(end_minutes, strip_from)))
cancelled_minutes.update(range(max(start_minutes, strip_from), end_minutes))

window = OCTOPUS_NIGHT_RATE_WINDOWS["iog"]
window_start = window["start"][0] * 60 + window["start"][1]
Expand Down
Loading
Loading