Skip to content

Commit 87b5971

Browse files
0xAHAclaude
andcommitted
Bump version to v1.0.10 — shared connection TID fix + 3000-range suppression (#351)
- Double-flush receive buffer in shared connection mode: first flush clears bytes already in TCP buffer; 30ms pause lets any in-flight RS485 bytes arrive; second flush clears them. Eliminates the residual TID mismatches that survived v1.0.9. - Add skip-on-failure caching to 3000-range register block reads (same pattern as VPP 31000+ blocks). First failure logs WARNING once; subsequent failures within the 5-minute retry window are suppressed to DEBUG. Eliminates the log flood in mixed-model setups where one inverter does not support the 3000-range. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent ff30822 commit 87b5971

5 files changed

Lines changed: 120 additions & 57 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# Growatt Modbus Integration for Home Assistant ☀️
44

55
![HACS Badge](https://img.shields.io/badge/HACS-Custom-orange.svg)
6-
![Version](https://img.shields.io/badge/Version-1.1.0-blue.svg)
6+
![Version](https://img.shields.io/badge/Version-1.0.10-blue.svg)
77
[![GitHub Issues](https://img.shields.io/github/issues/0xAHA/Growatt_ModbusTCP.svg)](https://github.com/0xAHA/Growatt_ModbusTCP/issues)
88
[![GitHub Stars](https://img.shields.io/github/stars/0xAHA/Growatt_ModbusTCP.svg?style=social)](https://github.com/0xAHA/Growatt_ModbusTCP)
99

RELEASENOTES.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,26 @@
44

55
---
66

7+
## v1.0.10
8+
9+
Issues: #351
10+
11+
- **Fix: Remaining transaction ID mismatches in shared connection mode (#351):**
12+
A single buffer flush after acquiring the lock cleared bytes already in the TCP buffer,
13+
but RS485 bytes still in transit through the gateway arrived milliseconds later — after
14+
the flush but before the first request, causing the occasional TID mismatch that survived
15+
v1.0.9. Fix: double-flush with a 30ms pause between flushes, giving in-flight RS485
16+
bytes time to arrive so the second flush catches them.
17+
18+
- **Fix: 3000-range register block warning flood in multi-inverter setups (#351):**
19+
In a setup with two different inverter models (e.g. SPH + MOD), the 3000-range register
20+
block may be defined for one profile but consistently rejected by the other inverter.
21+
Every failed read logged a WARNING every ~70 seconds. Fix: the 3000-range block now uses
22+
the same skip-on-failure caching as the VPP 31000+ blocks — the first failure logs a
23+
WARNING once, then the block is skipped silently for 5 minutes before retrying.
24+
25+
---
26+
727
## v1.1.0
828

929
Issues: #322

custom_components/growatt_modbus/coordinator.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -933,9 +933,13 @@ def _fetch_data_shared(self) -> GrowattData | None:
933933
)
934934
return None
935935

936-
# Flush any stale bytes left in the adapter's TCP buffer by the
937-
# previous slave's poll (late RS485 responses that arrived after the
938-
# lock was released cause transaction ID mismatches on this slave's reads).
936+
# Double-flush: the first flush clears bytes already in the TCP buffer.
937+
# A 30ms pause then lets any RS485 bytes still in transit through the
938+
# gateway arrive, so the second flush catches them too. Without the pause,
939+
# in-flight bytes arrive after the flush and cause TID mismatches on the
940+
# first read of this slave's poll.
941+
hub._flush_receive_buffer()
942+
time.sleep(0.030)
939943
hub._flush_receive_buffer()
940944

941945
self._client._battery_voltage_range = self.config_entry.options.get(

custom_components/growatt_modbus/growatt_modbus.py

Lines changed: 91 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1317,63 +1317,102 @@ def _read_sparse(addr_list: list, fatal: bool = False) -> bool:
13171317
addrs_3000 = sorted([addr for addr in addresses if 3000 <= addr < 4000])
13181318
max_3000_addr = max(addrs_3000)
13191319
count_3000 = (max_3000_addr - 3000) + 1
1320+
_3000_key = ('3000_block', count_3000)
1321+
_3000_RETRY_S = 300
1322+
1323+
# Skip-on-failure: after the first failure, suppress retries for 5 minutes.
1324+
# This prevents log flooding when an inverter simply doesn't support this range
1325+
# (e.g. one model in a two-inverter setup where only one uses 3000-range registers).
1326+
_3000_prev = self._failed_optional_ranges.get(_3000_key)
1327+
_3000_skip = False
1328+
if _3000_prev:
1329+
_3000_fail_time, _3000_fail_count = _3000_prev
1330+
if time.time() - _3000_fail_time < _3000_RETRY_S:
1331+
logger.debug(
1332+
f"Skipping 3000-range block (failed {_3000_fail_count}x, "
1333+
f"retry in {int(_3000_RETRY_S - (time.time() - _3000_fail_time))}s)"
1334+
)
1335+
_3000_skip = True
1336+
else:
1337+
logger.debug(f"Retrying previously failed 3000-range block (3000-{max_3000_addr})")
1338+
1339+
if not _3000_skip:
1340+
_3000_any_fail = False
1341+
_3000_any_ok = False
1342+
1343+
# Check if we need to split the read (max 125 registers per read)
1344+
if count_3000 > 125:
1345+
# Split into contiguous blocks
1346+
logger.debug(f"Splitting 3000 range into blocks (total range: 3000-{max_3000_addr}, {count_3000} registers)")
1347+
1348+
blocks = []
1349+
current_block = [addrs_3000[0]]
1350+
for addr in addrs_3000[1:]:
1351+
if addr - current_block[-1] <= 10: # Group if gap is small
1352+
current_block.append(addr)
1353+
else:
1354+
blocks.append(current_block)
1355+
current_block = [addr]
1356+
blocks.append(current_block)
13201357

1321-
# Check if we need to split the read (max 125 registers per read)
1322-
if count_3000 > 125:
1323-
# Split into contiguous blocks
1324-
logger.debug(f"Splitting 3000 range into blocks (total range: 3000-{max_3000_addr}, {count_3000} registers)")
1325-
1326-
blocks = []
1327-
current_block = [addrs_3000[0]]
1328-
for addr in addrs_3000[1:]:
1329-
if addr - current_block[-1] <= 10: # Group if gap is small
1330-
current_block.append(addr)
1331-
else:
1332-
blocks.append(current_block)
1333-
current_block = [addr]
1334-
blocks.append(current_block)
1335-
1336-
# Read each block separately
1337-
for block in blocks:
1338-
min_addr_block = min(block)
1339-
max_addr_block = max(block)
1340-
count_block = (max_addr_block - min_addr_block) + 1
1341-
1342-
# Further split if block still exceeds 125 registers
1343-
if count_block > 125:
1344-
# Read in 125-register chunks
1345-
for chunk_start in range(min_addr_block, max_addr_block + 1, 125):
1346-
chunk_count = min(125, max_addr_block - chunk_start + 1)
1347-
logger.debug(f"Reading 3000 sub-chunk ({chunk_start}-{chunk_start+chunk_count-1}, {chunk_count} registers)")
1348-
registers = self.read_input_registers(chunk_start, chunk_count)
1358+
# Read each block separately
1359+
for block in blocks:
1360+
min_addr_block = min(block)
1361+
max_addr_block = max(block)
1362+
count_block = (max_addr_block - min_addr_block) + 1
1363+
1364+
# Further split if block still exceeds 125 registers
1365+
if count_block > 125:
1366+
# Read in 125-register chunks
1367+
for chunk_start in range(min_addr_block, max_addr_block + 1, 125):
1368+
chunk_count = min(125, max_addr_block - chunk_start + 1)
1369+
logger.debug(f"Reading 3000 sub-chunk ({chunk_start}-{chunk_start+chunk_count-1}, {chunk_count} registers)")
1370+
registers = self.read_input_registers(chunk_start, chunk_count)
1371+
if registers is None:
1372+
_3000_any_fail = True
1373+
logger.debug(f"3000 sub-chunk failed ({chunk_start}-{chunk_start+chunk_count-1})")
1374+
else:
1375+
_3000_any_ok = True
1376+
for i, value in enumerate(registers):
1377+
self._register_cache[chunk_start + i] = value
1378+
else:
1379+
logger.debug(f"Reading 3000 sub-range ({min_addr_block}-{max_addr_block}, {count_block} registers)")
1380+
registers = self.read_input_registers(min_addr_block, count_block)
13491381
if registers is None:
1350-
logger.warning(f"Failed to read 3000 chunk ({chunk_start}-{chunk_start+chunk_count-1})")
1382+
_3000_any_fail = True
1383+
logger.debug(f"3000 sub-range failed ({min_addr_block}-{max_addr_block})")
13511384
else:
1385+
_3000_any_ok = True
13521386
for i, value in enumerate(registers):
1353-
self._register_cache[chunk_start + i] = value
1354-
else:
1355-
logger.debug(f"Reading 3000 sub-range ({min_addr_block}-{max_addr_block}, {count_block} registers)")
1356-
registers = self.read_input_registers(min_addr_block, count_block)
1357-
if registers is None:
1358-
logger.warning(f"Failed to read 3000 block ({min_addr_block}-{max_addr_block})")
1359-
else:
1360-
for i, value in enumerate(registers):
1361-
addr = min_addr_block + i
1362-
self._register_cache[addr] = value
1363-
# Log load_energy registers specifically
1364-
if addr in [3075, 3076, 3077, 3078]:
1365-
logger.debug(f"[{self.register_map['name']}@{self.connection_id}] Cached 3000 range: reg {addr} = {value}")
1366-
else:
1367-
# Single read is sufficient
1368-
logger.debug(f"Reading 3000 range (3000-{max_3000_addr}, {count_3000} registers)")
1369-
registers = self.read_input_registers(3000, count_3000)
1370-
if registers is None:
1371-
logger.warning("Failed to read 3000 register block (extended data may be unavailable)")
1372-
# Don't return None - continue with what we have
1387+
addr = min_addr_block + i
1388+
self._register_cache[addr] = value
1389+
if addr in [3075, 3076, 3077, 3078]:
1390+
logger.debug(f"[{self.register_map['name']}@{self.connection_id}] Cached 3000 range: reg {addr} = {value}")
13731391
else:
1374-
# Populate cache
1375-
for i, value in enumerate(registers):
1376-
self._register_cache[3000 + i] = value
1392+
# Single read is sufficient
1393+
logger.debug(f"Reading 3000 range (3000-{max_3000_addr}, {count_3000} registers)")
1394+
registers = self.read_input_registers(3000, count_3000)
1395+
if registers is None:
1396+
_3000_any_fail = True
1397+
else:
1398+
_3000_any_ok = True
1399+
for i, value in enumerate(registers):
1400+
self._register_cache[3000 + i] = value
1401+
1402+
# Update failure tracking based on outcome
1403+
if _3000_any_fail and not _3000_any_ok:
1404+
_prev = self._failed_optional_ranges.get(_3000_key)
1405+
_count = (_prev[1] + 1) if _prev else 1
1406+
self._failed_optional_ranges[_3000_key] = (time.time(), _count)
1407+
if _count == 1:
1408+
logger.warning(
1409+
f"Failed to read 3000 register block (extended data may be unavailable). "
1410+
f"Will suppress and retry in {_3000_RETRY_S}s."
1411+
)
1412+
else:
1413+
logger.debug(f"3000 register block still failing (attempt {_count})")
1414+
elif _3000_any_ok:
1415+
self._failed_optional_ranges.pop(_3000_key, None)
13771416

13781417
# Read 8000 range if needed - WIT/WIS battery/storage data
13791418
if has_8000_range:

custom_components/growatt_modbus/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@
1212
"pymodbus>=3.0.0",
1313
"pyserial>=3.4"
1414
],
15-
"version": "1.1.0"
15+
"version": "1.0.10"
1616
}

0 commit comments

Comments
 (0)