Skip to content

Commit 7b55299

Browse files
0xAHAclaude
andcommitted
Bump version to v1.1.1 — fix MIN/MOD all-zero readings until manual reload
Fixes a regression from v1.0.10 (#351) affecting every MIN and MOD-family profile, whose input registers live entirely in the 3000 range. The 3000-range skip-on-failure suppression added in v1.0.10 assumed the block was optional. On these profiles it is the only input range, so one failed read suppressed all reads for 300s, left _register_cache empty, and every field decoded to 0. read_all_data() returned that zeroed result rather than None, so the coordinator saw a successful poll: entities stayed available showing 0, _consecutive_failures never incremented, and no reconnect or adaptive backoff ran. The window re-armed every 300s and persisted until a manual reload — most visible as the overnight Waiting->Normal transition never being picked up. - Skip suppression is no longer applied when the 3000 range is a profile's primary (only) input range; multi-inverter setups keep anti-log-flood behaviour - Total failure of a primary range returns None so the coordinator marks the inverter offline and runs its reconnect/backoff path - Universal guard: an empty register cache after all reads reports the poll as failed instead of publishing zeros as valid data - Per-block success logging moved from INFO to DEBUG Behaviour change: on genuine comms failure sensors now go unavailable rather than reading 0 — correct HA semantics, since statistics ignore unavailable but record 0 as a real measurement. Also ships the MOD DO1.0 SOC control fix (#343, commit 6370a2b): holding registers 3048 and 3067 replace the dead 1091/1071 pair. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 6370a2b commit 7b55299

4 files changed

Lines changed: 81 additions & 4 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.1.1-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: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,47 @@
44

55
---
66

7+
## v1.1.1
8+
9+
Issues: #343
10+
11+
- **Fix: MIN/MOD inverters stuck reporting all zeros until manual reload (regression in v1.0.10):**
12+
On profiles whose input registers live entirely in the 3000 range — every MIN and
13+
MOD-family profile — a single failed read of that block put it into the 5-minute
14+
"optional range" suppression window introduced in v1.0.10 (#351). Because the 3000 range
15+
is the *only* input range on these profiles, suppression left the register cache empty
16+
and every value decoded to 0. `read_all_data()` returned that zeroed result instead of
17+
`None`, so the coordinator treated the poll as successful: entities stayed *available*
18+
showing 0, the failure counter never incremented, and no reconnect or adaptive backoff
19+
ever ran. The state re-armed every 300 s and persisted indefinitely — only reloading the
20+
integration recovered it. Most visible as the overnight Waiting → Normal transition not
21+
being picked up in the morning.
22+
23+
Three changes:
24+
- Retry suppression is no longer applied when the 3000 range is a profile's primary
25+
(only) input range. Multi-inverter setups keep the anti-log-flood behaviour.
26+
- A total failure of a primary range now returns `None`, so the coordinator marks the
27+
inverter offline and runs its reconnect/backoff path.
28+
- Added a universal guard: an empty register cache after all reads reports the poll as
29+
failed rather than publishing zeros as valid data.
30+
31+
**Behaviour change:** on a genuine communication failure, sensors now go *unavailable*
32+
rather than reading 0. This is the correct Home Assistant semantic — the statistics
33+
engine ignores unavailable states but records 0 as a real measurement, which previously
34+
polluted energy history with false zeros.
35+
36+
- **Fix: MOD charge/discharge SOC controls on DO1.0 firmware (#343):**
37+
Added holding registers 3048 (`batt_first_charge_stopped_soc`) and 3067
38+
(`grid_first_discharge_stopped_soc`) to the MOD 6000-15000TL3-XH profile. Registers
39+
1091 and 1071 are dead on DO1.0 firmware — writes are accepted but have no effect,
40+
and the whole 1060-1099 range reads back zeros. The 3000-range equivalents work
41+
correctly. Confirmed by @Rohde2026 and @TimOsth.
42+
43+
- Per-block "Successfully read N registers" logging moved from INFO to DEBUG. It fired on
44+
every register block of every poll, burying genuine warnings in the HA log.
45+
46+
---
47+
748
## v1.1.0
849

950
Issues: #322

custom_components/growatt_modbus/growatt_modbus.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -963,7 +963,7 @@ def read_input_registers(self, start_address: int, count: int, log_errors: bool
963963
)
964964
self._track_read_failure()
965965
return None
966-
logger.info("Successfully read %d registers from %d", len(registers), start_address)
966+
logger.debug("Successfully read %d registers from %d", len(registers), start_address)
967967
self._track_read_success()
968968
return registers
969969

@@ -1346,12 +1346,23 @@ def _read_sparse(addr_list: list, fatal: bool = False) -> bool:
13461346
_3000_key = ('3000_block', count_3000)
13471347
_3000_RETRY_S = 300
13481348

1349+
# The 3000 range is "optional" only for profiles that ALSO read a base range
1350+
# (0-874). For MIN/MOD-family profiles it is the only input range defined, so a
1351+
# failure here means the poll produced no data at all. Suppressing retries in
1352+
# that case leaves _register_cache empty for 5 minutes, every field below decodes
1353+
# to 0, and the coordinator publishes an all-zero reading as if the inverter were
1354+
# healthy — sensors read 0 instead of going unavailable, and none of the
1355+
# reconnect/backoff paths run because the poll "succeeded" (Issue: overnight
1356+
# Waiting->Normal transition never picked up until a manual reload).
1357+
_3000_is_primary = not has_base_range
1358+
13491359
# Skip-on-failure: after the first failure, suppress retries for 5 minutes.
13501360
# This prevents log flooding when an inverter simply doesn't support this range
13511361
# (e.g. one model in a two-inverter setup where only one uses 3000-range registers).
1362+
# Never applied when the range is primary — see above.
13521363
_3000_prev = self._failed_optional_ranges.get(_3000_key)
13531364
_3000_skip = False
1354-
if _3000_prev:
1365+
if _3000_prev and not _3000_is_primary:
13551366
_3000_fail_time, _3000_fail_count = _3000_prev
13561367
if time.time() - _3000_fail_time < _3000_RETRY_S:
13571368
_3000_skip = True
@@ -1422,6 +1433,18 @@ def _read_sparse(addr_list: list, fatal: bool = False) -> bool:
14221433
self._register_cache[3000 + i] = value
14231434

14241435
# Update failure tracking based on outcome
1436+
if _3000_any_fail and not _3000_any_ok and _3000_is_primary:
1437+
# Primary range failed completely — there is no data to report. Return
1438+
# None so the coordinator marks the inverter offline, increments its
1439+
# failure counter and runs its reconnect/backoff path, rather than
1440+
# publishing zeros as a healthy reading.
1441+
logger.warning(
1442+
"Failed to read primary 3000 register block (3000-%d) — "
1443+
"reporting poll as failed",
1444+
max_3000_addr
1445+
)
1446+
return None
1447+
14251448
if _3000_any_fail and not _3000_any_ok:
14261449
_prev = self._failed_optional_ranges.get(_3000_key)
14271450
_count = (_prev[1] + 1) if _prev else 1
@@ -1555,6 +1578,19 @@ def _read_sparse(addr_list: list, fatal: bool = False) -> bool:
15551578
for i, value in enumerate(registers):
15561579
self._register_cache[min_addr_block + i] = value
15571580

1581+
# Safety net: if every range failed or was suppressed the cache is empty and every
1582+
# field below would decode to 0. Returning that as valid data makes the coordinator
1583+
# read a dead link as a healthy inverter reporting zeros — entities stay "available"
1584+
# showing 0, _consecutive_failures never increments, and no reconnect or adaptive
1585+
# backoff ever runs, so the state persists until a manual reload.
1586+
if not self._register_cache:
1587+
logger.warning(
1588+
"[%s@%s] No registers read this poll (all ranges failed or suppressed) — "
1589+
"reporting poll as failed",
1590+
self.register_map['name'], self.connection_id
1591+
)
1592+
return None
1593+
15581594
# Now extract values using the register map
15591595
try:
15601596
# Status — look up by name rather than assuming it's at min_addr

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.1.1"
1616
}

0 commit comments

Comments
 (0)