Skip to content

Commit 25e5844

Browse files
0xAHAclaude
andcommitted
Bump version to v1.1.9 — TL-XH profiles were unrenderable in the options flow (#361)
Reported by Richardmarkink: opening Configure to change the scan interval failed with "value must be one of [...]" and no option could be saved. Auto-detection assigns tl_xh_3000_10000_v201 for DTC 5100, but that profile had no PROFILE_DISPLAY_NAMES entry. The options form resolves the stored profile ID to a display name to pre-select it; with no entry, get_display_name_for_profile() falls through to the profile's technical `name`, which is not a key of available_profiles, so vol.In() rejected the default and the whole form became unsavable. Every option was unreachable — not just the profile selector. Four profiles affected, all reachable via auto-detection: tl_xh_3000_10000, tl_xh_us_3000_10000, tl_xh_3000_10000_v201, tl_xh_us_3000_10000_v201. Added two dropdown entries covering them: "TL-XH (3-10kW)" and "TL-XH US (3-10kW)". This is the same defect as the missing SPA entry fixed in v1.1.6, so two guards are added rather than just the data: - device_profiles.py asserts at import that every INVERTER_PROFILES key is reachable from PROFILE_DISPLAY_NAMES via 'base' or 'v201', and logs a warning naming any that are not. Audited: 32/32 profiles now reachable, 0 orphans. - config_flow.py detects an unrenderable default and falls back to a valid one with a warning instead of building a form that cannot be submitted. The stored profile is untouched — only the form's pre-selected value changes. Also diagnosed but NOT fixed here: solar reads zero on MIN TL-XH2 because MIN_TL_XH_3000_10000_V201 unpacks VPP_V201_STATUS and VPP_V201_BATTERY2 but not VPP_V201_PV2_INPUT (31010-31017), so PV is sourced from 3000-range registers the hardware does not serve. Battery works because that profile's cluster-1 battery registers are inline at 31200+, which does respond. Needs the dedicated TL-XH2 profile. Battery register mapping now confirmed against the Growatt app by Richardmarkink: 31217 = SOC (75%) and 31214 = battery voltage both match. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 924d6ad commit 25e5844

5 files changed

Lines changed: 89 additions & 2 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.8-blue.svg)
6+
![Version](https://img.shields.io/badge/Version-1.1.9-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: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,43 @@
44

55
---
66

7+
## v1.1.9
8+
9+
Issues: #361
10+
11+
- **Fix: auto-detected TL-XH inverters were locked out of the options flow entirely:**
12+
Reported by @Richardmarkink. Opening **Configure** to change something unrelated — scan
13+
interval, for example — failed to save with:
14+
15+
> value must be one of ['MIC (0.6-3.3kW)', … 'WIT (4-15kW)']
16+
17+
Auto-detection assigns `tl_xh_3000_10000_v201` for DTC 5100, but that profile had **no
18+
entry in `PROFILE_DISPLAY_NAMES`**. The options form resolves the stored profile to a
19+
display name to pre-select it; with no entry, the lookup fell through to the profile's
20+
technical `name`, which isn't a valid dropdown key — so validation rejected the form
21+
before any change could be saved. Every option was unreachable.
22+
23+
Four profiles were affected, all reachable via auto-detection:
24+
`tl_xh_3000_10000`, `tl_xh_us_3000_10000`, `tl_xh_3000_10000_v201`,
25+
`tl_xh_us_3000_10000_v201`.
26+
27+
Two new dropdown entries cover them — **TL-XH (3-10kW)** and **TL-XH US (3-10kW)**.
28+
29+
- **Two guards so this cannot recur silently.** This was the same defect as the missing SPA
30+
entry in v1.1.6, so an audit of all 32 profiles now runs at import and logs a warning for
31+
any that no dropdown entry can reach (currently none). Separately, the options flow now
32+
detects an unrenderable default and falls back to a valid one with a warning, rather than
33+
presenting a form that cannot be saved. Your configured profile is not changed by that
34+
fallback — only the value the form pre-selects.
35+
36+
- **Note for MIN TL-XH2 owners — why solar still reads zero.** The `MIN TL-XH (V2.01)`
37+
profile does not include the VPP PV register block (`31010-31017`), so PV is sourced from
38+
the 3000-range registers your hardware does not serve. Battery works because that
39+
profile's battery cluster is read from `31200+`, which does respond. This is not fixed by
40+
a setting — it needs the dedicated TL-XH2 profile tracked in #361.
41+
42+
---
43+
744
## v1.1.8
845

946
Issues: #361

custom_components/growatt_modbus/config_flow.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -900,6 +900,23 @@ async def async_step_init(
900900
# Convert current profile ID to display name for default
901901
current_display_name = get_display_name_for_profile(current_series)
902902

903+
# get_display_name_for_profile() falls back to the profile's technical `name` when
904+
# the profile has no PROFILE_DISPLAY_NAMES entry. That value is not a valid dropdown
905+
# key, so vol.In() below would reject the default and the user could not save ANY
906+
# option change — locked out of scan interval, modbus delay, everything (Issue #361,
907+
# where auto-detection assigned tl_xh_3000_10000_v201 for DTC 5100).
908+
#
909+
# The missing entries are added, but keep this guard: an unrenderable default should
910+
# degrade to "profile shown as something else" rather than a dead options page.
911+
if current_display_name not in available_profiles:
912+
_LOGGER.warning(
913+
"Profile '%s' has no display-name entry (resolved to '%s', which is not a "
914+
"valid option). Falling back to the first available profile for the form "
915+
"default — the configured profile is unchanged unless you select a new one.",
916+
current_series, current_display_name,
917+
)
918+
current_display_name = next(iter(available_profiles), "MIN (7-10kW)")
919+
903920
options_schema = vol.Schema({
904921
vol.Required(
905922
"device_name",

custom_components/growatt_modbus/device_profiles.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -937,7 +937,40 @@
937937
"v201": "spa_3000_6000_tl_bl", # Only one variant
938938
"description": "AC-coupled battery storage, no solar DC inputs",
939939
},
940+
941+
# TL-XH (non-MIN variants)
942+
# Missing from this dict until v1.1.9 (Issue #361). Auto-detection assigns
943+
# tl_xh_3000_10000_v201 for DTC 5100, but with no display-name entry the options flow
944+
# could not render it: get_display_name_for_profile() fell through to the profile's
945+
# technical `name`, which is not a valid dropdown key, so vol.In() rejected the default
946+
# and the user was locked out of changing ANY option ("value must be one of [...]").
947+
"TL-XH (3-10kW)": {
948+
"base": "tl_xh_3000_10000",
949+
"v201": "tl_xh_3000_10000_v201",
950+
"description": "Single-phase hybrid with battery, legacy 0-124 + VPP ranges",
951+
},
952+
"TL-XH US (3-10kW)": {
953+
"base": "tl_xh_us_3000_10000",
954+
"v201": "tl_xh_us_3000_10000_v201",
955+
"description": "US single-phase hybrid with battery (split-phase)",
956+
},
957+
}
958+
959+
# Every INVERTER_PROFILES key MUST be reachable from PROFILE_DISPLAY_NAMES via 'base' or
960+
# 'v201'. A profile that isn't cannot be rendered by the options flow — see the TL-XH note
961+
# above. This has bitten twice (SPA in #360, TL-XH in #361), so assert it at import time
962+
# rather than waiting for a user to hit it.
963+
_ORPHANED_PROFILES = {
964+
pid for pid in INVERTER_PROFILES
965+
if not any(pid in (info["base"], info["v201"]) for info in PROFILE_DISPLAY_NAMES.values())
940966
}
967+
if _ORPHANED_PROFILES: # pragma: no cover — guards a developer error, not runtime state
968+
import logging as _logging
969+
_logging.getLogger(__name__).warning(
970+
"Profiles with no PROFILE_DISPLAY_NAMES entry — these cannot be selected or "
971+
"reconfigured in the options flow: %s",
972+
", ".join(sorted(_ORPHANED_PROFILES)),
973+
)
941974

942975

943976
def resolve_profile_alias(series: str) -> str:

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.8"
15+
"version": "1.1.9"
1616
}

0 commit comments

Comments
 (0)