Skip to content

Commit f62569d

Browse files
fix(output): a HoldChrg row reports the limit it holds at (#4445)
Now that HoldChrg is decided from charge_limit_best, quoting the predicted end-of-window SoC in its reason could contradict the row, for example 'already at or above the 70% target' on a row at 55% when PV is predicted to carry the SoC past a 50% limit. Show the limit instead, which matches main whenever the prediction equals the limit (the usual hold case). Chrg rows keep showing the predicted SoC. Tests cover the hold level and the PV-overshoot case (prediction above the limit), which now shows HoldChrg at the limit, not Chrg. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent e814aa5 commit f62569d

2 files changed

Lines changed: 26 additions & 6 deletions

File tree

‎apps/predbat/output.py‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1423,10 +1423,11 @@ def import_rate_color(rate):
14231423
if "target" in self.charge_window_best[charge_window_n]:
14241424
target = self.charge_window_best[charge_window_n]["target"]
14251425

1426-
# Classify Chrg vs HoldChrg against the real instruction (charge_limit_best), but display the
1427-
# window's predicted end-of-window SoC ("target", from plan.py), which is what the Limit column
1428-
# documents. Classifying against the prediction showed a full-rate charge that can't finish
1429-
# within the window as HoldChrg whenever the row's SoC sat above that prediction (issue #4445).
1426+
# Classify Chrg vs HoldChrg against the real instruction (charge_limit_best), but have a Chrg row
1427+
# display the window's predicted end-of-window SoC ("target", from plan.py), which is what the
1428+
# Limit column documents. Classifying against the prediction showed a full-rate charge that can't
1429+
# finish within the window as HoldChrg whenever the row's SoC sat above that prediction (issue #4445).
1430+
# A HoldChrg row shows the limit it is held at, which the prediction need not match.
14301431
instruction_percent = calc_percent_limit(limit, self.soc_max)
14311432
limit_percent = calc_percent_limit(target, self.soc_max)
14321433
if limit > 0.0:
@@ -1440,6 +1441,7 @@ def import_rate_color(rate):
14401441
state = "HoldChrg&rarr;"
14411442
state_color = "#34DBEB"
14421443
raw_state = "HoldChrg"
1444+
limit_percent = instruction_percent
14431445
reason_parts.append({"code": "hold_charge_at_target", "params": {"target_percent": limit_percent}})
14441446
else:
14451447
state = "Chrg&nearr;"
@@ -1459,7 +1461,7 @@ def import_rate_color(rate):
14591461
show_limit = str(limit_percent)
14601462
had_state = True
14611463
if plan_debug:
1462-
show_limit += " ({})".format(str(calc_percent_limit(limit, self.soc_max)))
1464+
show_limit += " ({})".format(str(instruction_percent))
14631465
raw_state_target = str(limit_percent)
14641466
else:
14651467
if export_window_n >= 0:

‎apps/predbat/tests/test_plan_why_reason.py‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -450,13 +450,31 @@ def render(car_hold_minutes=None):
450450
print("ERROR: #4445 plan_debug Limit cell should read '60 (80)' (predicted SoC, then limit)")
451451
failed = True
452452

453-
# A real hold still classifies as HoldChrg: the SoC is at or above the real limit
453+
# A real hold still classifies as HoldChrg (the SoC is at or above the real limit), and reports the
454+
# 80% limit it is held at rather than the predicted 60%
454455
my_predbat.predict_soc_best = _flat_soc(my_predbat, 9.0) # 90%, above the 80% limit
455456
_, raw_plan = render()
456457
row = _get_row(raw_plan, minutes_now)
457458
if row is None or _codes(row) != ["hold_charge_at_target"]:
458459
print("ERROR: #4445 SoC above the real limit should still show HoldChrg: {}".format(row and _codes(row)))
459460
failed = True
461+
elif row["reasons"][0]["params"] != {"target_percent": 80} or row["state_target"] != "80":
462+
print("ERROR: #4445 HoldChrg should report the 80% limit it holds at: {} -> {}".format(row["reasons"][0]["params"], row["state_target"]))
463+
failed = True
464+
465+
# The prediction can also sit above the limit (PV carries the SoC past it): a row already above the
466+
# real limit is held there, not charged towards the predicted SoC
467+
my_predbat.charge_window_best[0]["target"] = 9.5 # predicted to reach 95% by window end
468+
my_predbat.predict_soc_best = _flat_soc(my_predbat, 8.5) # 85%, between the 80% limit and the 95% target
469+
_, raw_plan = render()
470+
row = _get_row(raw_plan, minutes_now)
471+
if row is None or _codes(row) != ["hold_charge_at_target"]:
472+
print("ERROR: #4445 SoC above the limit but below a higher prediction should show HoldChrg: {}".format(row and _codes(row)))
473+
failed = True
474+
elif row["reasons"][0]["params"] != {"target_percent": 80} or row["state_target"] != "80":
475+
print("ERROR: #4445 HoldChrg below a higher prediction should report the 80% limit: {} -> {}".format(row["reasons"][0]["params"], row["state_target"]))
476+
failed = True
477+
my_predbat.charge_window_best[0]["target"] = 6.0 # back to the 60% prediction for the text summary check below
460478

461479
# --- Test 2c: get_charge_export_text reports the predicted SoC too, falling back to the limit ---
462480
print("Test get_charge_export_text shows the predicted SoC, or the limit when there is no prediction")

0 commit comments

Comments
 (0)