@@ -3601,6 +3601,10 @@ def rate_add_io_slots(self, car_n, rates, octopus_slots):
36013601 slots_added_set = set ()
36023602 plan_interval_minutes = self .plan_interval_minutes
36033603 saved_slots = set () # For logging purposes, track which slots we actually applied as low rate
3604+ # Dynamic load has seen this car in its slot but not charging: none of its dispatches from now
3605+ # on get the cheap rate (see dynamic_load_car_check()). Elapsed minutes keep theirs - they record
3606+ # what the tariff charged, and today's cost is built from them.
3607+ car_cancelled = self .dynamic_load_car_effective .get (car_n , False )
36043608
36053609 if octopus_slots :
36063610 # Add in IO slots
@@ -3638,6 +3642,9 @@ def rate_add_io_slots(self, car_n, rates, octopus_slots):
36383642 # is behaviour-preserving there and simply avoids the staleness this PR adds.)
36393643 assumed_price = rates .get (start_minutes , self .rate_min )
36403644
3645+ if car_cancelled and minute >= self .minutes_now :
3646+ continue # Car is not charging, its dispatch is not trusted as cheap
3647+
36413648 if minute in saved_slots :
36423649 continue # Already applied a low rate slot to this minute, skip
36433650 else :
@@ -3688,6 +3695,60 @@ def rate_add_io_slots(self, car_n, rates, octopus_slots):
36883695
36893696 return rates
36903697
3698+ def dynamic_load_car_strip_feed_rates (self , rates ):
3699+ """
3700+ Remove an Octopus Intelligent discount the rate feed itself delivered for a dispatch of a car that
3701+ dynamic load has cancelled (see dynamic_load_car_check()).
3702+
3703+ The feed marks dispatch minutes as is_intelligent_adjusted (self.io_adjusted) and has already
3704+ lowered their price, so unlike rate_add_io_slots()'s own overlay there is nothing to withhold -
3705+ the price has to be overwritten. Called before the rates are replicated and snapshotted, so
3706+ nothing downstream sees the discount.
3707+
3708+ A minute is set back to rate_max_base, and loses its io_adjusted marker, only when it is from now
3709+ onwards, outside the fixed 23:30-05:30 window (cheap by tariff, not by dispatch), covered by a
3710+ cancelled car's dispatch and not covered by a dispatch of any car still trusted - the rates are
3711+ shared by every car.
3712+ """
3713+ cancelled_cars = [car_n for car_n in range (self .num_cars ) if self .dynamic_load_car_effective .get (car_n , False )]
3714+ if not cancelled_cars or not self .io_adjusted :
3715+ self .dynamic_load_car_stripped = 0
3716+ return rates
3717+
3718+ # Rounded out to whole 30 minute rate periods: the feed marks and prices whole periods, and
3719+ # rate_add_io_slots() likewise treats any overlap with a period as the whole period
3720+ cancelled_minutes = set ()
3721+ trusted_minutes = set ()
3722+ for car_n in range (min (self .num_cars , len (self .octopus_slots ))):
3723+ covered = cancelled_minutes if car_n in cancelled_cars else trusted_minutes
3724+ for slot in self .octopus_slots [car_n ]:
3725+ start_minutes , end_minutes , _ , _ , _ = self .decode_octopus_slot (car_n , slot , raw = True , boundaries_only = True )
3726+ if start_minutes == end_minutes :
3727+ continue
3728+ start_minutes = (start_minutes // 30 ) * 30
3729+ end_minutes = ((end_minutes + 29 ) // 30 ) * 30
3730+ covered .update (range (max (start_minutes , self .minutes_now ), end_minutes ))
3731+
3732+ window = OCTOPUS_NIGHT_RATE_WINDOWS ["iog" ]
3733+ window_start = window ["start" ][0 ] * 60 + window ["start" ][1 ]
3734+ window_end = window ["end" ][0 ] * 60 + window ["end" ][1 ]
3735+ stripped = 0
3736+ for minute in sorted (cancelled_minutes - trusted_minutes ):
3737+ if not self .io_adjusted .get (minute , False ):
3738+ continue
3739+ minute_of_day = minute % (24 * 60 )
3740+ if minute_of_day >= window_start or minute_of_day < window_end :
3741+ continue
3742+ rates [minute ] = self .rate_max_base
3743+ del self .io_adjusted [minute ]
3744+ stripped += 1
3745+ # The feed is re-read every cycle, so the same minutes are stripped again each time - log only
3746+ # when that changes, not every 5 minutes for the length of a cancellation
3747+ if stripped and stripped != self .dynamic_load_car_stripped :
3748+ self .log ("Dynamic load: removed the Intelligent dispatch rate from {} minutes of cars {} which are not charging" .format (stripped , cancelled_cars ))
3749+ self .dynamic_load_car_stripped = stripped
3750+ return rates
3751+
36913752 def fetch_octopus_rates (self , entity_id , adjust_key = None ):
36923753 """
36933754 Fetch the Octopus rates from the sensor
0 commit comments