@@ -2670,70 +2670,92 @@ def load_octopus_slots(self, car_n, octopus_slots, octopus_intelligent_consider_
26702670 # Add in the current charging slot
26712671 for slot in slots_sorted :
26722672 start_minutes , end_minutes , kwh , source , location = slot
2673- kwh_original = kwh
2674- end_minutes_original = end_minutes
26752673
2676- # Determine rate for this slot, applying the midday-to-midday cap
2677- slot_average = self .rate_import .get (start_minutes , self .rate_min_base )
2674+ # Determine rate for this slot, applying the midday-to-midday cap. A slot that only
2675+ # partly fits the remaining daily budget is split at the point the budget runs out -
2676+ # e.g. a single 8-hour overnight IOG dispatch with only 4 of its 12 daily blocks left
2677+ # gets 4 blocks at the low rate and the other 12 at the max rate, not the whole 16
2678+ # blocks flipped to max rate the way an all-or-nothing check would (batpred#4624).
2679+ # kWh is apportioned to each chunk by its share of the slot's duration - an
2680+ # approximation (real draw isn't perfectly uniform across the slot) but matches how
2681+ # rate_add_io_slots() below treats rate as uniform per 30-min block too.
2682+ chunks = [(start_minutes , end_minutes , kwh , self .rate_import .get (start_minutes , self .rate_min_base ))]
26782683 if octopus_slot_low_rate and source != "bump-charge" and source != "BOOST" and (not location or location == "AT_HOME" ):
2679- # Count 30-min blocks for this slot against the midday-to-midday cap
26802684 slot_block_start = (start_minutes // 30 ) * 30
26812685 num_blocks = max (1 , (end_minutes - slot_block_start + 29 ) // 30 )
26822686 day_offset = (start_minutes - 720 ) // (24 * 60 )
26832687 if day_offset not in slots_per_day :
26842688 slots_per_day [day_offset ] = 0
2685- if slots_per_day [day_offset ] + num_blocks <= octopus_slot_max :
2686- slots_per_day [day_offset ] += num_blocks
2687- slot_average = self .rate_min_base
2689+ available_blocks = max (0 , octopus_slot_max - slots_per_day [day_offset ])
2690+ slots_per_day [day_offset ] += min (num_blocks , available_blocks )
2691+
2692+ if available_blocks >= num_blocks :
2693+ chunks = [(start_minutes , end_minutes , kwh , self .rate_min_base )]
2694+ elif available_blocks <= 0 :
2695+ chunks = [(start_minutes , end_minutes , kwh , self .rate_max_base )]
26882696 else :
2689- slot_average = self .rate_max_base
2690-
2691- if (end_minutes > start_minutes ) and (end_minutes > self .minutes_now ) and (not location or location == "AT_HOME" ):
2692- kwh_expected = kwh * self .car_charging_loss
2693- if octopus_intelligent_consider_full :
2694- kwh_expected = max (min (kwh_expected , limit - car_soc ), 0 )
2695- kwh = dp2 (kwh_expected / self .car_charging_loss )
2696-
2697- # Remove the remaining unused time
2698- if octopus_intelligent_consider_full and kwh > 0 and (min (car_soc + kwh_expected , limit ) >= limit ):
2699- required_extra_soc = max (limit - car_soc , 0 )
2700- required_minutes = int (required_extra_soc / (kwh_original * self .car_charging_loss ) * (end_minutes - start_minutes ) + 0.5 )
2701- required_minutes = min (required_minutes , end_minutes - start_minutes )
2702- end_minutes = start_minutes + required_minutes
2703-
2704- car_soc = min (car_soc + kwh_expected , limit )
2705- new_slot = {}
2706- new_slot ["start" ] = start_minutes
2707- new_slot ["end" ] = end_minutes
2708- new_slot ["kwh" ] = kwh
2709- new_slot ["average" ] = slot_average
2710- new_slot ["cost" ] = dp2 (new_slot ["average" ] * kwh )
2711- new_slot ["soc" ] = dp2 (car_soc )
2712- new_slot ["octopus" ] = True
2713- new_slots .append (new_slot )
2714-
2715- if end_minutes_original > end_minutes :
2697+ # Full precision here - rounding to dp2() on both sides can make low_kwh + high_kwh
2698+ # drift from the original kwh, and this function's non-split chunks already carry
2699+ # kwh at full precision too (dp2() is only applied downstream, to cost/soc).
2700+ split_minute = min (slot_block_start + available_blocks * 30 , end_minutes )
2701+ span = end_minutes - start_minutes
2702+ low_kwh = kwh * (split_minute - start_minutes ) / span if span > 0 else 0.0
2703+ chunks = [
2704+ (start_minutes , split_minute , low_kwh , self .rate_min_base ),
2705+ (split_minute , end_minutes , kwh - low_kwh , self .rate_max_base ),
2706+ ]
2707+
2708+ for chunk_start , chunk_end , chunk_kwh , slot_average in chunks :
2709+ kwh_original = chunk_kwh
2710+ end_minutes_original = chunk_end
2711+ start_minutes , end_minutes , kwh = chunk_start , chunk_end , chunk_kwh
2712+
2713+ if (end_minutes > start_minutes ) and (end_minutes > self .minutes_now ) and (not location or location == "AT_HOME" ):
2714+ kwh_expected = kwh * self .car_charging_loss
2715+ if octopus_intelligent_consider_full :
2716+ kwh_expected = max (min (kwh_expected , limit - car_soc ), 0 )
2717+ kwh = dp2 (kwh_expected / self .car_charging_loss )
2718+
2719+ # Remove the remaining unused time
2720+ if octopus_intelligent_consider_full and kwh > 0 and (min (car_soc + kwh_expected , limit ) >= limit ):
2721+ required_extra_soc = max (limit - car_soc , 0 )
2722+ required_minutes = int (required_extra_soc / (kwh_original * self .car_charging_loss ) * (end_minutes - start_minutes ) + 0.5 ) if kwh_original > 0 else 0
2723+ required_minutes = min (required_minutes , end_minutes - start_minutes )
2724+ end_minutes = start_minutes + required_minutes
2725+
2726+ car_soc = min (car_soc + kwh_expected , limit )
27162727 new_slot = {}
2717- new_slot ["start" ] = end_minutes
2718- new_slot ["end" ] = end_minutes_original
2719- new_slot ["kwh" ] = 0.0
2728+ new_slot ["start" ] = start_minutes
2729+ new_slot ["end" ] = end_minutes
2730+ new_slot ["kwh" ] = kwh
27202731 new_slot ["average" ] = slot_average
2721- new_slot ["cost" ] = 0.0
2732+ new_slot ["cost" ] = dp2 ( new_slot [ "average" ] * kwh )
27222733 new_slot ["soc" ] = dp2 (car_soc )
27232734 new_slot ["octopus" ] = True
27242735 new_slots .append (new_slot )
27252736
2726- else :
2727- car_soc = min (car_soc + kwh_expected , limit )
2728- new_slot = {}
2729- new_slot ["start" ] = start_minutes
2730- new_slot ["end" ] = end_minutes
2731- new_slot ["kwh" ] = kwh
2732- new_slot ["average" ] = slot_average
2733- new_slot ["cost" ] = dp2 (new_slot ["average" ] * kwh )
2734- new_slot ["soc" ] = dp2 (car_soc )
2735- new_slot ["octopus" ] = True
2736- new_slots .append (new_slot )
2737+ if end_minutes_original > end_minutes :
2738+ new_slot = {}
2739+ new_slot ["start" ] = end_minutes
2740+ new_slot ["end" ] = end_minutes_original
2741+ new_slot ["kwh" ] = 0.0
2742+ new_slot ["average" ] = slot_average
2743+ new_slot ["cost" ] = 0.0
2744+ new_slot ["soc" ] = dp2 (car_soc )
2745+ new_slot ["octopus" ] = True
2746+ new_slots .append (new_slot )
2747+
2748+ else :
2749+ car_soc = min (car_soc + kwh_expected , limit )
2750+ new_slot = {}
2751+ new_slot ["start" ] = start_minutes
2752+ new_slot ["end" ] = end_minutes
2753+ new_slot ["kwh" ] = kwh
2754+ new_slot ["average" ] = slot_average
2755+ new_slot ["cost" ] = dp2 (new_slot ["average" ] * kwh )
2756+ new_slot ["soc" ] = dp2 (car_soc )
2757+ new_slot ["octopus" ] = True
2758+ new_slots .append (new_slot )
27372759 return new_slots
27382760
27392761 def rate_add_io_slots (self , car_n , rates , octopus_slots ):
0 commit comments