Skip to content

docs(debug-journal): fold in the 2026-10-12 queue slice (11 candidates); mark GH#5438/#5492 fixed (#5439/#5493), GH#5472 and the GH#4900 compare-plan leak fixed (#5488/#5465), sigenergy docs split fixed (#5464), ML retrain interval configurable (#5433) - #5528

Merged
springfall2008 merged 1 commit into
mainfrom
bot/debug-journal-2026-10-13
Oct 13, 2026

Conversation

@springfall2008

Copy link
Copy Markdown
Owner

Automated journal flush — generated by the triage bot's /journal-update run of 2026-10-13. The queue held 11 candidates (limit 15), so none are left queued. Docs-only change: tools/debug-journal.md plus one genuine term added to .cspell/custom-dictionary-workspace.txt (entityfilter, the HA recorder-filter module cited by the new note). Every candidate was re-verified against main at 40b9ce28 before folding in; verification status is stated per item below.

Quality gate: all pre-commit hooks green (cspell, markdownlint, formatters, and the two test suites wired into hooks) after rewording four ordinary-English words and one inline-HTML token. The quick Predbat suite (coverage/run_all --quick) did not run this session — its runner is not permitted in the session's permission mode. This is a docs-only change, so the docs hooks are the gate, and they passed; saying so plainly rather than implying a suite run.

Folded in (10 of 11 candidates; one folded only as a pointer)

  • GH#4180 (30s GivTCP settle sleep) — folded. Verified on main: if changed_start_end: is unconditional (inverter.py), has_service_api has zero occurrences. Sharpened the two existing sleep mentions (Solis row, GE-discharge-window symptom row): the v8.37.x rest_data GivTCP-REST exemption is gone since refactor(givtcp): move GivTCP REST handling into its own component #4864, so the sleep applies to GivTCP-REST too, and the charge path never reaches it. Per-issue call-site detail stays in the issue.
  • GH#5502 (Kraken standing charge) — folded, rewriting the Kraken row's "suspected, no probe" sentence to probe-confirmed: async_fetch_standing_charges() reads results[0].value_inc_vat unfiltered (kraken.py), live EDF returned the overlapping NON_DIRECT_DEBIT row first (61.186 vs the Direct-Debit 52.317), the unit rate survives only by row order in minute_data()'s last-write-wins merge, filter_payment_method() (utils.py, fix(octopus): prefer the direct debit rate when both payment methods overlap #5145) replays correct on those rows, and the GraphQL fallback reads applicable_charges[0] with no method field requested. The 36-row structure comes from that run's live probe (2026-10-12); the code side re-verified here.
  • GH#5503 (stale next-day rates) — folded into the Octopus row. Verified: fetch_octopus_rates() concatenates previous→current→next event slices (octopus.py) with minute_data() last-write-wins, and the next-day read is the only one of the three with no empty-warn. The public-API provenance recipe and the same-code tariff-renewal trap are carried as probe-verified in that run.
  • GH#5507 (recorder include-glob flips to whitelist) — verified on main (the FAQ example at docs/faq.md now ships the include glob; compare entities are predbat.compare_tariff_<id>), folded into the compare row: HA's recorder filter has two fall-through modes and any domain/glob include excludes everything matching no include rule; the no-flip rescue lists each predbat.compare_tariff_<id> under include.entities explicitly.
  • GH#5508 (mixed-fleet plan not deliverable → dup of Predbat takes account of dissimilar battery capacities in the plan #830) — dropped as per-issue narrative (the dup classification and fix-ranking texture belongs on Predbat takes account of dissimilar battery capacities in the plan #830/Multi-inverter rework: tracking issue #4856, where it already lives); folded only the reusable class pointer into the charge/discharge-curve row's Charge / discharge curve only works for one inverter #5442 text: the single-battery prediction model (battery_to_min on fleet SoC + the execute.py fleet sums, both re-verified), dup target Predbat takes account of dissimilar battery capacities in the plan #830, umbrella Multi-inverter rework: tracking issue #4856, and the two-grep class-check against a future per-inverter prediction rework.
  • GH#5509 (export-then-refill arbitrage + dump reading) — folded: a new symptom row (the governing comparison is sell price vs refill price, not vs simultaneous import; best_soc_keep is the knob) plus dump-reading notes in the replay section (plan-relative vs absolute minutes; export_window_best[].target ≠ the packed export_limits_best[] level; the table card's rate columns are not SoC).
  • GH#5517 (manual-select prune re-prices history) — verified (manual_rates() prunes elapsed slots and rewrites every cycle, the dropdown offers only future slots, today_cost() re-prices from the live tables, and Retain past manual import/export rate overrides so cost_today and the savings baseline price consumed energy correctly #5078/Persist effective rates for historical cost calculations #5425 are still open). Folded into the manual-override-selects row.
  • GH#5520 (elapsed half hours of a live dispatch priced at base until the completed feed lists them) — folded into the Octopus row as the third trigger of the predbat.cost_today ammount jumps after unplugging car from charge #2785/IOG extra car charging slots still not holding the off-peak rate #5413/Manual rate overrides are dropped once a slot passes, so cost_today re-prices that slot at the base tariff #5517 cost_today family, with the "Elapsed minutes keep theirs" comment trap, the Octopus Intelligent: in-progress flexPlannedDispatches promoted to completed creates phantom cheap slots when EV unplugged #4114 no-promotion constraint, and the calculate_yesterday freeze note. trim_started_dispatch()/dispatch_billed_off_peak() and the round-out re-verified.
  • GH#5521 (Kraken SmartFlex dispatches ride the Octopus rule machinery) — folded into the Kraken row. One claim rewritten during verification: the candidate's proposed fix lever kraken_provider arg does not exist on main — the provider is a component instance attribute (self.provider), not a shared arg, so a provider-dependent pricing rule needs new wiring, not an existing knob (stated as such in the entry).
  • GH#5522 (frozen-SoC fingerprint) — verified (soc_kw = soc_percent × soc_max / 100 with no staleness guard, inverter.py) and folded into the SoC symptom row: the fingerprint, the mixed-GivTCP-prefix apps.yaml trap, and the "helper averages a different prefix, so both sides were truthful" reading.
  • GH#5523 (LoadML bypasses holiday mode) — verified (load_forecast_only gates the holiday-aware forecast behind not self.load_forecast_only; zero holiday mentions in load_ml_component.py; load_ml_source is apps.yaml-only) and folded into the holiday row.

