Skip to content

fix(sungrow): stop writing an EMS block the inverter has already refused - #49

Merged
frahlg merged 1 commit into
mainfrom
claude/sungrow-bound-ems-write
Jul 29, 2026
Merged

fix(sungrow): stop writing an EMS block the inverter has already refused#49
frahlg merged 1 commit into
mainfrom
claude/sungrow-bound-ems-write

Conversation

@frahlg

@frahlg frahlg commented Jul 29, 2026

Copy link
Copy Markdown
Member

From a field report: an SG12RT logging self-consumption reset write failed: modbus exception function=0x06 once per watchdog tick, indefinitely.

Rebased onto #45. The earlier draft of this PR argued from #42's always-accept-zero; that premise is gone and the conclusion did not move, which is most of the point.

What it does

driver_init's startup reset, the watchdog, driver_cleanup and the deinit action all write 13049-13051, and nothing counted how often the device said no. The EMS block now follows the rule the register reads have followed since #36: absence has to be proved, three refusals prove it, and the paths that write unprompted stop.

before after
writes per watchdog tick 3,3,3,3,3,3,3,3 3,3,0,0,0,0,0,0
warn lines over 8 ticks 9 3

Why not if model_family == "string"

That is the obvious fix and the one the field thread proposed. It strands inverters.

The startup reset exists because the device can be holding a forced state this driver did not set — a container that died mid-force, an older driver version, iSolarCloud or another EMS on the same bus. None of those care what classify_device_type decided, and a hybrid shipped under a device-type code the driver has not been taught is classified string. Gate on the label and that inverter stays forced, with nothing left to write mode 0 back.

The label does not separate the cases. Whether the device took the write does. One that refuses it cannot be holding a forced state. One that accepts it keeps its release.

It also stops the question being re-argued. Whether a zero-watt command reaches the EMS block has been settled in both directions inside one day — #42 opened it, #45 closed it. This rule does not depend on that answer, and did not change when it flipped.

The tests separate the three designs

design result
no bound (today) 3 fail — the field spam
model_family gate 5 fail, incl. the stranded inverter
refusal count all 34 pass

The reported device ships as a fixture: device type 9268, no meter wired, no hybrid block, every EMS write refused.

Deliberately unchanged

  • A command always attempts, however many refusals came before, and its outcome feeds the count — so one write that lands puts the release back at once.
  • The count lives in the process. A restart always attempts the startup reset, exactly as a restart re-probes a read.
  • A refused write still reports failure while the driver is attempting. Reporting success over a write that did not land is the silent-failure shape #164 is about. What stops is the failure repeating forever.

The other half of that report needs no driver change

The same device shows driver_poll: 2 of 9 modbus reads failed on every poll and never comes online — that, not the writes, is what puts it offline. #36 already bounded those reads:

version failed reads per poll
1.4.0 2/9 2/9 2/9 2/9 2/9 2/9
main 2/9 2/9 2/9 0/7 0/7 0/7

The site runs 1.4.0 and its update panel reads "up to date", so what it needs is FTW's pin moved, not a patch.

Checks

make check — 3115 passed, 1361 skipped.

🤖 Generated with Claude Code

An SG12RT in the field logged "self-consumption reset write failed:
modbus exception function=0x06" once per watchdog tick, indefinitely.
driver_init's startup reset, the watchdog, driver_cleanup and the deinit
action all write 13049-13051, and nothing counted how often the device
said no.

The EMS block now follows the rule the register reads have followed
since #36: absence has to be proved, three refusals prove it, and the
paths that write unprompted stop. Measured on the reported device:

  writes per watchdog tick   3,3,3,3,3,3,3,3  ->  3,3,0,0,0,0,0,0
  warn lines over 8 ticks    9                ->  3

Deliberately not gated on model_family, which is the obvious fix, the
one the field thread proposed, and the wrong one. The startup reset
exists because the inverter can be holding a forced state this driver
did not set: a container that died mid-force, an older driver version,
iSolarCloud or another EMS on the same bus. None of those care what
classify_device_type decided, and a hybrid shipped under a device-type
code the driver has not been taught is classified "string". Skipping the
release on the label leaves exactly that inverter forced, with nothing
left that writes mode 0 back.

What separates the cases is not the label but whether the device took
the write. One that refuses it cannot be holding a forced state. One
that accepts it keeps its release.

It also stops the question being re-argued. Whether a zero-watt command
reaches the EMS block has been settled in both directions inside one day
-- #42 opened it, #45 closed it again. This rule does not depend on that
answer, and did not change when it flipped.

A command always attempts, however many refusals came before, and its
outcome still feeds the count, so one write that lands puts the release
back at once. The count lives in the process, so a restart always
attempts the startup reset -- the case that reset exists for -- exactly
as a restart re-probes a read.

While the driver is still attempting, a refused write is reported as the
failure it is. Reporting success over a write that did not land is the
silent-failure shape #164 is about. What stops is the failure repeating
forever, so the watchdog settles rather than escalating against an
inverter that was never under control.

The reported device ships as a fixture -- device type 9268, no meter
wired, no hybrid block, every EMS write refused. The tests separate the
three candidate designs: with no bound three fail, with a model_family
gate five fail including the stranded-inverter case, and only the
refusal count passes all of them.

The other half of that field report needs no driver change. The same
device shows "driver_poll: 2 of 9 modbus reads failed" on every poll and
never comes online, which is what puts it offline. #36 already bounded
those reads: against 1.4.0 they fail 2/9 on every poll forever, against
main they settle to 0/7 by the fourth. The site runs 1.4.0 and its
update panel reads "up to date", so what it needs is FTW's pin moved.

make check -- 3115 passed, 1361 skipped.

Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@frahlg
frahlg merged commit 672ab53 into main Jul 29, 2026
5 checks passed
@frahlg
frahlg deleted the claude/sungrow-bound-ems-write branch July 29, 2026 16:32

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ff64d0fe91

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread drivers/lua/sungrow.lua
Comment on lines +948 to +949
if not ems_block_worth_writing() then
return true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep retrying the safety release after transport failures

When a hybrid is already in forced mode and the bus returns errors for three EMS operations—such as failed commands or watchdog ticks—ems_write_failures reaches the limit; after communication recovers, this branch returns success without writing mode 0. The inverter can therefore remain at its previous forced charge/discharge setpoint while the host is told that default mode was restored, until a restart or another successful command resets the counter. Only unsupported-register refusals should disable these writes; generic transport failures must not permanently suppress the safe-default release.

AGENTS.md reference: AGENTS.md:L17-L18

Useful? React with 👍 / 👎.

Comment thread drivers/lua/sungrow.lua
name = "Sungrow SH Hybrid Inverter",
manufacturer = "Sungrow",
version = "1.5.6",
version = "1.5.7",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Apply the refusal bound to the package target

This version bump fixes only the catalog copy, while packages/v1/sungrow/targets/ftw.lua:449-456 still has driver_default_mode_v2 write register 13049 and report failure on every invocation. If that control-v2 target is used with the same SG inverter, it retains the original indefinite write/failure behavior, allowing the separately maintained target to reintroduce the bug when packaged; update the target and its versioned metadata with the catalog change.

AGENTS.md reference: AGENTS.md:L22-L30

Useful? React with 👍 / 👎.

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