Skip to content

fix(marstek): let a Venus D read outlast the battery's five-minute stall - #504

Merged
ffunes merged 1 commit into
ffunes:release/v1.5.0from
sphings79:fix/marstek-read-timeout
Sep 21, 2026
Merged

ffunes merged 1 commit into
ffunes:release/v1.5.0from
sphings79:fix/marstek-read-timeout

Conversation

@sphings79

Copy link
Copy Markdown
Contributor

Closes #503.

A Venus D stops answering for about four seconds every five minutes. Measured at the Modbus proxy between Home Assistant and the battery, over eleven hours:

210.077 requests
138 stalls longer than 1.5 s
mean         4.05 s
longest      4.46 s
interval     297.9 s, very regular

Everything else answers in under 250 ms, mean 81 ms. There is nothing in between the two groups, so this is one behaviour of the device rather than a tail of the normal distribution. Since it is measured on the proxy's own socket to the battery, neither Home Assistant nor the client library is in the path.

Why three seconds does not survive it

The comment above READ_TIMEOUT_S says why the v3 family was kept short:

Each pymodbus-internal retry re-sends the SAME transaction_id (>=3.8), so a reply arriving after a per-attempt timeout still matches the retry and is consumed. Keep v3 attempts short so the retries land inside the stall window.

That reasoning holds for a stall the attempt can sit out. It does not hold for a four second one: the attempt expires in every single stall, the retry puts a duplicate request on the wire, and the battery answers both. The first answer then arrives with nothing waiting for it:

ERROR: received pdu without a corresponding request, IGNORING
ERROR: request ask for transaction_id=9490 but got id=9489, Skipping.

122 of them in eleven hours — one per stall.

The change

READ_TIMEOUT_S["vD"] 3 → 6, and the comment rewritten to say what was measured.

Six rather than five: the longest stall seen is 4.46 s, and a second request queued behind the stalled one waits a little longer still (4.53 s worst case at the proxy, out of 141.054 client requests). Five would have covered every case in the sample; six leaves room for the sample to be wrong.

The cost is that a battery that is genuinely gone gets longer before an attempt is abandoned: the outer safety net in infra/modbus_client.py is timeout * (retries + 1) + 2, so 11 s becomes 20 s. For a device whose observed failure mode is being four seconds late, that seems the right way round, but it is your call — the same relief is available at 5 s if you would rather keep the worst case tighter.

v3 and vA are left at three seconds. Same family, but I have only a Venus D (EMS v150) to measure. If someone confirms the same five-minute stall on a v3 or a vA, it is a one-line follow-up.

What this does not change

No values were being lost. The retry fetched them; in eleven hours exactly one key failed outright, and not during a stall. What goes is the duplicate request and a log that reports a fault twelve times an hour for a battery that always came back.

Verification

  • Full suite: 2648 passed, 10 skipped, plus the eleven hass-fixture tests the workflow runs separately.
  • Two new tests in tests/test_marstek_driver.py: the Venus D attempt must outlast the measured stall (fails with the old value), and each version's timeout must reach the client it is built with.
  • On the battery: the other integration reading it through the same proxy had the same three second timeout and the same symptom, with a connection rebuild on top. Raising it removed every timeout and every rebuild — zero in 6.6 hours across roughly 80 stalls, same battery, same proxy, same load.

A Venus D stops answering for about four seconds on a five-minute clock:
138 stalls over eleven hours, mean 4.05 s, longest 4.46 s, measured at
the Modbus proxy between Home Assistant and the battery, so it is the
device and not the link.

The per-attempt timeout was three seconds, chosen so a pymodbus retry -
which re-sends the same transaction id - would land inside such a stall
and consume the late reply. Against a four second stall it does not: the
attempt expires every single time, the retry duplicates the request, and
the battery answers both. One of the two replies then arrives with
nothing waiting for it and is discarded, either as "received pdu without
a corresponding request" or as a transaction id mismatch against the
request that followed. Twelve times an hour.

Six seconds covers the stall with room over the longest one seen. The
cost is that a battery that is genuinely gone gets longer before the
attempt is abandoned - the outer safety net grows from 11 s to 20 s.

No value was being lost either way, the retry saw to that (one failed
key in eleven hours, unrelated). What goes is the wasted round trip and
a log that reports a fault twelve times an hour for a battery that is
merely four seconds late.

v3 and vA keep their three seconds: same family, but the measurement is
from a Venus D on EMS v150 and nowhere else.
@sphings79

Copy link
Copy Markdown
Contributor Author

On whether the same six seconds belong on a vA and a v3 — I would rather not claim that, and here is what I can and cannot say.

What I measured is a Venus D on EMS v150, over eleven hours at the proxy. I have no A and no E v3 to point an analyser at, so for those the five-minute stall is a guess, however plausible the shared lineage makes it.

What does not depend on the guess is the asymmetry of the change. A longer per-attempt timeout only ever does something when the device has not answered yet. On a battery that never stalls, nothing changes at all — same traffic, same latency, same values. The only price is paid when a battery is genuinely gone: the outer safety net in infra/modbus_client.py is timeout * (retries + 1) + 2, so a dead device is waited on for 20 s instead of 11 s per read.

That price lands differently per model, which is why I would treat them separately rather than as one family:

  • vA shares the register map and the generation of EMS firmware with the D, and its documented failure mode is the same. If someone with a Venus A sees the pattern, extending it is a one-line follow-up and I would expect it to be right.
  • v3 is the one I would not touch without looking. Its known failure mode is the opposite of a stall it recovers from: it accepts the TCP connection and then answers nothing at all ([BUG] Marstek Venus A stuck as unavailable #445), and there the slower detection is a real cost rather than a theoretical one.

Checking it takes half an hour and no equipment: set pymodbus to debug, leave the battery polling, and watch for

ERROR: received pdu without a corresponding request, IGNORING
ERROR: request ask for transaction_id=N but got id=N-1, Skipping.

If those appear on a five-minute cadence, it is the same thing. Happy to widen the PR if an A or an E v3 owner reports back.

@ffunes
ffunes merged commit 071ab6f into ffunes:release/v1.5.0 Sep 21, 2026
3 checks passed
ffunes added a commit that referenced this pull request Sep 22, 2026
fix(marstek): let a Venus D read outlast the battery's five-minute stall

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

# Conflicts:
#	CHANGELOG.md
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