Skip to content

[BUG] A refused block read drops its members from the snapshot with no per-register fallback #501

Description

@sphings79

Affected area

Battery communication or availability

Omnibattery version

release/v1.5.0 (89d3a96) plus #500, built locally as 1.5.0-blockread.1. The code path below is unchanged since block reads were introduced in #361.

Home Assistant version

2026.9.3 (HA OS 18.3)

Installation details

Marstek Venus D, EMS v150, seven packs, Modbus TCP. Measured both through a Modbus proxy and against the battery directly; the behaviour is the same, the proxy only changes how fast a refusal comes back.

What happened?

When a block read fails, its members are dropped from the snapshot and no attempt is made to read them individually. In drivers/marstek.py:

pending.difference_update(member_keys)
regs = await self._client.async_read_block(...)
if regs is None:
    continue                      # <- members are gone from `pending` already

The keys leave pending before the request is sent, so the per-register loop below never sees them. One refused span therefore takes every register in it out of that cycle, and the only trace is a generic Modbus read error at register N, which does not say which entities just went without a value.

Today this is mostly invisible, because the spans in the table are known-good on the models that have them. It stops being invisible the moment a span covers an address a given device does not implement — and it is what makes an otherwise obvious optimisation unusable, which is how I ran into it.

I measured bridging small gaps on the Venus D, since the tables only ever group registers that are already adjacent:

fast schedule full pass values returned
no gap 11 requests 41 57
gap of 2 11 requests 38 57
gap of 10 9 requests 35 53

With a gap of 10, four of the five new spans answer fine:

30001+6   high      battery_power, ac_power                 answered in 360 ms
30200+5   very_low  ems/vms/bms_version                     answered in 163 ms
30300+4   low       wifi_status, cloud_status, signal        answered in 300 ms
34003+7   low       battery_cycle_count, balancing_mode     answered in 299 ms
42010+12  high      force_mode, charge_to_soc,
                    set_charge_power, set_discharge_power   REFUSED after 503 ms

Some address between 42012 and 42019 is not implemented, so the whole span is refused — and that span carries the four control registers. They simply vanish from every fast cycle, silently, and the two requests the gap "saves" are exactly the ones whose answers are lost.

Steps to reproduce

  1. Point the Marstek driver at a battery and block-read a span that covers an address the firmware does not implement (on a Venus D: 42010, 12 registers).
  2. Request all of that span's keys in one read_telemetry call, the way a read group does.
  3. The returned snapshot contains none of them, async_read_register is never called for them, and nothing in the log names the keys that were dropped.

Expected behavior

A block is a way to fetch registers that would otherwise be fetched one at a time, so a failed block should cost no more than the block read itself: fall back to reading its members individually. Failing that, log at warning which keys were dropped, so it is visible rather than silent.

One caveat from trying it: a blanket fallback is not free. regs is None also covers a connection outage, and then the fallback fires N doomed requests per cycle at the weakest Modbus stack in the supported range — which is the thing #361 set out to relieve. It probably needs to tell a refusal (the device answered, with an exception) from a timeout, and to remember a span that was refused rather than retrying it forever.

Relevant Home Assistant logs

Modbus read error at register 42010 (0xA41A) on attempt 1

That is the whole trace. Nothing says that force_mode, charge_to_soc, set_charge_power and set_discharge_power are missing from this cycle.

Additional context

Not a regression, and #500 does not introduce it: the derived table still only groups registers that are already adjacent, all of which would be read — and refused — individually anyway. I am filing it because the measurement above is worth recording either way, so nobody else spends a night finding out that gap bridging looks free and is not, and because the silent drop is worth fixing on its own terms.

Nothing of this is in #500 — that one changes which registers get grouped, not what happens when a group is refused. Happy to prepare a separate PR for it, once it is clear which of the two shapes above you would rather have.

Confirmations

  • I am using the latest available Omnibattery release and restarted Home Assistant after updating.
  • I searched the documentation, existing issues, and existing discussions.
  • I can reproduce this unexpected behavior; this is not a request for installation or configuration help.
  • I removed passwords, tokens, API keys, complete serial numbers, IP addresses, and other sensitive information.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    implementedBug fixed or feature implemented

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions