Skip to content

docs(debug-journal): fold in the 2026-10-01 queue slice (9 candidates); mark GH#5313 fixed by PR #5315, GH#5136 by PR #5293, GH#5222 by PR #5283, the iog-slot-io-adjusted row by PR #5304, GH#5068 reworked by PRs #5309/#5314; add the dynamic-load car cancel row and the GH#2649/#5302/#5324 findings - #5327

Merged
springfall2008 merged 2 commits into
mainfrom
bot/debug-journal-2026-10-01
Oct 2, 2026

Conversation

@springfall2008

Copy link
Copy Markdown
Owner

Automated daily journal flush — the 2026-09-30 queue sliced into tools/debug-journal.md, every candidate re-verified against current main (57ec7bf1, v9.3.2-46) before landing. Nothing is left queued: the queue held exactly 9 candidates (limit 15), all folded in below.

Folded in, per candidate

  • GH#2649 (GE-mode discharge window times appear only once the window starts; HA Activity echoes) — folded as a new symptom-table row. Verified on main: adjust_force_export()'s GE-mode times-nulling (inverter.py, "no point in changing times before we enable export"), the execute.py pre-window/engagement/clear call sites, inverter_set_charge_before: false zeroing both look-aheads (fetch.py) though the docs describe it as charge-only, the freeze-sentinel (99) never-engages reading (export_mode_of(), utils.py), and the HA Activity "triggered by action" discriminator. One sharpening from verification: set_window_minutes = plan_interval_minutes first shipped in v8.27.5 (git tag --contains db3b78c3 | sort -V — the naive listing ordered v8.27.10 first), commits db3b78c3 (Make default 30 minute timeslot configurable with plan_interval_minutes #2865) + 91f60d51 (Fix for plan interval minutes, Fix Solcast Timezone issue #2875).
  • GH#5302 (explicit freeze export at exactly 100% SoC reroutes to idle/stop) — folded into the HA write/verify row. Verified live on main: adjust_export_immediate()'s whole freeze path sits inside if target_soc < 100:, so at a full battery discharge_freeze_service never fires, the explicit charge-side stop never runs (the else only issues charge_stop_service when discharge_stop_service is unconfigured), and the Solis FB00 freeze mode-64 write is skipped; reporter A/B-verified via helper hooks on a Victron-shaped SK placeholder. Folded the companion call_service_template() return-semantics note (False only for unconfigured keys; dedup hits are not failures).
  • GH#5313 (multi-car overlapping-slot load overcount, parity blind spot) — fixed on main since it was written (PR fix(prediction): count each charging car's energy once in the house load #5315, 626129d8): each car's energy is added once; tests/test_multi_car_load.py pins it. Folded as fixed with the mechanism kept for pre-fix(prediction): count each charging car's energy once in the house load #5315 logs, plus the two transferable lessons (kernel_parity cannot catch a bug mirrored in both engines — probe expected values, not engine agreement; and re-run a kernel-touching probe once after a parity-revision bump so the freshly-built .so is the one validated). The candidate's "PR fix(octopus): model a vanishing IOG dispatch for octopus_intelligent_slot users #5304 also bumps the revision 14→15" coordination note is resolved: both merged, KERNEL_PARITY_REVISION/PK_PARITY_REVISION now 16.
  • GH#5316 (mid-dispatch car stop cancels the house's cheap slot remainder) — folded into a new "Dynamic-load car cancel" row. Verified: the dynamic_load_car_* chain on main, the strip log line, the dispatch_billed_off_peak() completed-vs-tail policy tension, and the octopus_intelligent_dynamic: false workaround (cost: re-opens the withdrawn-slot exposure). PR fix(octopus): keep a started IOG dispatch's half hour cheap after the car stops #5319 remains open (verified on GitHub just now) — recorded as open, not merged.
  • GH#5317 (three candidates, one ticket) — all folded into the same new row: the false-"not charging" low-load test on load-excluded installs; the maintainer-endorsed car_energy_reported_load-off effect chain (kills the low-load test, force-clears car_charging_hold in fetch.py and load_ml_component.py, leaves the discharge hold untouched, and trips the "their Intelligent slots will never be trusted" tripwire when octopus_intelligent_trust_slots is also off); the provenance split (cancel machinery 079509ca first tagged v9.3.0 — git-verified — while car_energy_reported_load itself is New feature to supress car charging hold and correctly model car chargers outside CT clamp #3455/v8.33.8 default-On, so an HA entity-history date is update noise, not the code arriving); the dispatch_timeline_pending "unplugged"-label trap; and the pv_forecast_minute_step: {} replay KeyError without --redo.
  • GH#5318 (dispatch-cancel evidence tiers) — folded into the same row: exactly two evidence tiers (car_charging_now at 2 minutes exact-clock vs the low-load test at 5 minutes on the 5-minute grid), car_charging_energy is never consulted (the gap the enhancement asks for), and the reading machinery that exists (get_from_incrementing() clamping at 0).
  • GH#5324 (entity-write verify tolerance scaled from the capped rate) — folded into the "Hundreds of identical didn't complete" symptom row. Verified on main: battery_rate_max_charge = min(inverter_limit_charge, battery_rate_max_raw)/MINUTE_WATT, the skip-gate and write_and_poll_value(fuzzy=…) both size …× MINUTE_WATT / 20, the REST layer's wider write_tolerance_watts()/12 is immune to the cap, the stable-quantised-read-back signature (GE 1300→1206), and the register-exact-override workaround. Still live; no PR open.

