Skip to content

fix(sungrow): an outage no longer drops registers or the release (1.6.1) - #202

Merged
frahlg merged 1 commit into
mainfrom
fix/sungrow-outage-latch-signed
Oct 28, 2026
Merged

frahlg merged 1 commit into
mainfrom
fix/sungrow-outage-latch-signed

Conversation

@frahlg

@frahlg frahlg commented Oct 28, 2026

Copy link
Copy Markdown
Member

Problem

FTW's nightly random site rig (master 83c2b24c, 27 October 2026, 10 seeds) takes the Sungrow offline for 20 to 90 s. While it is gone, every read and write fails with a transport error, such as dial tcp …: connection refused or modbus reconnect backoff active. The driver counted those as if the device had refused them, in two places:

Reads (optional_read, triage item D1). Three failed polls dropped a register for the rest of the session. From ftw.log, register N failed 3 reads in a row; treating it as absent:

Seed Registers dropped
207530, 207532, 207533, 207538 12999
207535, 207536 12999, 13000
207534 12999, 13000, 5016
207539 12999, 13000, 5010, 5016

13000 is the power-flow status that signs the battery. Without it, FTW read a discharge as a charge: 105 samples in 207539 (655–1010 s) and 13 in 207534 (968–980 s). PV was lost in 207534 and 207539, and the home-use check failed in 207539.

Writes (note_ems_write / set_self_consumption, the driver half of triage item C1). Three failed EMS writes during the outage marked the block absent (the EMS control block rejected 3 writes in a row appears in 9 of 10 seeds). From then on, the watchdog's release returned true without writing. The inverter kept its last forced command after it came back:

Seed Window (elapsed s) Command
207533 654–888 187 / 2668 W
207535 579–812 170 / 2632 W (charge)
207537 213–510 187 / 3854 W
207538 1034–1209 187 / 4674 W

In 207534 it discharged 5000 W, from 5.6 % to 5.0 %, while FTW was not dispatching it. The comment on the skip said "a write it rejects cannot have latched a forced state". That holds for a device that never took forced mode. It does not hold after an outage.

Change

drivers/lua/sungrow.lua, 1.6.0 → 1.6.1:

  • device_refused(err) is true only for a Modbus exception reply (modbus exception … code=0x.., or a host that names the code, e.g. Illegal Data Address). Gateway codes 0x0A and 0x0B and busy 0x06 do not count: they say the device could not answer.
  • Writes: only a refusal counts toward ems_write_failures. Once this session has written forced mode (13049 = 2 accepted), the release is always written. It is never reported as held until a release lands.
  • Reads: a refusal or a short reply counts at once. Any other failure counts only if another read in the same poll returned data. driver_poll now wraps the old body. A poll that reads nothing marks the link down. The next poll that reads data clears miss_counts and ems_write_failures.
  • The reconnect-backoff exemption from 1.6.0 stays.

Other drivers with the same latch (not changed here). 50-125k-svk, 50-125k-svk-slew, 50-125k-svk-ac-slew, deye-svk and konja-261-svk use BUNDLE_MISSES_BEFORE_SKIP. That is a different shape: bundle_read skips a block for BUNDLE_RETRY_EVERY (12) polls and then tries it again, so an outage costs about a minute, not the session. Each also has its own DEFAULT_MODE_REFUSALS_BEFORE_STOP write latch. Neither is identical to this change, so I left them for a separate PR.

Tests

drivers/tests/test_sungrow_outage.py, 18 cases. 14 fail on main and all pass here:

  • After an outage, using each of five link-error strings (dial refused, backoff, i/o timeout, gateway 0x0B, gateway 0x0A), 13000, 5016, 5010 and 12999 are read again, and the battery reads −900 W (discharging).
  • A forced discharge was accepted before the outage. After it, the watchdog's release writes 13049–13051 and leaves mode 0, for each of the same five errors.
  • After forced mode, even five real exception refusals never return true without writing.
  • During an outage the release returns false, not true.
  • A reconnect clears both the read misses and the write count.
  • These still hold: a register refused with an exception is given up; a register that times out while the rest answer is given up; a device that never took forced mode and refuses the block settles (the SG12RT rule from 1.5.7).

Also run:

  • make test-driver ID=sungrow: 114 passed, 4 skipped.
  • make check: 5007 passed.
  • make refused-write-report ID=sungrow: SETTLED 1 WRITES 3,3,3,0,0,0,0,0, the same as before.
  • make absent-register-report ID=sungrow: every address SETTLED 1.

No hardware run. This changes only how failures are counted, not any register, scale or sign.

Risk

  • After each outage, a device that really lacks a register reads it up to three more times before it is given up again. This is bounded per reconnect.
  • A device that took forced mode and then refuses every EMS write with a real exception keeps getting the release on each watchdog tick, and the tick reports false. That is deliberate: it may be holding forced mode.
  • A host whose exception text carries no exception or illegal would see refusals treated as link errors on writes. FTW's host uses modbus exception function=0x.. code=0x... On reads, such a refusal still counts when the rest of the poll answered.
  • Core keeps the driver excluded for driverRefusalRetryInterval (5 min) after a refused default, and does not resend the default when the device returns. That is the Core half of C1 and is not in this PR.

Paired PR in srcfl/ftw moves the bundled pin: see the comment below once it is open.

🤖 Generated with Claude Code


Same change as #201 (cc16f0e), re-committed with a Signed-off-by line so the DCO check passes. #201 closes in favour of this PR.

🤖 Generated with Claude Code

Count a failed read or EMS write only when the device answers with a
Modbus exception. A failed read also counts when another read in the
same poll returned data. Transport errors, reconnect backoff and gateway
or busy exceptions never count. The first poll that reads data after a
poll that read nothing clears the read misses and the EMS write count.

Once forced mode has landed, set_self_consumption always writes the
release and never reports the default as held until the inverter takes
it.

FTW's random site rig (27 October 2026, master 83c2b24c) found both:
13000 dropped after an outage flipped the battery sign (207539, 207534),
and the release was skipped while the inverter held its last forced
command for up to 5 minutes (8 of 10 seeds).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-28T10:15:34.015055Z 47a23f1 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@frahlg
frahlg merged commit 46b28a1 into main Oct 28, 2026
5 checks passed
@frahlg
frahlg deleted the fix/sungrow-outage-latch-signed branch October 28, 2026 10:29
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.

1 participant