@@ -117,9 +117,8 @@ def test_basic_rates(my_predbat):
117117 my_predbat .midnight_utc = my_predbat .local_tz .localize (datetime .strptime ("2025-07-05T00:00:00" , "%Y-%m-%dT%H:%M:%S" )) # Saturday (day 6)
118118 old_minutes_now = my_predbat .minutes_now
119119 my_predbat .minutes_now = 14 * 60 # 14:00 Saturday afternoon
120- # Tests 6 and 7 also assert the third day, which basic_rates() leaves to rate_replicate() where
121- # no rule seeds it - pin the forecast length so that day is replicated, and the future rate
122- # offset so the replicated values are comparable against the tariff's own rates
120+ # Tests 6 and 7 also assert the third day - pin the forecast length so it lies inside the horizon,
121+ # and the future rate offset so any replicated minute is comparable against the tariff's own rates
123122 old_forecast_minutes = my_predbat .forecast_minutes
124123 old_rate_offset_import = my_predbat .metric_future_rate_offset_import
125124 my_predbat .forecast_minutes = 24 * 60
@@ -139,11 +138,8 @@ def test_basic_rates(my_predbat):
139138 failed |= assert_rates (results , 24 * 60 , 48 * 60 , 8 ) # All day Sunday = 8
140139
141140 # Day 3 (Monday): the weekday pattern must survive 48 hours out (issue #5168). basic_rates()
142- # seeds the slot past the days it models so rate_replicate() has something to copy forward,
143- # and that seeding write used to ignore day_of_week - so the weekend rule, being processed
144- # last, flattened Monday to the weekend rate and erased the weekday peak. 00:00-07:00 is not
145- # seeded by any rule here (the off-peak window seeds the fourth day from 22:00 onwards), so it
146- # comes from rate_replicate() copying the same time of day 24 hours earlier
141+ # used to seed that day with a write that ignored day_of_week, so the weekend rule, being
142+ # processed last, flattened Monday to the weekend rate and erased the weekday peak
147143 failed |= assert_rates (results , 48 * 60 , 48 * 60 + 7 * 60 , 8 ) # 00:00-07:00 off-peak
148144 failed |= assert_rates (results , 48 * 60 + 7 * 60 , 48 * 60 + 22 * 60 , 14 ) # 07:00-22:00 peak
149145 failed |= assert_rates (results , 48 * 60 + 22 * 60 , 72 * 60 , 8 ) # 22:00-24:00 off-peak
@@ -293,10 +289,10 @@ def test_basic_rates(my_predbat):
293289 my_predbat .manual_api = old_manual_api
294290 my_predbat .had_errors = old_had_errors
295291
296- # Test 13: the export config from issue #5168 - weekday rules tiling the whole day plus a
297- # weekend catch-all. The weekend rule is processed last, and before the fix its seeding write
298- # for the day beyond the ones basic_rates() models ignored day_of_week, so it flattened that
299- # day to the weekend rate and the weekday peaks vanished 48 hours out
292+ # Test 13: the export config from issue #5168 - weekday rules covering the day plus a weekend
293+ # catch-all. The weekend rule is processed last, and before the fix its seeding write for the
294+ # day beyond the ones basic_rates() modelled ignored day_of_week, so it flattened that day to
295+ # the weekend rate and the weekday peaks vanished 48 hours out
300296 print ("*** Running test: Weekend day_of_week rule must not flatten future weekdays (issue #5168)" )
301297 saved_midnight_utc = my_predbat .midnight_utc
302298 my_predbat .midnight_utc = my_predbat .local_tz .localize (datetime .strptime ("2025-07-06T00:00:00" , "%Y-%m-%dT%H:%M:%S" )) # Sunday, so the third day is Tuesday
@@ -312,9 +308,9 @@ def test_basic_rates(my_predbat):
312308 results = my_predbat .basic_rates (weekend_catch_all , "export" )
313309 results , results_replicated = my_predbat .rate_replicate (results , is_import = False , is_gas = False )
314310
315- # Day 3 (Tuesday) must keep the weekday peaks. The rules end on :59:59 so each leaves the
316- # final minute of its window unset - assert the interiors, which are the ones the rules
317- # actually write
311+ # Day 3 (Tuesday) must keep the weekday peaks. The rules end on :59:59, which floors to the
312+ # :59 minute, and the window excludes its end minute - so each rule leaves its final minute
313+ # (06:59, 08:59, ...) to rate_replicate(). Assert the interiors, which the rules write
318314 failed |= assert_rates (results , 48 * 60 , 48 * 60 + 6 * 60 + 59 , 11.5 ) # 00:00-07:00 off-peak
319315 failed |= assert_rates (results , 48 * 60 + 7 * 60 , 48 * 60 + 8 * 60 + 59 , 23.0 ) # 07:00-09:00 peak
320316 failed |= assert_rates (results , 48 * 60 + 9 * 60 , 48 * 60 + 16 * 60 + 59 , 11.5 ) # 09:00-17:00 off-peak
@@ -323,4 +319,72 @@ def test_basic_rates(my_predbat):
323319 finally :
324320 my_predbat .midnight_utc = saved_midnight_utc
325321
322+ # Test 14: every day out to the end of the horizon rate_replicate() covers keeps its own weekday's
323+ # pattern, whichever order the rules are listed in. rate_replicate() fills a missing minute from
324+ # the same time of day 24 hours earlier and knows nothing about day_of_week, so any day left to
325+ # it takes the previous day's pattern - Monday copying Sunday's flat rate, say. The weekday
326+ # off-peak differs from the weekend rate so the early hours of the midnight-spanning window are
327+ # checked too, and the peak carries a load_scaling to check that write follows the same days
328+ print ("*** Running test: day_of_week honoured on every day of the horizon, in either rule order (issue #5168)" )
329+ saved_midnight_utc = my_predbat .midnight_utc
330+ saved_forecast_minutes = my_predbat .forecast_minutes
331+ saved_rate_offset_import = my_predbat .metric_future_rate_offset_import
332+ saved_load_scaling_dynamic = my_predbat .load_scaling_dynamic
333+ # rate_replicate() then runs to the end of the fifth day, and replicated minutes stay comparable
334+ my_predbat .forecast_minutes = 72 * 60
335+ my_predbat .metric_future_rate_offset_import = 0
336+ try :
337+ weekday_rules = [
338+ {"start" : "07:00:00" , "end" : "22:00:00" , "rate" : 14.0 , "day_of_week" : "1,2,3,4,5" , "load_scaling" : 1.5 },
339+ {"start" : "22:00:00" , "end" : "07:00:00" , "rate" : 12.0 , "day_of_week" : "1,2,3,4,5" },
340+ ]
341+ weekend_rule = [{"rate" : 8.0 , "day_of_week" : "6,7" }]
342+ for midnight in ("2025-07-04" , "2025-07-05" ): # Friday and Saturday
343+ for rules in (weekday_rules + weekend_rule , weekend_rule + weekday_rules ):
344+ my_predbat .midnight_utc = my_predbat .local_tz .localize (datetime .strptime (midnight , "%Y-%m-%d" ))
345+ my_predbat .load_scaling_dynamic = {}
346+ results = my_predbat .basic_rates (rules , "import" )
347+ results , results_replicated = my_predbat .rate_replicate (results , is_import = True , is_gas = False )
348+ for day in range (5 ):
349+ day_start = day * 24 * 60
350+ if (my_predbat .midnight_utc .weekday () + day ) % 7 >= 5 :
351+ failed |= assert_rates (results , day_start , day_start + 24 * 60 , 8.0 )
352+ expect_scaling = None
353+ else :
354+ failed |= assert_rates (results , day_start , day_start + 7 * 60 , 12.0 )
355+ failed |= assert_rates (results , day_start + 7 * 60 , day_start + 22 * 60 , 14.0 )
356+ failed |= assert_rates (results , day_start + 22 * 60 , day_start + 24 * 60 , 12.0 )
357+ expect_scaling = 1.5
358+ scaling = my_predbat .load_scaling_dynamic .get (day_start + 12 * 60 )
359+ if scaling != expect_scaling :
360+ print ("ERROR: load_scaling at noon on day {} from midnight {} should be {} got {}" .format (day , midnight , expect_scaling , scaling ))
361+ failed = 1
362+ finally :
363+ my_predbat .midnight_utc = saved_midnight_utc
364+ my_predbat .forecast_minutes = saved_forecast_minutes
365+ my_predbat .metric_future_rate_offset_import = saved_rate_offset_import
366+ my_predbat .load_scaling_dynamic = saved_load_scaling_dynamic
367+
368+ # Test 15: yesterday's minutes are filtered against yesterday's weekday. The day offset was
369+ # int(minute / 1440), which truncates towards zero, so every minute of yesterday bar its first
370+ # was checked against today's weekday instead
371+ print ("*** Running test: yesterday's rates follow yesterday's day_of_week" )
372+ saved_midnight_utc = my_predbat .midnight_utc
373+ my_predbat .midnight_utc = my_predbat .local_tz .localize (datetime .strptime ("2025-07-07T00:00:00" , "%Y-%m-%dT%H:%M:%S" )) # Monday, so yesterday is Sunday
374+ try :
375+ results = my_predbat .basic_rates ([{"rate" : 14.0 , "day_of_week" : "1,2,3,4,5" }, {"rate" : 8.0 , "day_of_week" : "6,7" }], "import" )
376+ failed |= assert_rates (results , - 24 * 60 , 0 , 8.0 )
377+ failed |= assert_rates (results , 0 , 24 * 60 , 14.0 )
378+ finally :
379+ my_predbat .midnight_utc = saved_midnight_utc
380+
381+ # Test 16: a rate_increment rule adds to the rate already there on every day it is stamped on,
382+ # including the days past the two modelled explicitly - the seeding write for the third day
383+ # used to assign the bare increment there
384+ print ("*** Running test: rate_increment adds to the base rate on every day" )
385+ results = my_predbat .basic_rates ([{"rate" : 10.0 }, {"start" : "17:00:00" , "end" : "19:00:00" , "rate_increment" : 5.0 }], "import" )
386+ for day in range (3 ):
387+ failed |= assert_rates (results , day * 24 * 60 + 17 * 60 , day * 24 * 60 + 19 * 60 , 15.0 )
388+ failed |= assert_rates (results , day * 24 * 60 + 19 * 60 , day * 24 * 60 + 20 * 60 , 10.0 )
389+
326390 return failed
0 commit comments