|
| 1 | +# RS485-to-TCP Gateways |
| 2 | + |
| 3 | +Most problems that look like integration bugs turn out to be the box between Home Assistant and the inverter. This page collects what has actually been measured on real hardware, with the issue numbers behind each claim. |
| 4 | + |
| 5 | +> A gateway can look correctly configured, pass a casual test, and still be the thing that's broken. Some fail loudly. The dangerous ones fail quietly. |
| 6 | +
|
| 7 | +--- |
| 8 | + |
| 9 | +## The one setting that matters most |
| 10 | + |
| 11 | +Your gateway must do **Modbus TCP to RTU translation**, not transparent passthrough. |
| 12 | + |
| 13 | +This integration uses `pymodbus`'s `ModbusTcpClient`, which speaks Modbus TCP with MBAP framing. A transparent passthrough forwards raw RTU bytes with no MBAP header, and the client cannot parse them. It will not work at all, no matter how good the hardware is. |
| 14 | + |
| 15 | +That is a **mode setting**, not a hardware quality question. Check it before buying anything or blaming anything else. |
| 16 | + |
| 17 | +--- |
| 18 | + |
| 19 | +## Field-tested hardware |
| 20 | + |
| 21 | +### ✅ Waveshare RS485 TO POE ETH (B) — known good |
| 22 | + |
| 23 | +Firmware V1.523, reported in [#367](https://github.com/0xAHA/Growatt_ModbusTCP/issues/367) on a MID 25KTL3-XH. |
| 24 | + |
| 25 | +| Setting | Value | |
| 26 | +|---|---| |
| 27 | +| Protocol | `Modbus TCP to RTU` — **not** "None"/transparent | |
| 28 | +| Instruction Timeout | 288 ms — must exceed the transmission time of your largest block | |
| 29 | +| RS485 Conflict Gap | 20 ms | |
| 30 | + |
| 31 | +Measured: zero short/misaligned reads, 89 sensors populated, two full register scans of 2300 registers across 17 ranges with no read errors, and **26 days of statistics from before the v1.3.7 guard existed with no corrupt values at all**. On this gateway there was never anything to catch. |
| 32 | + |
| 33 | +### ⚠️ PUSR / ShineWiFi-class serial bridges — replay stale frames |
| 34 | + |
| 35 | +Reported in [#360](https://github.com/0xAHA/Growatt_ModbusTCP/issues/360) and [#367](https://github.com/0xAHA/Growatt_ModbusTCP/issues/367). |
| 36 | + |
| 37 | +These can return **a complete, valid response to an earlier request** when answering the current one. Measured at roughly **one poll in three**, with 30 of 31 mismatches returning exactly 125 registers regardless of what was asked for. |
| 38 | + |
| 39 | +Since v1.3.7 the integration detects this and discards the frame, so the data is safe — but you will see `Short/misaligned read at N: got X of Y registers` warnings. If you are on an older version, this is the failure mode that published a serial-number fragment as 85,893,614.8 W of AC power. |
| 40 | + |
| 41 | +Two settings materially improved a PUSR unit on #360: |
| 42 | + |
| 43 | +| Setting | Change | Why | |
| 44 | +|---|---|---| |
| 45 | +| TCP timeout | disabled → **30 s** | With it disabled, dead sessions are never reaped and eventually every connection slot is held by a connection to nobody | |
| 46 | +| UART AutoFrame | disabled → **100 ms** | Frame fragmentation causes the parser to lock onto the wrong byte offset and read a nonsense unit ID | |
| 47 | + |
| 48 | +### ❌ Olimex ESP32-POE-ISO + `esphome_modbus_bridge` — unstable |
| 49 | + |
| 50 | +Reported in [#367](https://github.com/0xAHA/Growatt_ModbusTCP/issues/367). Repeated dropouts, TCP host unreachable for two to three minutes at a time, recovering on its own with no pattern tied to load, time of day or PV production. RS485 bias resistors made no difference, which pointed at the network side rather than the serial side. Replaced with the Waveshare above. |
| 51 | + |
| 52 | +--- |
| 53 | + |
| 54 | +## Diagnosing your own gateway |
| 55 | + |
| 56 | +**Is it replaying stale frames?** Look for `Short/misaligned read` warnings. Note whether the count returned is *larger* than requested — a reply longer than the request cannot be a truncation, and points at a replayed earlier response. |
| 57 | + |
| 58 | +**Is latency per-request or per-register?** This decides whether a smaller block size helps or hurts. Read the same register range at several block sizes and compare total time: |
| 59 | + |
| 60 | +- If time scales with the number of registers, smaller blocks help. |
| 61 | +- If time is roughly **fixed per request**, smaller blocks are much worse. |
| 62 | + |
| 63 | +On the PUSR unit in #367, 113 registers cost the same as 1 — every read landed in one of two clusters ~500 ms apart, which looks like an internal scheduling tick. Block size 1 would have meant ~113 requests of ~0.8 s each in place of a single 1.3 s read. **Block size 25 was kept.** |
| 64 | + |
| 65 | +**Careful measuring latency from logs.** A 15-18 s figure reported on #367 turned out to be the integration's own failure cycle — a 10 s timeout plus reset and retry — not gateway latency. Measure with raw sockets and the integration disabled. |
| 66 | + |
| 67 | +**Running a register scan?** Disable the integration entry first (**⋮ → Disable**, don't delete), wait ~30 s, then scan. The scanner opens a second connection, and on a sensitive gateway that contends with the poller. A scan taken while polling came back with 9 successful reads out of 1304 rows, every range reporting "no response" on a device that was working fine. |
| 68 | + |
| 69 | +--- |
| 70 | + |
| 71 | +## Does a persistent connection cause this? |
| 72 | + |
| 73 | +No — and this was tested directly. |
| 74 | + |
| 75 | +The integration holds one socket per host:port across polls. It was suspected of allowing a stale frame to linger in the buffer, but the clean Waveshare setup uses **the same shared connection and the same 60 s interval** with zero mismatches. A persistent socket is not the mechanism; it is what exposes a gateway that replays. Since v1.3.7 a detected mismatch also drains the receive buffer, so a misaligned stream does not persist into the next read. |
| 76 | + |
| 77 | +| Gateway | Socket | Result | |
| 78 | +|---|---|---| |
| 79 | +| ShineWiFi-class | persistent | mismatch ~1 poll in 3 | |
| 80 | +| ShineWiFi-class | fresh per read | 21/21 clean | |
| 81 | +| Waveshare RS485 TO POE ETH (B) | persistent | clean | |
| 82 | + |
| 83 | +--- |
| 84 | + |
| 85 | +## Keeping the cloud app working |
| 86 | + |
| 87 | +You do not necessarily have to choose. On both systems in #367 the inverter has a **`SYS COM` port separate from the USB port the ShineWiFi dongle occupies**, so a second RS485 master can run alongside the stock dongle. Home Assistant gets a local Modbus path, and the dongle keeps feeding Growatt's own app. |
| 88 | + |
| 89 | +Note this is one local path and one cloud path — not two paths into Home Assistant. If you keep the dongle, be aware the Growatt cloud can overwrite local writes to control registers within seconds; the integration logs a `Write reversion detected` warning when it sees this. |
0 commit comments