Skip to content

Commit 2e70e45

Browse files
CIclaude
andcommitted
fix(output): the plan's first row shows its rates from now
The first row of the plan covers now to the end of its plan interval: its time and its car, PV and load kWh all start at minute_start. Its Import and Export rates were read at the aligned interval start instead, a minute already in the past. A rate that changed earlier in the interval was then shown for the whole row. For example, a cancelled Intelligent dispatch still showed its 6.90p discount after the plan had moved the rest of the half hour to the day rate. The rate cells, a merged row's min-max range, and the replicated-rate symbol now all read from minute_start. Every later row starts on its interval boundary, so only the first row can change. The JSON slot_minute stays the aligned start, which the override system keys on. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent d8c60df commit 2e70e45

2 files changed

Lines changed: 37 additions & 6 deletions

File tree

‎apps/predbat/output.py‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1181,8 +1181,11 @@ def import_rate_color(rate):
11811181
minute_timestamp = self.midnight_utc + timedelta(minutes=(minute_relative_start + self.minutes_now))
11821182

11831183
rate_start = minute_timestamp
1184-
rate_value_import = dp2(self.rate_import.get(minute, 0))
1185-
rate_value_export = dp2(self.rate_export.get(minute, 0))
1184+
# From minute_start, not the aligned interval start: the first row covers now to the end of its
1185+
# interval, like its times and kWh, and a rate that changed earlier in the interval (a cancelled
1186+
# Intelligent dispatch, say) no longer applies to it. Every later row starts on its interval anyway.
1187+
rate_value_import = dp2(self.rate_import.get(minute_start, 0))
1188+
rate_value_export = dp2(self.rate_export.get(minute_start, 0))
11861189
# Default to a single value; overridden to a "{min}-{max}" range below when this row
11871190
# turns out to be the first of a merged/rowspan cell whose minutes span more than one
11881191
# distinct rate - only the first row of a span is ever actually rendered as a tooltip.
@@ -1231,7 +1234,7 @@ def import_rate_color(rate):
12311234
in_span = True
12321235
start_span = True
12331236
minute_relative_end = self.charge_window_best[charge_window_n]["end"] - minute_now_align
1234-
rate_text_import = self.rate_range_text(self.rate_import, minute, charge_end_minute, rate_value_import)
1237+
rate_text_import = self.rate_range_text(self.rate_import, minute_start, charge_end_minute, rate_value_import)
12351238
else:
12361239
rowspan = 0
12371240

@@ -1243,7 +1246,7 @@ def import_rate_color(rate):
12431246
in_span = True
12441247
start_span = True
12451248
minute_relative_end = self.export_window_best[export_window_n]["end"] - minute_now_align
1246-
rate_text_export = self.rate_range_text(self.rate_export, minute, export_end_minute, rate_value_export)
1249+
rate_text_export = self.rate_range_text(self.rate_export, minute_start, export_end_minute, rate_value_export)
12471250
else:
12481251
rowspan = 0
12491252

@@ -1553,7 +1556,7 @@ def import_rate_color(rate):
15531556
soc_sym = "&#11015; " + soc_sym
15541557

15551558
# Import and export rates -> to string
1556-
adjust_type = self.rate_import_replicated.get(minute, None)
1559+
adjust_type = self.rate_import_replicated.get(minute_start, None)
15571560
adjust_symbol = self.adjust_symbol(adjust_type)
15581561
if adjust_symbol:
15591562
rate_str_import = "<i>%02.02f %s</i>" % (rate_value_import, adjust_symbol)
@@ -1566,7 +1569,7 @@ def import_rate_color(rate):
15661569
if charge_window_n >= 0:
15671570
rate_str_import = "<b>" + rate_str_import + "</b>"
15681571

1569-
adjust_type = self.rate_export_replicated.get(minute, None)
1572+
adjust_type = self.rate_export_replicated.get(minute_start, None)
15701573
adjust_symbol = self.adjust_symbol(adjust_type)
15711574
if adjust_symbol:
15721575
rate_str_export = "<i>%02.02f %s</i>" % (rate_value_export, adjust_symbol)

‎apps/predbat/tests/test_plan_json_rate_adjust.py‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,34 @@ def run_test_plan_json_rate_adjust(my_predbat):
239239
my_predbat.car_charging_slots[1] = []
240240
my_predbat.num_cars = saved_num_cars
241241

242+
# The first row covers now to the end of its plan interval - its kWh and times already start at now, so
243+
# its rates must too. Reading the rate at the aligned interval start showed a price the plan no longer
244+
# uses, e.g. a cancelled Intelligent dispatch still at 6.90 after the minutes from now went to 30.26.
245+
print("Test the first plan row shows the rate from now, not from the start of its half hour")
246+
saved_minutes_now = my_predbat.minutes_now
247+
saved_rate_import = my_predbat.rate_import
248+
saved_rate_export = my_predbat.rate_export
249+
saved_car_slots = my_predbat.car_charging_slots[0]
250+
try:
251+
aligned = (saved_minutes_now // my_predbat.plan_interval_minutes) * my_predbat.plan_interval_minutes
252+
my_predbat.minutes_now = aligned + 10
253+
my_predbat.car_charging_slots[0] = []
254+
my_predbat.rate_import = {minute: (6.9 if minute < aligned + 10 else 30.26) for minute in range(aligned - 60, aligned + 48 * 60)}
255+
my_predbat.rate_export = {minute: (5.0 if minute < aligned + 10 else 12.0) for minute in range(aligned - 60, aligned + 48 * 60)}
256+
html_plan, raw_plan = my_predbat.publish_html_plan(pv_step, pv_step, load_step, load_step, my_predbat.end_record, publish=False)
257+
first = raw_plan["rows"][0] if raw_plan.get("rows") else {}
258+
if first.get("import_rate") != 30.26 or first.get("export_rate") != 12.0:
259+
print("ERROR: Expected the first row's rates from now (30.26 / 12.0), got {} / {}".format(first.get("import_rate"), first.get("export_rate")))
260+
failed = True
261+
if first.get("slot_minute") != aligned:
262+
print("ERROR: slot_minute must stay the aligned interval start {} for the override system, got {}".format(aligned, first.get("slot_minute")))
263+
failed = True
264+
finally:
265+
my_predbat.minutes_now = saved_minutes_now
266+
my_predbat.rate_import = saved_rate_import
267+
my_predbat.rate_export = saved_rate_export
268+
my_predbat.car_charging_slots[0] = saved_car_slots
269+
242270
# Clean up
243271
my_predbat.num_cars = 0
244272
my_predbat.car_charging_slots[0] = []

0 commit comments

Comments
 (0)