Existing entries corrected against the 2026-10-12 merges

Not counted, and honest caveats

  • Dropped outright: only GH#5508's narrative (kept as a class pointer, above).
  • One candidate claim corrected rather than folded: GH#5521's kraken_provider arg (does not exist on main).
  • The quick Predbat suite did not run this session (permission mode); the docs-relevant hooks all passed, and no code changed.
  • Nothing was left queued (11 candidates, limit 15).

🤖 Generated with Claude Code

…s); mark GH#5438/#5492 fixed (#5439/#5493), GH#5472 and the GH#4900 compare-plan leak fixed (#5488/#5465), sigenergy docs split fixed (#5464), ML retrain interval configurable (#5433)

Co-Authored-By: Claude Code <noreply@anthropic.com>
@springfall2008
springfall2008 marked this pull request as ready for review October 13, 2026 07:31
Copilot AI balanced review requested due to automatic review settings October 13, 2026 07:31
@springfall2008
springfall2008 merged commit a3d8e8b into main Oct 13, 2026
2 checks passed
@springfall2008
springfall2008 deleted the bot/debug-journal-2026-10-13 branch October 13, 2026 07:31

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.

🟡 Changes recommended

Three journal statements are inaccurate or stale and could misdirect future triage.

1 open finding
What changed in this PR

Updates the debug journal with newly verified diagnostic findings and merged-fix status.

