Repository navigation
docs(debug-journal): fold in 2026-09-23 queue slice; correct entries for the #5100/#5102 merge batch - #5199
Conversation
There was a problem hiding this comment.
Copilot review overview
🔵 Needs a closer look
Four documentation corrections remain unresolved, warranting human review.
Review effort: Lite
Findings: 2
Open (2)
What changed in this PR
Updates the debug journal with verified findings and corrections from the 2026-09-23 queue slice.
Changes:
- Adds troubleshooting guidance for battery control, integrations, storage, UI, and symptoms.
- Corrects test-isolation, clock, inverter-rebuild, and PV-calibration documentation.
- Adds
uncalibratedto the cspell dictionary.
| File | Description |
|---|---|
tools/debug-journal.md |
Adds and corrects triage guidance and verified findings. |
.cspell/custom-dictionary-workspace.txt |
Adds the uncalibrated term. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| @@ -49,6 +50,8 @@ A `predbat_debug.yaml` or debug-history snapshot attached to a bug report can ca | |||
|
|
|||
| - **Three different "keep" mechanisms** get conflated in reports, and only two of them can hold a battery up. `best_soc_keep` is *soft*: purely a cost-model penalty added when predicted `soc <= best_soc_keep` (the `metric_keep +=` line in `prediction.py`), scaled by `best_soc_keep_weight` and ramped in over the first four hours of the horizon. It never creates a charge window and never raises the reserve, so an automation setting it to 3 kWh cannot pin a full 8.64 kWh battery at 100% (GH#4961). `manual_soc_keep` (the web-UI `manual_soc` override) and weather-alert keeps are *hard-ish*: both merge into `all_active_keep`, which forces `keep_minute_scaling` to at least 10.0 - a 10x-import-rate penalty that effectively forbids discharging below the keep percent. `set_reserve_enable` with a high `reserve` is the only true hard floor. Related, because it comes up in the same reports: `set_discharge_during_charge` **defaults True** (`config.py`), and even when it is on, `prediction.py` still pins the discharge rate to `battery_rate_min` while SoC sits within 1% of the window's charge limit. That at-target hold is what `HoldChrg` means, and it is not `FrzChrg` - `is_freeze_charge()` requires the limit's percent to equal `reserve_percent`. | |||
|
|
|||
| There is no *charge-start* hysteresis anywhere (GH#2513, enhancement — nothing in `config.py`, only predheat has hysteresis settings): the execute-side hold band is additionally gated on `inv_target_soc_percent < 100.0` (`execute.py`), so with a 100% target an inverter sitting at 99% misses the hold gate entirely and the plain `Charging` branch re-commands the window every 5-minute cycle while the BMS tapers. `battery_charge_power_curve` shapes only the *modelled* rate — a 0.0 taper above ~97% is honoured in the model through the `battery_rate_min` default of 0 — and cannot stop the writes. #2628 is the generic deadband request, so design any fix with it. Whether the ±1% band itself still produces GH#5083's charge/hold thrash on current main is untested — the gate exists in the same shape. | |||
There was a problem hiding this comment.
Automated reply from the triage bot.
Correct, and the entry now says so. Verified on main: execute.py:453 passes self.battery_charge_power_curve into find_charge_rate(), the first return value becomes new_charge_rate → charge_rate (execute.py:490) and is written by adjust_charge_rate() (execute.py:290), so "shapes only the modelled rate" was wrong.
Two qualifications went in with the correction, because both bear on whether the curve could be the mechanism here:
- The curve only selects the written rate when
set_charge_low_poweris on. With it off,find_charge_rate()returnsmax_rateunchanged (utils.py, theelseat the end of the low-power branch) and the curve shapes only the second return value, whichexecute.pylogs asrealand never writes. - Even in low-power mode the rate search floors at
max(400W, battery_rate_min * MINUTE_WATT), so the 0.0 taper above ~97% thatget_charge_rate_curve_cached()honours in the model (max(min(rate, max_charge_rate), battery_rate_min),utils.py:2395) never becomes a zero write.
The re-command/hysteresis gap is left as the separate mechanism it is — the curve cannot stop the window being re-commanded on either path.
…6-09-23 # Conflicts: # tools/debug-journal.md
Resolve the tools/debug-journal.md conflict with the now-merged 2026-09-23 slice (#5199) by a word-level splice: the two slices edited disjoint spans of the same table rows. Where both appended a sentence to the end of the same cell (GivTCP REST, Component health) both are kept, #5199's text first. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Took the first 15 candidates from the queue in plain filename sort order:
2513,2689,2716,5099,5100,5101,5103,5105,5106,5107,5112,5116,5121,5122,5125. 47 candidates remain queued (62 at the top level minus this slice); a slice drains per day.Every candidate's claims were re-verified against current
main(8b9fe029, v9.1.0-58) before folding: each cited symbol was grepped/read on the working tree, and PR merge states were checked viagit log --grep.Folded in (11)
inv_target_soc_percent < 100.0, so a 100% target re-commands the window at SoC 99% (execute.pyhysteresis verified on main). Folded into the keep-mechanisms paragraph, withbattery_charge_power_curve's model-only scope and the Improvement Proposal: SoC Buffer Handling in Predbat #2628 deadband link.find_charge_curveIndexError guard fixed in PR Fixes for discharge/charge curve calc #2814 (first in v8.26.2); the four-way guard verified correct atinverter.pyon main. Folded into the Charge/discharge curve row with the stale-version triage rule.Inverterobjects are rebuilt every 5-minute cycle (fetch_inverter_data()defaultscreate=True; verifiedexecute.py+predbat.pycall sites on main) — folded as a correction to theself.base.argstrap entry, which said rebuilds happened only on the balance path (which no longer constructs anything since Fold balance_inverters into execute: one source of inverter rate writes #5149); (b)battery_calibrationliteral-value semantics vs GivTCP's!= "Off"detection (re-verified both sides on main) — folded into the GivTCP REST row. The candidate's pre-Fold balance_inverters into execute: one source of inverter rate writes #5149 balance-path claim was dropped as overtaken.save_plan()fixed by PR fix(plan): save_plan() expiry computed from the wrong clock (#5079) #5100. Folded as a new Storage row, including the counterweight rule against "build test dates from Predbat's clock" and the run-the-victim-alone-at-two-times-of-day fuzzer tip.givtcp_restentry crashes the v9 poll loop anonymously; re-verified the unvalidatedInverterRestStateconstruction (givtcp.py) and therest_api + "/" + apicrash line (givtcp_rest.py) on main. Folded into the GivTCP REST row with the log signature (nodiscovered N inverter(s)line anywhere; malformed entry produces no Errno-22 retries of its own)./settingsre-read only on the first tick once an EMS setspolling_mode = False; re-verified the gate and thesettings_from_cachestartup skip on main (line drift 1808→1978 noted; symbols named). Folded into the GE Cloud row.manual_select()'s inability to drivemanual_api; re-verified the three builders and theirvalues not in time_valuestails on main. New Manual override selects row.marginal.pyexists on main with the sameMARGINAL_TIME_OFFSETS/threshold structure and theplan.pycall site. New Marginal rate sensors row.asyncio.to_threadworker, and never setsapi_stop; thewait_for/shield/cancel()shape re-verified oncomponent_base.pyon main. Folded into the Component health row, kept marked as a finding on the still-open PR Run ML training off the event loop and let a shutdown abandon it #5112.d9592461,91b2d8a7) that went further than the candidate's fix idea: a dedicatedsensor.<prefix>_pv_forecast_h0_uncalibratedpublished unconditionally, per-pointfallback_to_state, and the h0-now-attribute/state legacy fallback — all verified on main (solcast.py,utils.py,db_engine.py). Folded into the Solcast row as fixed-since with the mechanism kept, plus the survivinghistory_attribute/db_enginefacts and the calibration-test fidelity trap.execute.py/plan.pylines re-verified on main. New symptom-table row, including theHold charging target X%-Y%= SoC–target-pair decoder and the unresolved SoC-rising-past-target branch. The candidate's suspected items (what selected markturner's 17:30 window) were dropped as issue-specific.ECO Modebranch atprediction.pyon main. New symptom-table row.get_header_html) folded into the Web row; the vacuous-red-gate trap (a test importing a not-yet-added constant fails at import and skips every test in the module) added as a new bullet under "Traps when investigating".Existing entries corrected (3)
PredBat/HA fixture" (GH#5079 section opening) — no longer true: PR test: give each test its own PredBat rather than sharing one (#5079) #5102 merged,unit_test.pynow builds acreate_predbat()per registry test; reworded, with the non-registry paths still sharing one instance and the known pairs marked as pre-test: give each test its own PredBat rather than sharing one (#5079) #5102 finds. This is the re-check candidate 5100 predicted would be needed once fix(plan): save_plan() expiry computed from the wrong clock (#5079) #5100/test: give each test its own PredBat rather than sharing one (#5079) #5102 merged.minutes_nowresidue trap (GH#5026) — the fixture-level pin it said would close the class has landed:create_predbat()now pinsminutes_now = FIXTURE_MINUTES_NOW(720) andnow_utcfrom the pinnedmidnight_utc; updated to say the class is closed for registry tests on current main.self.base.argstrap — "inverters are rebuilt from scratch on the balance path and whenevernum_inverterschanges" corrected to "rebuilt every 5-minute cycle" (GH#5099's verification, against currentexecute.py), with the pre-Fold balance_inverters into execute: one source of inverter rate writes #5149 balance-path construction marked gone.Dropped (0)
None — every candidate in the slice verified or was rewritten with its mechanism kept.
Notes
uncalibrated(introduced to the codebase by the merged fix(solar): calibrate PV against the raw provider forecast, not Predbat's own calibrated output #5121).run_pre_commitran end to end; the pre-commit hooks (cspell, markdownlint, formatting) passed — the run then aborted attest_teslemetry_local_weekday_follows_the_base_clockat 00:09 BST, the known host-clock-window failure documented in this journal ("Build test dates from Predbat's clock": the test asserts its no-base fallback against the host'sdatetime.now().weekday()while the fallback isdatetime.now(timezone.utc), so it crashes in the hour after local midnight until 01:00). Docs-only change; unrelated to this diff.