Skip to content

fix(output): classify plan-table charge state against the real limit, not the predicted end-of-window SoC (#4445) - #5440

Open
chalfontchubby wants to merge 8 commits into
mainfrom
fix/hold-charge-classification-4445
Open

chalfontchubby wants to merge 8 commits into
mainfrom
fix/hold-charge-classification-4445

Conversation

@chalfontchubby

@chalfontchubby chalfontchubby commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

Posted by Claude on behalf of @chalfontchubby.

Fixes #4445.

Problem

output.py decided a charge window's plan-table state (Chrg/HoldChrg) by comparing the row's SoC with charge_window_best[n]["target"]. That is the SoC the window is predicted to reach by its end, stamped by plan.py's discard_unused_charge_slots() and clip_charge_slots() when a charge can't complete within the window. It is not the real charge_limit_best[n] instruction that execute.py sends to the inverter.

So a genuine full-rate charge that can't finish within one window could display HoldChrg -> (predicted %) whenever the row's SoC sat above that prediction. Confirmed live: charge_limit_best[0] was 100% (both in the optimiser's own output and in the GivTCP command actually sent), while the plan displayed HoldChrg -> 60% for the same window.

Fix

Following the direction agreed on #4445, the fix is narrow:

  • Chrg vs HoldChrg is decided from the real limit (charge_limit_best), the same way execute.py decides "Hold charging" live (SoC at or above the target).
  • A Chrg row keeps displaying the predicted SoC in the Limit cell, the reason text and state_target, as before. That predicted figure is what PR Add textual plan, levels rate fix when no charge/export windows exist #2358 added target to show. Showing the instruction instead would make every window that clip_charge_slots raises to a full charge read 100%, even when the plan only expects it to reach a lower SoC.
  • A HoldChrg row displays the limit it is held at. Otherwise its reason ("already at or above the X% target") could quote a prediction that contradicts the row. Where the prediction equals the limit (the usual hold case), this is the same figure as before.
  • The plan_debug bracket still follows the displayed figure with the limit (e.g. 60 (80) on a Chrg row). On a HoldChrg row both are now the limit (80 (80)), so the prediction is no longer shown there. The predbat.status summary text and export rows are unchanged.

One consequence: when PV is predicted to carry the SoC past a charge limit, a row already above the limit now shows HoldChrg at the limit instead of Chrg towards the higher prediction. That matches what the inverter is doing, since it is not grid-charging.

Known limit, not addressed here: the mismatch behind #4445 looks like a stale target. The final discard_unused_charge_slots() (plan.py ~2196) runs after clip_charge_slots() but recalculates target from self.predict_soc, which was simulated before the limits were raised. The published plan (predict_soc_best) comes from a later prediction with the clipped limits, and in low-power charge mode that later prediction can charge faster. So a Chrg row can still display a predicted figure below its own SoC (e.g. Chrg -> 60 at 70%). Refreshing target from the final prediction would fix the display too, but that is a planner change and would be better as a separate issue.

Testing

New cases in test_plan_why_reason.py:

  • The Plan shows 'Hold Charge' for windows that are actually issuing a full-rate charge #4445 symptom: SoC 70%, between a 60% prediction and an 80% limit, now shows Chrg (displaying 60), not HoldChrg. This fails on main with hold_charge_at_target.
  • A real hold: SoC above the limit still shows HoldChrg and reports the 80% limit.
  • PV overshoot: the prediction (95%) is above the limit (80%) and the SoC (85%) is between them. The row shows HoldChrg at 80%.
  • plan_debug Limit cell: reads 60 (80) (predicted SoC, then limit).
  • get_charge_export_text() (previously untested): reports the predicted SoC, or the limit when there is no prediction.

--quick suite and pre-commit green.

🤖 Generated with Claude Code

… not the predicted end-of-window SoC (#4445)

output.py compared a window's charge-state classification (Chrg/HoldChrg)
against charge_window_best[n]["target"] - the simulated SoC the window is
predicted to reach by its end, stamped by plan.py's discard_unused_charge_slots()
and clip_charge_slots() whenever a charge can't complete within one slot -
instead of the real charge_limit_best[n] instruction actually sent to the
inverter by execute.py. A genuine full-rate charge that can't finish in a
single window displayed HoldChrg -> (predicted %) instead of Chrg -> (real
limit %).

Both the plan-table row and get_charge_export_text()'s "charging to X%" text
now classify and display against the real limit. Where the window's predicted
SoC genuinely diverges from the limit (clip_charge_slots raising the limit to
a full charge while the window can only reach a lower SoC in time), that
predicted-achievable figure - the original point of PR #2358 - is still
surfaced: in the plan-debug parenthesis for the table row, and as a
"(predicted X%)" suffix on the textual summary line, so that information
isn't lost outside of plan_debug.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The regression test setup does not reproduce the prior misclassification and should be adjusted before approval.

0 open findings

What changed in this PR

Fixes charge-state reporting to use the real inverter charge limit while preserving predicted SoC details.

Changes:

  • Corrects plan-table classification and debug output.
  • Updates textual charge summaries.
  • Adds regression and summary-text tests.
File Reviewed changes
apps/​predbat/​tests/​test_plan_why_reason.py Adds regression coverage for charge display behavior.
apps/​predbat/​output.py Uses real charge limits for classification and summaries.

🧠 Review effort: Lite


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

chalfontchubby and others added 3 commits October 11, 2026 11:04
The existing regression test put the SoC (20%) below both the predicted
target (60%) and the real limit (80%), so on main it still classified as
Chrg and only failed on the reported target_percent. Add a case with the
SoC between the two (70%), which on main shows HoldChrg -> 60% - the
symptom #4445 reported - and on this branch shows Chrg -> 80%.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r in the comment

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chalfontchubby
chalfontchubby marked this pull request as ready for review October 11, 2026 10:49
chalfontchubby and others added 4 commits October 11, 2026 11:51
…ing the predicted SoC

Decide Chrg vs HoldChrg from the real charge_limit_best, but keep the
Limit cell, the reason text and state_target on the window's predicted
end-of-window SoC, which is what docs/predbat-plan-card.md says the
Limit column shows and what #4445's agreed fix direction asked for.

Showing the instruction instead made every window that clip_charge_slots
raises to a full charge read 100% while the plan only expects it to
reach a lower SoC. This restores get_charge_export_text and the
plan_debug bracket order (predicted, then limit) to their main
behaviour, and reworks the tests: the SoC-between-target-and-limit case
fails on main with HoldChrg, a real hold still shows HoldChrg, and the
plan_debug cell and text summary show the predicted SoC.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
docs/predbat-plan-card.md describes the end of the slot, not of the window.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The test header still cited docs/predbat-plan-card.md for the Limit
column, the claim 05f4a6c dropped from output.py (the docs describe the
end of the slot, not of the window). And the old classification compared
the prediction with the row's lowest SoC and also fired when it sat
exactly at the prediction, not only above it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Plan shows 'Hold Charge' for windows that are actually issuing a full-rate charge

2 participants