Changes:

  • Adds findings from 11 triage candidates.
  • Updates entries for recently merged fixes.
  • Adds entityfilter to the spelling dictionary.
File Description
tools/​debug-journal.md Updates diagnostic guidance and issue history.
.cspell/​custom-dictionary-workspace.txt Adds the new recorder-filter term.

🧠 Review effort: Balanced


💡 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
| Solis Modbus (`GS`, `GS_fb00` in `inverter.py`) | The Energy Storage Control Switch (Modbus 43110, `energy_control_switch`) is driven by `alt_charge_discharge_enable()`, gated on the `has_solis_energy_control` capability. `GS` reaches it through `mimic_target_soc()` and uses the Timed Charge/Discharge bit as the charge enable (35/33). `GS_fb00` has a target SoC, so it never reached `mimic_target_soc()` - until the fix it never wrote the switch at all, and a switch left on `Self-Use - No Grid Charging` (1, bit 5 clear) made every charge slot hold the battery at 0 W while Predbat logged a correct slot, target and rate. For FB00 it is driven through `write_solis_energy_control()` from `adjust_charge_immediate()` on non-exporting cycles - `Backup/Reserve` (49) to charge or idle, `Backup/Reserve - No Grid Charging` (17) for a freeze or a car/iBoost/reserve hold - and from `adjust_export_immediate()` on exporting cycles - `Feed-in priority - No Grid Charging` (64) for a freeze export, `Backup/Reserve` for a real export (PR #5258). The Battery Reserve bit (bit 4) is what makes the template's `reserve` (`backup_mode_soc`, 43024) a discharge floor, so the reserve holds in `execute_plan()` work unchanged; grid charging goes off during holds because the reserve is raised to SoC + 1 and the inverter would import to reach it, again each cycle. `execute_plan()` never calls both immediate methods with a real target in one cycle, and the idle `adjust_export_immediate(100)` deliberately leaves the switch alone; GS still writes from `mimic_target_soc()`. Adding a writer anywhere else (e.g. `adjust_battery_target()`) flips the switch twice per cycle. Older logs: PR #5256 (merged 2026-09-26) held it on `Self-Use` (33) from `adjust_battery_target()`, and before that GS_fb00 never wrote the switch at all. A SolisCloud session is the usual way the switch ends up at 1: `solis.py` maps `"Self-Use - No Timed Charge/Discharge"` to `ENUM_SELF_USE_NO_GRID_CHARGING`, which clears bit 5, and writes it whenever no slot is active - so stopping the cloud component outside a slot leaves grid charging disallowed. The FB00 Solax Modbus plugin's option names differ from the pre-FB00 plugin's (`Self-Use` is 33 there, 35 here) - `SOLAX_SOLIS_MODES_FB00`. Verified from a user log (26 Sep 2026) and the plugin source. | `./run_all --test inverter` (`solis_fb00_*`, `solis_gs_*`) |
| SolaX (`solax.py`) | Code `10402` is a token/auth failure, retried in-request rather than waiting for the next cycle. SolaX clamps battery minimum SOC at 10% (`SOLAX_MIN_RESERVE_PERCENT`), so `battery_min_soc` is auto-configured to stop Predbat writing limits the inverter will reject. **Follow-up on the GH#4993 thread (2026-09-21, from a 23-hour reporter log):** `10402` repeating every cycle with data still flowing is **server-side token invalidation between cycles**, not broken auth or lost data — Predbat's in-request refresh recovers every cycle. The tell: the same token value succeeds mid-cycle and is rejected next cycle while Predbat holds it untouched in memory between cycles (the auth-error branch of `_request_get_impl()` clears it, and `request_wrapper()` retries with a fresh token instead of failing the fetch), and a stated ~30-day `expires_in` against a real ~1-cycle token lifetime. Prime suspect is a second client sharing the same `solax_client_id`/`solax_client_secret` — the reporter ran the separate SolaX Developer API HA integration — **suspected, not proven** (one-active-token-per-app is common but could not be confirmed against SolaX docs); the decisive test is disabling the second client for an hour or registering each client its own app. Count `error 10402` lines against `Fetched ... successfully` lines before calling data lost, and note each 10402 also increments the component's lifetime error counter (`solax.py` auth-error branch), which is what accumulates in the Components panel — do not "fix" this by removing the in-request retry. Secondary, same thread: `Warn: SolarAPI: No solar forecast data was returned from HA sensors.` with the SolaX Cloud template means **no PV forecast source is configured at all** — with `forecast_solar`/`open_meteo`/`solcast_host`+`solcast_api_key` all absent, `fetch_pv_forecast()` falls through to the HA-sensor branch (`solcast.py` sets `active_source = "ha_sensors"`) and the template's `pv_forecast_today`-style `re:` regexes (`templates/solax_cloud.yaml`) match nothing unless the Solcast HA integration is installed. Check which source the log names ("Obtaining solar forecast from Forecast Solar API" vs "Using Solcast integration from inside HA") — a working source dropped during a template migration looks exactly like this. **Plant `totalYield` overstates PV on a mixed plant (GH#5356, code-verified on main 2026-10-02, payload semantics rest on the reporter's plant+device captures):** `automatic_config()` binds `pv_today` to the plant-level `sensor.<prefix>_solax_<plant>_total_yield` (solax.py:447), `publish_plant_info()` fills that sensor straight from plant realtime `totalYield` (solax.py:2640), and `total_load` adds the same `totalYield` to imported+discharged−exported−charged (solax.py:2709) — but SolaX's plant total is PV **plus** every deviceType-1 inverter's AC output, so on a plant mixing a PV-only inverter with a separate AC-coupled battery inverter, battery discharge is counted twice (once in `totalDischarged`, once inside `totalYield`), PV history is inflated (reporter's calibration scaled 2.89x, ~1.6x legitimate), and the class presents as "calibration too high / overnight under-charging / wrong savings". `pv_power` is unaffected (per-device `pvMap`/`mpptMap` sums, solax.py:2276-2285). The ingredient a fix needs all already exist: a device-level `..._<sn>_total_yield` sensor (solax.py:2366), failed reads keep the previous value (solax.py:2245), and `plant_inverters[plant_id]` holds every deviceType-1 device (solax.py:1483). Not a regression — the plant wiring dates to original SolaX Cloud development (#3140). The reporter's proposed fix direction (maintainer call pending): sum device `totalYield` over `plant_inverters` into a new `pv_today` sensor; caveat for whoever builds it — the plant **lifetime** total does not decompose as PV+discharge on that site, so derive from the daily relationship (which matched exactly on three days), not the lifetime number. **PR #5357:** `pv_today` is now `..._pv_yield`, summed by `get_plant_pv_yield()` from each inverter's **last known** `totalYield` (kept per inverter, seeded after a restart from its own `..._<sn>_total_yield` sensor), so a failed read or a dead inverter does not drop out of the sum; an inverter with no `totalYield` of its own counts as 0, and the value is held only when an inverter that `onlineStatus` says should answer failed its read and has never given a yield. `pv_power`/`grid_power` are plant sensors summed over every inverter, no longer the first inverter listed. Device `dailyYield` reads 0.0 once the inverter sleeps in the evening, so it is no use as a daily counter. On a live plant of two DC-coupled hybrids with panels (2026-10-04) device `dailyYield` (23.3, 2.6) and `dailyACOutput` (24.1, 9.5) were separate counters and the plant `dailyYield` (33.6) was exactly the sum of the AC outputs, so the plant figure is inverter AC output on every plant shape seen and device yield is not; device yield was not checked against an independent PV measurement. Still wrong after the fix: a plant whose only inverter is an X1-AC (no `totalYield`, empty `mpptMap`/`pvMap`) falls back to the plant figure, which there is battery discharge (`dailyYield` = `dailyACOutput` = `dailyDischarged`), and its `load_power` goes negative when a third-party PV inverter SolaX cannot see is exporting. `plant_inverters` is keyed by the plant ID as SolaX gives it while entity names use the lowercased form - look up with the raw ID. **GH#5388 (2026-10-04):** a plant lifetime counter can dip and recover within a day (`totalYield` 4437.8 → 4260.4 → 4441.7, `totalDischarged` with it) and, published as received, the recovery was recorded as 181.3 kWh of PV that day - an absurd `pv_today` or `export_today` on one day followed by a PV forecast several times the array size is this, via calibration. An upward step of thousands of kWh on the same counters did not show in `pv_today`. `hold_counter_dip()` now holds the five plant totals and each inverter's `total_yield` at the last published value and takes a lower value as a genuine reset only after `SOLAX_COUNTER_DIP_HOLD_HOURS`; how long a real dip lasts was not measured. After a restart the last value is seeded from the sensor, or from the `counters` entry in storage when the sensor is gone (Predbat's sensors do not survive a Home Assistant restart); the time a dip started is saved there too, without it a restart began the hold again and a system restarted daily would never accept a genuine reset. A plant where no inverter has PV inputs (X1-AC only: no `totalYield`, empty `mpptMap`/`pvMap`) now publishes **0** for `pv_yield` instead of the plant total, with a one-off `has no inverter with PV inputs` warning; `total_load` then has no PV term, so on a site with third-party PV it under-reads by what that PV supplies and can fall while the site exports. Nothing in the SolaX data measures that PV: a 12 hour dump of `/openapi/v2/device/history_data` (params `snList`, `deviceType`, `startTime`/`endTime` in ms, `timeInterval` minutes, `businessType`) carried the same fields as realtime, yield null and `gridPowerM2` 0.0 throughout daylight, and no plant checked had a `deviceType=3` meter device. `M2` fields are meter 2, positive = export per the SolaX API text; `solax.py` does not read them, and a meter 2 that was once fitted shows as a non-zero `totalExportEnergyM2` that no longer moves. A hybrid with no panels still reports a populated `mpptMap` of zeros, so it counts as having PV inputs and is not covered by the 0. | `solax` |
| Sigenergy (`sigenergy.py`) | Lifetime/history totals are cumulative server-side and reset around EU midnight, which showed up as an overnight dip; `fetch_history_totals` applies a monotonic clamp. "Energy totals went backwards overnight" starts here. Separately, a user `apps.yaml` override of `inverter.has_reserve_soc: true` (stock `SIG` default is `False`, deliberately — reverted in `5bfc5c80` after #2873/#3124) bypasses two guards that normally make reserve writes inert for Sigenergy (`execute.py:947-950`, `inverter.py:547-549`), so `adjust_reserve()` writes straight to `number.sigen_plant_ess_discharge_cut_off_state_of_charge`. Confirmed from a reporter's log: zero occurrences of the "Inverter does not support reserve" disable message the first guard would normally log, plus 56 reserve writes landing on that register (GH#4728). The stock template's `reserve:` line also points at that same entity rather than `ess_backup_state_of_charge`, which is what makes the override immediately destructive rather than merely inert. `Applying mode=eco ... duration=720min` is emitted only by the no-window fall-through branch (`sigenergy.py:2199`), so seeing it while a plan export window is live means the component's control snapshot never saw the window. Two code-verified routes to that, both still live: with `sigenergy_automatic: False`, `automatic_config()` - the only wiring of the component's control entities into Predbat - never runs, so Predbat's plan writes land on its own simulated dummy entities (`inverter.py:539-545`) and a `True == True` read-back is Predbat comparing against itself rather than reading a register; and `fetch_controls` runs on the first tick only (`if first:`, `sigenergy.py:2658`), so a single missed `call_service` event leaves the component's internal state permanently out of step while Predbat's write-skip comparison guarantees the healing write is never re-issued (GH#4894, probe-verified in `test_sigenergy.py`). `_publish_mqtt` logged the live `accessToken` on every command until PR #4926 added `SigenergyAPI.redact()`. The docs template still writes `number.sigen_plant_grid_import_limitation` to 0 in both freeze branches (`docs/inverter-setup.md:1936-1940` and `:1971-1975`), and Sigenergy's own EVAC/EVDC chargers are EMS-throttled to the plant import limit - so the block lands exactly when Predbat asks for Freeze Charging to hold for a car (GH#4911). On the **stock** `has_reserve_soc: False` shape the planned reserve floor is model-only and the battery drains to the inverter's own cut-off — see the "Plan shows a flat hold at the reserve" symptom row (GH#5358). **Every power lever on SIGCLOUD has been silently inert (GH#5376, hardware-verified by the reporter's live MQTT log and battery data; **fixed in PR #5377, merged 2026-10-03 and first shipped in v9.3.5** — the six optional fields are now built into the command object, `sigenergy.py` — keep the pre-#5377 reading for older logs):** `send_battery_command()` writes its six optional power/priority fields — `chargingPower`, `pvPower`, `maxSellPower`, `maxPurchasePower`, `chargePriorityType`, `dischargePriorityType` — at the **top level** of the payload while the required command fields sit inside `commands[0]` (since `cba12cc3`, first released v8.36.11); the API reads the levers from the command objects, so the top-level fields are silently ignored, and planned charge rate, bidirectional export cap, freeze-charge/`freeze_export_hold` `chargingPower: 0` and both priority types (all set in `apply_controls()`, `sigenergy.py`) have never reached the hardware. The suite passed while the bug was live because `test_sigenergy.py` asserted `payload["chargingPower"]` at the top level while checking `commands[0]` for the required fields — **a test that locks in the implementation's current output makes suite-green meaningless for that surface**; the #5377 fix rewrote the test to assert inside `commands[0]` (its "optional fields that were not passed are omitted" cases are the in-tree example of testing the fixed shape). Suspect, not verified: the fixed surface has never been hardware-tested by the triage bot, so a post-v9.3.5 regression report about holds/exports on SIGCLOUD may be that surface going live for the first time; pre-#5377, the #4761 freeze-export reasoning and the `freeze_export_hold` branch never acted with real effect. **Docs entity-name split (GH#5459, docs defect, fix in flight on PR #5464):** `docs/inverter-setup.md`'s `predbat_max_*_limit_action` automations reference `sensor.sigen_inverter_ess_rated_charging_power`/`..._discharging_power` - inverter level with the plant suffix - while the upstream integration defines the rated-power pairs at two levels with different suffix spellings: plant `plant_ess_rated_charging/discharging_power` (-ing) and inverter `inverter_ess_rated_charge/discharge_power` (no -ing), so the docs names match neither set; the docs' own rename note points at the inverter alternates, and the template binds `battery_rate_max` to the plant-level `-ing` name (the right clamp level for the plant-level limit numbers the automations write). A Sigenergy "float got invalid input 'unknown'" automation error resolves by mapping level first (plant vs inverter), then suffix; the automations are docs content and no Predbat code creates them. | `sigenergy` |
| Sigenergy (`sigenergy.py`) | Lifetime/history totals are cumulative server-side and reset around EU midnight, which showed up as an overnight dip; `fetch_history_totals` applies a monotonic clamp. "Energy totals went backwards overnight" starts here. Separately, a user `apps.yaml` override of `inverter.has_reserve_soc: true` (stock `SIG` default is `False`, deliberately — reverted in `5bfc5c80` after #2873/#3124) bypasses two guards that normally make reserve writes inert for Sigenergy (`execute.py:947-950`, `inverter.py:547-549`), so `adjust_reserve()` writes straight to `number.sigen_plant_ess_discharge_cut_off_state_of_charge`. Confirmed from a reporter's log: zero occurrences of the "Inverter does not support reserve" disable message the first guard would normally log, plus 56 reserve writes landing on that register (GH#4728). The stock template's `reserve:` line also points at that same entity rather than `ess_backup_state_of_charge`, which is what makes the override immediately destructive rather than merely inert. `Applying mode=eco ... duration=720min` is emitted only by the no-window fall-through branch (`sigenergy.py:2199`), so seeing it while a plan export window is live means the component's control snapshot never saw the window. Two code-verified routes to that, both still live: with `sigenergy_automatic: False`, `automatic_config()` - the only wiring of the component's control entities into Predbat - never runs, so Predbat's plan writes land on its own simulated dummy entities (`inverter.py:539-545`) and a `True == True` read-back is Predbat comparing against itself rather than reading a register; and `fetch_controls` runs on the first tick only (`if first:`, `sigenergy.py:2658`), so a single missed `call_service` event leaves the component's internal state permanently out of step while Predbat's write-skip comparison guarantees the healing write is never re-issued (GH#4894, probe-verified in `test_sigenergy.py`). `_publish_mqtt` logged the live `accessToken` on every command until PR #4926 added `SigenergyAPI.redact()`. The docs template still writes `number.sigen_plant_grid_import_limitation` to 0 in both freeze branches (`docs/inverter-setup.md:1936-1940` and `:1971-1975`), and Sigenergy's own EVAC/EVDC chargers are EMS-throttled to the plant import limit - so the block lands exactly when Predbat asks for Freeze Charging to hold for a car (GH#4911). On the **stock** `has_reserve_soc: False` shape the planned reserve floor is model-only and the battery drains to the inverter's own cut-off — see the "Plan shows a flat hold at the reserve" symptom row (GH#5358). **Every power lever on SIGCLOUD has been silently inert (GH#5376, hardware-verified by the reporter's live MQTT log and battery data; **fixed in PR #5377, merged 2026-10-03 and first shipped in v9.3.5** — the six optional fields are now built into the command object, `sigenergy.py` — keep the pre-#5377 reading for older logs):** `send_battery_command()` writes its six optional power/priority fields — `chargingPower`, `pvPower`, `maxSellPower`, `maxPurchasePower`, `chargePriorityType`, `dischargePriorityType` — at the **top level** of the payload while the required command fields sit inside `commands[0]` (since `cba12cc3`, first released v8.36.11); the API reads the levers from the command objects, so the top-level fields are silently ignored, and planned charge rate, bidirectional export cap, freeze-charge/`freeze_export_hold` `chargingPower: 0` and both priority types (all set in `apply_controls()`, `sigenergy.py`) have never reached the hardware. The suite passed while the bug was live because `test_sigenergy.py` asserted `payload["chargingPower"]` at the top level while checking `commands[0]` for the required fields — **a test that locks in the implementation's current output makes suite-green meaningless for that surface**; the #5377 fix rewrote the test to assert inside `commands[0]` (its "optional fields that were not passed are omitted" cases are the in-tree example of testing the fixed shape). Suspect, not verified: the fixed surface has never been hardware-tested by the triage bot, so a post-v9.3.5 regression report about holds/exports on SIGCLOUD may be that surface going live for the first time; pre-#5377, the #4761 freeze-export reasoning and the `freeze_export_hold` branch never acted with real effect. **Docs entity-name split (GH#5459, docs defect, fixed in PR #5464, `c0d901db`, 2026-10-12):** `docs/inverter-setup.md`'s `predbat_max_*_limit_action` automations reference `sensor.sigen_inverter_ess_rated_charging_power`/`..._discharging_power` - inverter level with the plant suffix - while the upstream integration defines the rated-power pairs at two levels with different suffix spellings: plant `plant_ess_rated_charging/discharging_power` (-ing) and inverter `inverter_ess_rated_charge/discharge_power` (no -ing), so the docs names match neither set; the docs' own rename note points at the inverter alternates, and the template binds `battery_rate_max` to the plant-level `-ing` name (the right clamp level for the plant-level limit numbers the automations write). A Sigenergy "float got invalid input 'unknown'" automation error resolves by mapping level first (plant vs inverter), then suffix; the automations are docs content and no Predbat code creates them. | `sigenergy` |
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.

2 participants