Repository navigation
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
Merged
Conversation
springfall2008
marked this pull request as ready for review
October 2, 2026 07:16
Copilot stopped reviewing on behalf of
springfall2008 due to an error
October 2, 2026 07:16
Contributor
There was a problem hiding this comment.
Note
Copilot was unable to run its full agentic suite in this review.
Copilot review overview
Review effort: Lite
Findings: 1
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
solaxquick-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.
| | 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` | |
| | 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. | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


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
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: falsezeroing 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_minutesfirst shipped in v8.27.5 (git tag --contains db3b78c3 | sort -V— the naive listing ordered v8.27.10 first), commitsdb3b78c3(Make default 30 minute timeslot configurable with plan_interval_minutes #2865) +91f60d51(Fix for plan interval minutes, Fix Solcast Timezone issue #2875).adjust_export_immediate()'s whole freeze path sits insideif target_soc < 100:, so at a full batterydischarge_freeze_servicenever fires, the explicit charge-side stop never runs (the else only issuescharge_stop_servicewhendischarge_stop_serviceis unconfigured), and the Solis FB00 freeze mode-64 write is skipped; reporter A/B-verified via helper hooks on a Victron-shapedSKplaceholder. Folded the companioncall_service_template()return-semantics note (False only for unconfigured keys; dedup hits are not failures).626129d8): each car's energy is added once;tests/test_multi_car_load.pypins 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.sois 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_REVISIONnow 16.dynamic_load_car_*chain on main, the strip log line, thedispatch_billed_off_peak()completed-vs-tail policy tension, and theoctopus_intelligent_dynamic: falseworkaround (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.car_energy_reported_load-off effect chain (kills the low-load test, force-clearscar_charging_holdin fetch.py and load_ml_component.py, leaves the discharge hold untouched, and trips the "their Intelligent slots will never be trusted" tripwire whenoctopus_intelligent_trust_slotsis also off); the provenance split (cancel machinery079509cafirst tagged v9.3.0 — git-verified — whilecar_energy_reported_loaditself 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); thedispatch_timeline_pending"unplugged"-label trap; and thepv_forecast_minute_step: {}replay KeyError without--redo.car_charging_nowat 2 minutes exact-clock vs the low-load test at 5 minutes on the 5-minute grid),car_charging_energyis never consulted (the gap the enhancement asks for), and the reading machinery that exists (get_from_incrementing()clamping at 0).battery_rate_max_charge = min(inverter_limit_charge, battery_rate_max_raw)/MINUTE_WATT, the skip-gate andwrite_and_poll_value(fuzzy=…)both size…× MINUTE_WATT / 20, the REST layer's widerwrite_tolerance_watts()/12is 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)
fix/iog-slot-io-adjustedbranch; PR fix(octopus): model a vanishing IOG dispatch for octopus_intelligent_slot users #5304 merged 2026-09-30 (carryinga9af46a3, which models a vanishing dispatch foroctopus_intelligent_slotusers). Reworded to fixed-on-main, mechanism kept for pre-fix(octopus): model a vanishing IOG dispatch for octopus_intelligent_slot users #5304 logs.battery_size_tracking()re-publishes the sensor from the recovered recorder entry when the live entity is missing, behind a string-key/finite-value guard (a7d12462), and the regression test now asserts re-publication and no double-publish. Row rewritten as fixed; the pre-fix(inverter): restore calculated capacity sensor after restart #5283 test-trap kept as guidance.f052cd59+385dddac): the integer3HY-<n>segment is parsed (bounded soGW2/G3suffixes are never read as a rating), with null-model/trailing-punctuation guards and a warn-once fallback. Pre-fix(gecloud): parse the integer rating of 3-phase hybrid models (GIV-3HY-11) #5293 mechanism kept. The entry's other half —publish_info()reading onlymax_charge_rate, nevermax_discharge_rate— re-verified still live and said so.sysCommandmetadata across six slot CIDs; PR fix(solis): stop trusting SolisCloud's slot metadata, and sweep the probe to a half amp #5314 merged 2026-09-30 stops trusting that meta, and PR fix(solis): probe the slot current ceiling the inverter enforces but does not report #5309 merged 2026-09-30 added the--probe-ceilingmeasurement (a measured ceiling replaces the rated estimate; half-amp sweep;describe_slot_current_limits()names every input) plus theautomatic_config()binding ofinverter_limit_charge/_dischargeto the newslot_{charge,discharge}_power_maxsensors — so the planner now knows the clamp. Both paragraphs rewritten; pre-fix(solis): stop trusting SolisCloud's slot metadata, and sweep the probe to a half amp #5314 mechanisms kept.coordinator.py) touched a subsystem the journal does not yet cover — no entry invalidated, nothing corrected.Quality gates:
python3 -m pre_commit run --all-filespasses (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.pyasserts the localdatetime.now()month against the component'sdatetime.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