Skip to content

Detect lost link, validate setpoint writes, batch read_all_* (v1.15.0) - #3

Merged
MattieGit merged 4 commits into
mainfrom
fix/connection-safety-perf
Oct 1, 2026
Merged

MattieGit merged 4 commits into
mainfrom
fix/connection-safety-perf

Conversation

@MattieGit

@MattieGit MattieGit commented Oct 1, 2026 •

Copy link
Copy Markdown
Owner

Summary

Connection handling, write safety and request batching (v1.15.0).

  • Lost link detected. The connected flag was only cleared by close(), so a dropped link was never noticed and the reconnect backoff never ran. Link failures (ConnectionException, ModbusIOException, OSError) now mark the client disconnected and close the pymodbus transport; the next poll reconnects.
  • Dead device stops the poll early. After a failed block, the first single read acts as a probe; if it also hits a link failure the poll stops. get_all_data() returns None when the link is lost mid-poll. Modbus error replies keep the per-entity fallback.
  • Warn-once re-arms when a target reads fine again, so a later outage is logged at WARNING again.
  • Setpoint validation. EntityDef gains optional min_value/max_value; the four writable setpoints use the ranges both HA integrations already expose (DHW 40–65, heating 20–65, cooling 7–25 °C). NaN, inf and out-of-range values are refused before any Modbus traffic. 16-bit writes round instead of truncating and refuse values that do not fit.
  • SG Ready set atomically with one write_coils request (falls back to two single writes if the device rejects it); get_sg_ready_mode() reads both coils in one request.
  • read_all_* batched: 94 → 9 requests, same keys, key order and values.
  • async_get_mac_address() no longer blocks the event loop (DNS lookup and /proc/net/arp read run in a thread).
Measurement Before After
Requests per get_all_data() poll, device unreachable 68 2
Poll on a device that accepts TCP but never answers (real pymodbus) 96 s, reports connected 24 s, returns None, reconnects next poll
read_all_sensors + read_all_binary_sensors + read_all_switches 94 9

Compatibility

  • Public API unchanged; new EntityDef fields are optional and appended at the end.
  • get_all_data() returning None on a lost link is within its documented contract; the HA core coordinator already raises UpdateFailed on None.
  • Values the integrations' UIs allow are all accepted; out-of-range values sent through the HACS write_register service are now refused.
  • Multi-coil write (FC15) is untested on a real Qube; the single-write fallback covers a device that rejects it.

Test plan

  • Library: 130 passed (47 new), ruff check and ruff format --check clean
  • HA core tests/components/hr_energy_qube against this branch: 30 passed (same as 1.12.0 and main)
  • HACS integration tests against this branch: 332 passed (same as main)
  • End-to-end against a real pymodbus TCP server: decoding, SG Ready write/read, setpoint write/read-back and range refusal, server stop → None without delay, restart → reconnect, silent device → 24 s
  • SG Ready mode change on real Qube hardware

MattieGit and others added 4 commits October 1, 2026 10:19
The connected flag was only cleared by close(), so a dropped link was
never noticed: _ensure_connected() never reconnected and every block of
every poll fell back to per-entity reads, each waiting for its own
timeout (68 transactions per get_all_data() poll, 101 per
get_all_entities()).

Link failures (ConnectionException, ModbusIOException, OSError) now mark
the client disconnected and close the pymodbus transport, so the next
poll reconnects with backoff. After a failed block, the first single read
acts as a probe; if it also hits a link failure the poll stops, capping a
dead link at 2 transactions. get_all_data() returns None when the link is
lost mid-poll. Modbus error replies keep the per-entity fallback.

The warn-once read-failure log now re-arms when a target reads fine
again, so a later outage is logged at WARNING again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
write_setpoint() sent any value to the controller, including NaN, inf
and out-of-range temperatures. EntityDef gains optional min_value and
max_value; the four writable setpoints get the ranges both HA
integrations already expose (DHW 40-65, heating 20-65, cooling 7-25 C).
Non-finite and out-of-range values are refused before any Modbus
traffic. 16-bit writes now round instead of truncating (21.9 wrote 21)
and refuse values that do not fit the register.

set_sg_ready_mode() wrote coil 65 and 66 in two requests, so a failed
second write left the pump in an unintended mode. Both coils are now
written in one write_coils request, falling back to the old two single
writes if the device rejects it. get_sg_ready_mode() reads both coils in
one request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
read_all_sensors(), read_all_binary_sensors() and read_all_switches()
issued one Modbus request per entity (94 together). They now use the
batched block reader (9 requests), returning the same keys, key order
and values as before. HA core calls read_all_switches() every poll.

async_get_mac_address() resolved the host with socket.gethostbyname()
and read /proc/net/arp on the event loop; both now run in a worker
thread.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MattieGit
MattieGit merged commit 346a2a7 into main Oct 1, 2026
3 checks passed
@MattieGit
MattieGit deleted the fix/connection-safety-perf branch October 1, 2026 08:42
MattieGit added a commit that referenced this pull request Oct 20, 2026
#3)

* fix: detect lost Modbus link, stop polls early and reconnect

The connected flag was only cleared by close(), so a dropped link was
never noticed: _ensure_connected() never reconnected and every block of
every poll fell back to per-entity reads, each waiting for its own
timeout (68 transactions per get_all_data() poll, 101 per
get_all_entities()).

Link failures (ConnectionException, ModbusIOException, OSError) now mark
the client disconnected and close the pymodbus transport, so the next
poll reconnects with backoff. After a failed block, the first single read
acts as a probe; if it also hits a link failure the poll stops, capping a
dead link at 2 transactions. get_all_data() returns None when the link is
lost mid-poll. Modbus error replies keep the per-entity fallback.

The warn-once read-failure log now re-arms when a target reads fine
again, so a later outage is logged at WARNING again.

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

* fix: validate setpoint writes and set SG Ready mode atomically

write_setpoint() sent any value to the controller, including NaN, inf
and out-of-range temperatures. EntityDef gains optional min_value and
max_value; the four writable setpoints get the ranges both HA
integrations already expose (DHW 40-65, heating 20-65, cooling 7-25 C).
Non-finite and out-of-range values are refused before any Modbus
traffic. 16-bit writes now round instead of truncating (21.9 wrote 21)
and refuse values that do not fit the register.

set_sg_ready_mode() wrote coil 65 and 66 in two requests, so a failed
second write left the pump in an unintended mode. Both coils are now
written in one write_coils request, falling back to the old two single
writes if the device rejects it. get_sg_ready_mode() reads both coils in
one request.

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

* perf: batch read_all_* helpers and keep MAC lookup off the event loop

read_all_sensors(), read_all_binary_sensors() and read_all_switches()
issued one Modbus request per entity (94 together). They now use the
batched block reader (9 requests), returning the same keys, key order
and values as before. HA core calls read_all_switches() every poll.

async_get_mac_address() resolved the host with socket.gethostbyname()
and read /proc/net/arp on the event loop; both now run in a worker
thread.

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

* chore: bump version to 1.15.0

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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