Skip to content

Commit 537e9b2

Browse files
CIclaude
andcommitted
Honour day_of_week when seeding rates beyond the modelled days
basic_rates() seeds the slot past the days it models so rate_replicate() has something to copy forward, but that write ignored the rule's day_of_week filter - so the last rule processed for a given time of day won there whatever the weekday, and a weekend catch-all flattened the weekday peaks 48 hours out. Fixes #5168 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 3aec98c commit 537e9b2

2 files changed

Lines changed: 68 additions & 3 deletions

File tree

‎apps/predbat/fetch.py‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2309,9 +2309,19 @@ def basic_rates(self, info, rtype, prev=None, rate_replicate=None, include_manua
23092309
break
23102310
minute_index += 24 * 60
23112311
if not date and not prev:
2312-
rates[minute_mod + max_minute] = rate
2313-
if load_scaling is not None:
2314-
self.load_scaling_dynamic[minute_mod + max_minute] = load_scaling
2312+
# Seed the slot past the days modelled above so rate_replicate() has
2313+
# something to copy forward. This has to honour the rule's day filter
2314+
# in the same way the loop just did, or the last rule processed for a
2315+
# given time of day wins out there whatever the weekday - a weekend
2316+
# rule flattening the weekday peaks two days out (batpred#5168).
2317+
# Minutes left unwritten here are no loss: rate_replicate() fills them
2318+
# from the same time of day 24 hours earlier.
2319+
extend_minute = minute_mod + max_minute
2320+
extend_day_of_week = (day_of_week_midnight + int(extend_minute / (24 * 60))) % 7
2321+
if not day_of_week or (extend_day_of_week in day_of_week):
2322+
rates[extend_minute] = rate
2323+
if load_scaling is not None:
2324+
self.load_scaling_dynamic[extend_minute] = load_scaling
23152325
else:
23162326
self.log("Warn: Bad rate data provided in energy rates type {} {}".format(rtype, this_rate))
23172327

‎apps/predbat/tests/test_basic_rates.py‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,13 @@ 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
123+
old_forecast_minutes = my_predbat.forecast_minutes
124+
old_rate_offset_import = my_predbat.metric_future_rate_offset_import
125+
my_predbat.forecast_minutes = 24 * 60
126+
my_predbat.metric_future_rate_offset_import = 0
120127
dutch_rate = [
121128
{"start": "07:00:00", "end": "22:00:00", "rate": 14.0, "day_of_week": "1,2,3,4,5"}, # Weekday peak
122129
{"start": "22:00:00", "end": "07:00:00", "rate": 8.0, "day_of_week": "1,2,3,4,5"}, # Weekday off-peak (spans midnight)
@@ -131,6 +138,16 @@ def test_basic_rates(my_predbat):
131138
# Day 2 (Sunday): All day = 8 (weekend flat rate)
132139
failed |= assert_rates(results, 24 * 60, 48 * 60, 8) # All day Sunday = 8
133140

141+
# 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
147+
failed |= assert_rates(results, 48 * 60, 48 * 60 + 7 * 60, 8) # 00:00-07:00 off-peak
148+
failed |= assert_rates(results, 48 * 60 + 7 * 60, 48 * 60 + 22 * 60, 14) # 07:00-22:00 peak
149+
failed |= assert_rates(results, 48 * 60 + 22 * 60, 72 * 60, 8) # 22:00-24:00 off-peak
150+
134151
# Test 7: Monday (weekday) - peak/off-peak pattern
135152
print("*** Running test: Simple rate7 - Weekday peak/off-peak pattern")
136153
my_predbat.midnight_utc = my_predbat.local_tz.localize(datetime.strptime("2025-07-07T00:00:00", "%Y-%m-%dT%H:%M:%S")) # Monday (day 1)
@@ -148,8 +165,16 @@ def test_basic_rates(my_predbat):
148165
failed |= assert_rates(results, 24 * 60 + 7 * 60, 24 * 60 + 22 * 60, 14) # 07:00-22:00 peak
149166
failed |= assert_rates(results, 24 * 60 + 22 * 60, 48 * 60, 8) # 22:00-24:00 off-peak
150167

168+
# Day 3 (Wednesday): same again with midnight on a weekday, where the weekend rule must leave
169+
# the third day alone rather than flatten it (issue #5168)
170+
failed |= assert_rates(results, 48 * 60, 48 * 60 + 7 * 60, 8) # 00:00-07:00 off-peak
171+
failed |= assert_rates(results, 48 * 60 + 7 * 60, 48 * 60 + 22 * 60, 14) # 07:00-22:00 peak
172+
failed |= assert_rates(results, 48 * 60 + 22 * 60, 72 * 60, 8) # 22:00-24:00 off-peak
173+
151174
my_predbat.minutes_now = old_minutes_now
152175
my_predbat.midnight_utc = old_midnight_utc
176+
my_predbat.forecast_minutes = old_forecast_minutes
177+
my_predbat.metric_future_rate_offset_import = old_rate_offset_import
153178

154179
# Test 8: predbat_manual_api rate override only marks the actually-overridden window in
155180
# rate_replicate, not the whole day (issue #2578). get_manual_api() returns each override
@@ -268,4 +293,34 @@ def test_basic_rates(my_predbat):
268293
my_predbat.manual_api = old_manual_api
269294
my_predbat.had_errors = old_had_errors
270295

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
300+
print("*** Running test: Weekend day_of_week rule must not flatten future weekdays (issue #5168)")
301+
saved_midnight_utc = my_predbat.midnight_utc
302+
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
303+
try:
304+
weekend_catch_all = [
305+
{"start": "00:00:00", "end": "06:59:59", "rate": 11.5, "day_of_week": "1,2,3,4,5"},
306+
{"start": "07:00:00", "end": "08:59:59", "rate": 23.0, "day_of_week": "1,2,3,4,5"},
307+
{"start": "09:00:00", "end": "16:59:59", "rate": 11.5, "day_of_week": "1,2,3,4,5"},
308+
{"start": "17:00:00", "end": "20:59:59", "rate": 23.0, "day_of_week": "1,2,3,4,5"},
309+
{"start": "21:00:00", "end": "23:59:59", "rate": 11.5, "day_of_week": "1,2,3,4,5"},
310+
{"start": "00:00:00", "end": "23:59:59", "rate": 11.5, "day_of_week": "6,7"},
311+
]
312+
results = my_predbat.basic_rates(weekend_catch_all, "export")
313+
results, results_replicated = my_predbat.rate_replicate(results, is_import=False, is_gas=False)
314+
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
318+
failed |= assert_rates(results, 48 * 60, 48 * 60 + 6 * 60 + 59, 11.5) # 00:00-07:00 off-peak
319+
failed |= assert_rates(results, 48 * 60 + 7 * 60, 48 * 60 + 8 * 60 + 59, 23.0) # 07:00-09:00 peak
320+
failed |= assert_rates(results, 48 * 60 + 9 * 60, 48 * 60 + 16 * 60 + 59, 11.5) # 09:00-17:00 off-peak
321+
failed |= assert_rates(results, 48 * 60 + 17 * 60, 48 * 60 + 20 * 60 + 59, 23.0) # 17:00-21:00 peak
322+
failed |= assert_rates(results, 48 * 60 + 21 * 60, 48 * 60 + 23 * 60 + 59, 11.5) # 21:00-24:00 off-peak
323+
finally:
324+
my_predbat.midnight_utc = saved_midnight_utc
325+
271326
return failed

0 commit comments

Comments
 (0)