All 9 candidates verified — none dropped, none left marked-suspected that could be upgraded (two of #5318's "would the energy tier have fixed #5317" replay questions stay explicitly suspected). Two candidates had details corrected during verification (noted above).

Existing entries corrected (merges since the last flush)

Quality gates: python3 -m pre_commit run --all-files passes (cspell and markdownlint included; the dictionary needed no new words). The quick suite at this flush's run time (00:15 BST) reports one module failure — solax, with an empty failure detail — which is pre-existing and unrelated to this docs-only diff: test_solax.py asserts the local datetime.now() month against the component's datetime.now(timezone.utc) month (query_plant_statistics_daily()), which disagree 00:00–01:00 local in summer; the newest member of the midnight-window trap family this journal already documents (a one-line instance has been added to that trap bullet). Every other quick module was green at that hour.

🤖 Generated with Claude Code

CI and others added 2 commits October 1, 2026 00:21
…); mark GH#5313 fixed by PR #5315, GH#5136 by PR #5293, GH#5222 by PR #5283, the iog-slot-io-adjusted row by PR #5304, GH#5068 reworked by PRs #5309/#5314; add the dynamic-load car cancel row and the GH#2649/#5302/#5324 findings

Co-Authored-By: Claude Code <noreply@anthropic.com>
@springfall2008
springfall2008 marked this pull request as ready for review October 2, 2026 07:16
Copilot AI balanced review requested due to automatic review settings October 2, 2026 07:16
@springfall2008
springfall2008 merged commit 41b7360 into main Oct 2, 2026
2 checks passed
@springfall2008
springfall2008 deleted the bot/debug-journal-2026-10-01 branch October 2, 2026 07:16
Copilot stopped reviewing on behalf of springfall2008 due to an error October 2, 2026 07:16

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.

Note

Copilot was unable to run its full agentic suite in this review.

Copilot review overview

Review effort: Lite
Findings: 1 Medium severity · 1 Low severity

Open (2)
What changed in this PR

Updates the internal debugging journal with additional troubleshooting notes around recurring inverter write warnings, GE (GivEnergy-mode) discharge-window portal behavior, and a newly observed time-zone-related test failure.

Changes:

  • Expanded the “Trying to write…” warning entry with a verified quantization/tolerance failure mode and suggested fix direction.
  • Added a new debug-journal case documenting GE-mode discharge window times appearing only once the window starts.
  • Appended a newly observed solax quick-suite failure window (local vs UTC date derivation mismatch) to the testing pitfalls section.
File Description
tools/​debug-journal.md Adds new troubleshooting cases and extends existing ones with additional verified failure modes and references.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread tools/debug-journal.md
| Energy totals jump or reset | Provider-side cumulative counters and any monotonic clamping. |
| Works in HA, broken in Docker/standalone | `ha.py` websocket and `userinterface.py` callback return values. |
| Every charge/export window executes ~N minutes late | The inverter's own clock. Compensation is manual-only via the four `inverter_clock_skew_*` settings. The warn band has moved twice: originally only ≥30 minutes was flagged (restart threshold, `INVERTER_CLOCK_SKEW_RESTART_MINUTES`), so a consistent 25-minute drift sailed under it while still logging a `difference -25.47 minutes` line every cycle (GH#4927); since PR #4991 (`87d4703a`, v9.0.2) `check_clock_skew()` also warns on a moderate 5-29 minute skew, repeated at most hourly per inverter (`INVERTER_CLOCK_SKEW_WARN_MINUTES` = 5, `INVERTER_CLOCK_SKEW_WARN_REPEAT_MINUTES` = 60), so a silent moderate skew is no longer the explanation on a current version — but "current" means v9.0.2+: a reporter on v9.0.1 or earlier is back to "only ≥30 minutes was flagged". Measured from one reporter's log: an export window written at 23:05 drew house-load import until 23:35, and a charge window enabled at 00:30 did not start until ~01:00 (GH#4927). |
| GE (GivEnergy-mode) discharge window times only appear in the inverter portal once the window starts | As-coded, not a bug (GH#2649, verified on main): `adjust_force_export()` nulls the start/end times when `inv_has_ge_inverter_mode and not force_export` ("no point in changing times before we enable export", `inverter.py`); both the pre-window call (execute.py, inside the `set_window_minutes` look-ahead — it passes `False` + the plan times for GE-mode inverters) and the every-cycle outside-window clear (`adjust_force_export(False)`) therefore carry no times, and times are written only by the in-window TARGET engagement branch (`adjust_force_export(True, ...)`) — with the ~10 s-per-polled-write chain (idle start → discharge start → discharge end → enable switch → mode) plus the 30 s "Sleeping (workaround) as start/end of discharge window was just adjusted" settle per real change, so full rate can lag the window start by ~10 min including the inverter's own ramp. The pre-window TARGET programming is gated on `set_window_minutes` (= `plan_interval_minutes` since v8.27.5 — `db3b78c3`/#2865 + `91f60d51`/#2875; a hard-coded 30 on v8.24.x), and `inverter_set_charge_before: false` zeroes **both** the charge and the discharge look-ahead (`fetch.py`, `fetch_config_options()`) though docs/customisation.md describes the switch as charge-only — no discharge-only knob exists (the reporter's ask stands). A freeze-sentinel (99) export limit never engages a timed export on any version (`export_mode_of()`, `utils.py`) — no start/end writes in any cycle until a mid-session replan downgrades the 99 to a real TARGET limit; the DFS window that started 15 min late did exactly that. Discriminator for "Predbat writes X randomly" with an HA Activity screenshot: Predbat writes appear as rows marked **"triggered by action Select: Select"** (attributed to the account owning the HA token); plain "changed to X" rows matching no Predbat log line are the integration echoing inverter state — the echoed `20:58:00` was the inverter's own programmed slot, which Predbat read back as `Base export window [...]`. Per-inverter asymmetry (one unit churns, the other never changes) can also be legitimate write dedup — `adjust_force_export` writes only when that inverter's read-back differs from the plan — but an unmarked echo is the discrimination above, not dedup. | `inverter` |
Comment thread tools/debug-journal.md
| First cheap window charges to 0% / the charge target lands later than the cheap band | A metric tie in the charge-window SoC scan: the running-best adoption is non-strict `<=` (`plan.py`, `(metric + min_improvement_scaled) <= best_metric_first and metric <= best_metric`) and `min_improvement_scaled` gates only against the first (full-charge) candidate, so an exact tie lets the later, lower-SoC candidate replace the fuller one. Ties are realistic because `metric_battery_cycle` defaults 0, making charging 1 kWh cost ≈ its stored-energy credit (GH#5069). Neither knob fixes it: `metric_min_improvement` doesn't cover the running best, and raising `metric_battery_cycle` raises the stored-energy credit too. |
| I set a config item above its maximum and it stays capped — no YAML workaround | An `APPS_SCHEMA` item's `max` is enforced as a hard clamp on **every** value source, not just the HA UI: the input_number branch of `userinterface.py` re-clamps after `float()` (deliberate — an apps.yaml override bypasses the HA entity's own min/max; `Warn: Config item ... clamping to ...` is the diagnostic). GH#5070 instance: `debug_history_count` max 50 while the capture path itself is uncapped (retention is `interval × count` in `_capture_debug_history()`), so widening the window was a schema change — PR #5071 (merged 2026-09-19) raised the max from 50 to 500, with the storage warning in docs/customisation.md (the top of the range is ~2.5 GB on disk at 2-5 MB per snapshot). |
| Hundreds of identical `Trying to write N to X didn't complete got M` warnings overnight | Before concluding control is broken, check whether the planner even had a window in that period: the writes may be the routine idle reset — `execute.py` re-writes charge/discharge rate to max every cycle while not charging so PV can still charge (GH#5073, where the planner's own overnight windows were empty and no charging was lost — pure log noise). `write_and_poll_value()` has no failure dedup: after `INVERTER_MAX_RETRY` attempts it warns once, flags `had_errors` and clears the ledger entry, so the next cycle re-attempts from scratch — a firmware state that permanently refuses a register (SolarEdge preserve-charge holding its charge-limit register at 0 is the first confirmed instance) produces ~10 refused writes + a warning per cycle, indefinitely; any brand with a refuse-silently register maps here. Exception since PR #5297: on the `GWMQTT` (Predbat hub) type the retry policy is 3 attempts, and a same-target control failing twice degrades to one write per 5 minutes until it verifies - the "indefinitely" reading is a non-gateway-type reading only. Two case notes from that investigation: the preserve-mode hypothesis could not be verified from the dump because Predbat reads no SE battery-state entity, so the preserve flag is invisible in debug dumps; and a second execute pass ~46s after the first roughly doubled the warning volume in that log — observed only, mechanism never pinned down. `'Connection to inverter ID N failed'` on SE comes from the HA SolarEdge Modbus Multi integration, not Predbat. |
| Hundreds of identical `Trying to write N to X didn't complete got M` warnings overnight | Before concluding control is broken, check whether the planner even had a window in that period: the writes may be the routine idle reset — `execute.py` re-writes charge/discharge rate to max every cycle while not charging so PV can still charge (GH#5073, where the planner's own overnight windows were empty and no charging was lost — pure log noise). `write_and_poll_value()` has no failure dedup: after `INVERTER_MAX_RETRY` attempts it warns once, flags `had_errors` and clears the ledger entry, so the next cycle re-attempts from scratch — a firmware state that permanently refuses a register (SolarEdge preserve-charge holding its charge-limit register at 0 is the first confirmed instance) produces ~10 refused writes + a warning per cycle, indefinitely; any brand with a refuse-silently register maps here. Exception since PR #5297: on the `GWMQTT` (Predbat hub) type the retry policy is 3 attempts, and a same-target control failing twice degrades to one write per 5 minutes until it verifies - the "indefinitely" reading is a non-gateway-type reading only. **A stable quantised read-back below the verify tolerance never converges (GH#5324, verified on main):** the entity-write tolerance is `battery_rate_max_charge * MINUTE_WATT / 20` — 5% of the **allocated** rate, and `battery_rate_max_charge = min(inverter_limit_charge, battery_rate_max_raw)/MINUTE_WATT` (`inverter.py`) — so an `inverter_limit_charge` cap (including the API override, which is in `CONFIG_API_OVERRIDE`) below ~5 register steps makes a quantised read-back (GE: a 1300 W write sits stably at 1206) fail verify forever: 10 retries plus a full repeat every cycle, while the identical write at the uncapped rate verifies (any allocation below ~1.9 kW on a register-quantised rate is the risky band). The REST layer's tolerance for the same write is sized differently — `write_tolerance_watts()/12` from `Invertor_Max_Bat_Rate` with a 2600 W fallback (`givtcp_rest.py`) — and is immune to the cap, so two layers with different tolerances answer one write and the narrower one decides. The signature is a read-back **stable** at the same value off-target every cycle (never self-heals) - distinct from the settling bounce above, where the value sits between old and target only transiently. Workaround keeps the cap exactly: pick a register-exact override value; fix direction for a maintainer: size the entity-level fuzzy from the inverter max rate or one register step, mirroring the REST layer. Two case notes from that investigation: the preserve-mode hypothesis could not be verified from the dump because Predbat reads no SE battery-state entity, so the preserve flag is invisible in debug dumps; and a second execute pass ~46s after the first roughly doubled the warning volume in that log — observed only, mechanism never pinned down. `'Connection to inverter ID N failed'` on SE comes from the HA SolarEdge Modbus Multi integration, not Predbat. |
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.

battery exporting while EV charging and battery set to demand in cheap rate with IOG

2 participants