Skip to content

Commit 0ada98b

Browse files
0xAHAclaude
andcommitted
Bump version to v1.2.1 — add MIN TL-XH2 profile (#361)
The TL-XH2 serves only the VPP ranges: legacy 0-124, storage 1000-1124 and the whole 3000+ block return Illegal Function. MIN_TL_XH_3000_10000_V201 is therefore structurally wrong for this hardware — it sources PV and battery from 3000-range addresses that do not exist, so those entities stay empty even while the inverter responds normally on 31000+. New profile MIN_TL_XH2_3000_10000_V201, selectable as "MIN TL-XH2 (3-10kW)". Manual selection only: it shares DTC 5100 with the first generation, so auto-detection cannot distinguish them. Every mapping was verified against the Growatt portal by Richardmarkink on a MIN 4200TL-XH2, not inferred from the V2.01 spec: 31011 / 31013 portal MPPT1 6.3 A, MPPT2 6.7 A 31109 portal grid current L1 8.3 A 31214 / 31217 app battery voltage and SOC 31058/31059 total PV power — confirmed on two scans an order of magnitude apart: 773.5 W vs 747 W computed, 2854.7 W vs 2843 W computed Two things that would have been wrong had this been built from the shared blocks: - The PV block does NOT match VPP_V201_PV2_INPUT, which defines 31012/31013 as PV1 power high/low. On TL-XH2 they are PV2 voltage and current. Unpacking that block would have reported PV2 voltage as PV1 power — plausible-looking and silently wrong. PV registers are defined inline, with a comment warning against "simplifying" them back to the shared block. - The battery registers are deliberately unsuffixed. The first-gen profile names them battery_soc_vpp etc. specifically to block fallback to its 3000-range equivalents; TL-XH2 has no 3000 range, so these ARE the battery sensors and the suffixes would have left every battery entity empty. PV3 (31014/31015) is included despite reading zero on the 4200, which is a two-string model — the 7-10kW variants have three strings and omitting it would silently lose a string on every larger unit. Verified by AST inspection: 27 inline registers, all >= 31000; zero base, storage or 3000-range addresses; registry linkage resolves; no accidental _vpp suffixes. Profile audit still reports 33/33 reachable, 0 orphans. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 6957143 commit 0ada98b

5 files changed

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

55
---
66

7+
## v1.2.1
8+
9+
Issues: #361
10+
11+
- **New profile: MIN TL-XH2 (3-10kW)** — for second-generation TL-XH inverters.
12+
13+
The TL-XH2 serves **only** the VPP ranges. Legacy 0-124, storage 1000-1124 and the whole
14+
3000+ block all return `Illegal Function`. The existing MIN TL-XH profile is therefore
15+
structurally wrong for this hardware: it sources PV and battery from 3000-range addresses
16+
that don't exist, so those entities stay empty even when the inverter is responding
17+
normally.
18+
19+
Select **MIN TL-XH2 (3-10kW)** manually via *Configure → Inverter Series*. It shares
20+
DTC 5100 with the first generation, so auto-detection cannot tell them apart — if your
21+
logs show `Illegal Function` on the 3000-range registers, this is your profile.
22+
23+
**Every mapping was verified against the Growatt portal** by @Richardmarkink on a MIN
24+
4200TL-XH2, rather than inferred from the V2.01 specification:
25+
26+
| Register | Confirmed against |
27+
|---|---|
28+
| 31011 / 31013 | portal MPPT1 6.3 A, MPPT2 6.7 A |
29+
| 31109 | portal grid current L1 8.3 A |
30+
| 31214 / 31217 | app battery voltage and SOC |
31+
| 31058/31059 | total PV power — two scans an order of magnitude apart, 773.5 W vs 747 W computed and 2854.7 W vs 2843 W computed |
32+
33+
Two things that would have been wrong had this been built from the shared V2.01 blocks:
34+
35+
- **The PV block does not match `VPP_V201_PV2_INPUT`.** That block defines 31012/31013 as
36+
PV1 *power* high/low; on TL-XH2 they are PV2 voltage and current. Unpacking it would
37+
have reported PV2 voltage as PV1 power — a plausible-looking value that would have been
38+
silently wrong. The PV registers are defined inline for that reason.
39+
- **The battery registers are deliberately unsuffixed.** The first-gen profile names them
40+
`battery_soc_vpp` and similar to stop them being used as fallbacks for its 3000-range
41+
equivalents. TL-XH2 has no 3000 range, so these *are* the battery sensors — keeping the
42+
suffixes would have left every battery entity empty.
43+
44+
PV3 (31014/31015) is included even though it reads zero on the 4200, which is a two-string
45+
model. The 7-10 kW variants have three strings, and omitting it would silently lose a
46+
string on every larger unit.
47+
48+
---
49+
750
## v1.2.0
851

952
Issues: #360

custom_components/growatt_modbus/device_profiles.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,29 @@
405405
},
406406

407407
# MIN TL-XH Hybrid - Uses MIN 3000+ range with VPP battery
408+
# Second-generation TL-XH. Serves ONLY the VPP ranges — legacy 0-124, 1000-1124 and
409+
# the whole 3000+ block return Illegal Function, so the first-gen profile below reads
410+
# battery and PV from addresses that do not exist on this hardware (Issue #361).
411+
# Shares DTC 5100 with the first generation, so auto-detection cannot tell them apart:
412+
# this is a manual-selection profile.
413+
"min_tl_xh2_3000_10000_v201": {
414+
"name": "MIN TL-XH2 3000-10000",
415+
"description": "MIN series TL-XH2 hybrid with battery (3-10kW), VPP-only (30000+/31000+)",
416+
"register_map": "MIN_TL_XH2_3000_10000_V201",
417+
"phases": 1,
418+
"has_pv3": True, # 3-6kW: 2 strings, 7-10kW: 3 strings
419+
"has_battery": True,
420+
"max_power_kw": 10.0,
421+
"protocol_version": "v2.01",
422+
"sensors": (
423+
BASIC_PV_SENSORS |
424+
PV3_SENSORS |
425+
BASIC_AC_SENSORS |
426+
BATTERY_SENSORS |
427+
STATUS_SENSORS
428+
),
429+
},
430+
408431
"min_tl_xh_3000_10000_v201": {
409432
"name": "MIN TL-XH 3000-10000",
410433
"description": "MIN series TL-XH hybrid with battery (3-10kW) using 3000+ and 31000+ ranges",
@@ -954,6 +977,14 @@
954977
"v201": "tl_xh_us_3000_10000_v201",
955978
"description": "US single-phase hybrid with battery (split-phase)",
956979
},
980+
# Second-generation TL-XH — shares DTC 5100 with the first generation, so
981+
# auto-detection cannot distinguish them. Select this manually if your legacy and
982+
# 3000-range registers return Illegal Function (Issue #361).
983+
"MIN TL-XH2 (3-10kW)": {
984+
"base": "min_tl_xh2_3000_10000_v201",
985+
"v201": "min_tl_xh2_3000_10000_v201", # VPP-only; no legacy variant exists
986+
"description": "Second-gen MIN TL-XH2 hybrid, VPP registers only",
987+
},
957988
}
958989

959990
# Every INVERTER_PROFILES key MUST be reachable from PROFILE_DISPLAY_NAMES via 'base' or

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

custom_components/growatt_modbus/profiles/tl_xh.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,11 +458,119 @@
458458
}
459459
}
460460

461+
# ============================================================================
462+
# MIN TL-XH2 — second-generation, VPP-only (Issue #361)
463+
# ============================================================================
464+
#
465+
# The TL-XH2 serves ONLY the VPP ranges. Every legacy range — base 0-124, storage
466+
# 1000-1124 and the whole 3000+ block — returns Modbus "Illegal Function". The
467+
# first-generation MIN_TL_XH_3000_10000_V201 profile above is therefore unusable on
468+
# this hardware: its PV and battery sensors read from 3000-range addresses that do
469+
# not exist, and it carries three legacy registers (91, 92, 97) that made the poll
470+
# abort entirely before v1.1.8.
471+
#
472+
# Mapping verified against the Growatt portal by Richardmarkink on a MIN 4200TL-XH2
473+
# (DTC 5100, protocol V2.01), rather than assumed from the V2.01 spec blocks:
474+
#
475+
# 31011 = 6.3 A portal MPPT1 current 6.3 A
476+
# 31013 = 6.9 A portal MPPT2 current 6.7 A
477+
# 31109 = 8.3 A portal grid current L1 8.3 A
478+
# 31214 = 404.3 V matched the app's battery voltage
479+
# 31217 = 75 % matched the app's battery SOC
480+
# 31059 total PV power, confirmed on two scans an order of magnitude
481+
# apart: 773.5 W vs 747 W computed, 2854.7 W vs 2843 W computed
482+
#
483+
# NOTE ON THE PV BLOCK: this does NOT match VPP_V201_PV2_INPUT, which defines
484+
# 31012/31013 as PV1 power high/low. On TL-XH2 they are PV2 voltage and current.
485+
# Unpacking that shared block here would report PV2 voltage as PV1 power — a
486+
# plausible-looking value that would be silently wrong. Defined inline for that
487+
# reason; do not "simplify" it back to **VPP_V201_PV2_INPUT.
488+
#
489+
# PV3 (31014/31015) reads zero on the 4200 because it is a two-string model. The
490+
# 7-10 kW variants have three strings, so it is defined here rather than omitted —
491+
# otherwise every larger unit would silently lose a string.
492+
MIN_TL_XH2_3000_10000_V201 = {
493+
'name': 'MIN TL-XH2 3000-10000',
494+
'description': 'MIN series TL-XH2 hybrid with battery (3-10kW), VPP-only (30000+/31000+)',
495+
'notes': (
496+
'Second-generation TL-XH. Serves ONLY the VPP ranges — legacy 0-124, 1000-1124 '
497+
'and 3000+ all return Illegal Function. PV block layout differs from '
498+
'VPP_V201_PV2_INPUT and is defined inline. Verified against the Growatt portal '
499+
'on a MIN 4200TL-XH2 (Issue #361).'
500+
),
501+
'input_registers': {
502+
# === Status (31000-31004) ===
503+
**VPP_V201_STATUS,
504+
505+
# === PV strings — inline, NOT VPP_V201_PV2_INPUT (see note above) ===
506+
31010: {'name': 'pv1_voltage', 'scale': 0.1, 'unit': 'V', 'desc': 'PV1 DC voltage'},
507+
31011: {'name': 'pv1_current', 'scale': 0.1, 'unit': 'A', 'desc': 'PV1 DC current'},
508+
31012: {'name': 'pv2_voltage', 'scale': 0.1, 'unit': 'V', 'desc': 'PV2 DC voltage'},
509+
31013: {'name': 'pv2_current', 'scale': 0.1, 'unit': 'A', 'desc': 'PV2 DC current'},
510+
31014: {'name': 'pv3_voltage', 'scale': 0.1, 'unit': 'V',
511+
'desc': 'PV3 DC voltage (7-10kW models only; 0 on 2-string units)'},
512+
31015: {'name': 'pv3_current', 'scale': 0.1, 'unit': 'A',
513+
'desc': 'PV3 DC current (7-10kW models only; 0 on 2-string units)'},
514+
515+
# === Total PV power (32-bit) ===
516+
# 31058 reads 0 while 31059 carries the value, consistent with a high/low pair.
517+
# Treated as 32-bit so output above 6.5 kW cannot overflow a single register.
518+
31058: {'name': 'pv_total_power_high', 'scale': 1, 'unit': '', 'pair': 31059,
519+
'desc': 'Total PV power HIGH word'},
520+
31059: {'name': 'pv_total_power_low', 'scale': 1, 'unit': '', 'pair': 31058,
521+
'combined_scale': 0.1, 'combined_unit': 'W',
522+
'desc': 'Total PV power LOW word (confirmed on two scans, Issue #361)'},
523+
524+
# === AC output / grid ===
525+
31100: {'name': 'ac_power_high', 'scale': 1, 'unit': '', 'pair': 31101,
526+
'desc': 'AC output power HIGH word'},
527+
31101: {'name': 'ac_power_low', 'scale': 1, 'unit': '', 'pair': 31100,
528+
'combined_scale': 0.1, 'combined_unit': 'W',
529+
'desc': 'AC output power LOW word'},
530+
31105: {'name': 'ac_frequency', 'scale': 0.01, 'unit': 'Hz', 'desc': 'Grid frequency'},
531+
31106: {'name': 'ac_voltage', 'scale': 0.1, 'unit': 'V', 'desc': 'Grid voltage'},
532+
31109: {'name': 'ac_current', 'scale': 0.1, 'unit': 'A', 'desc': 'Grid current'},
533+
534+
# === Battery (cluster 1) ===
535+
# Unsuffixed on purpose. The first-gen profile names these *_vpp to stop them
536+
# being used as fallbacks for its 3000-range battery registers. TL-XH2 has no
537+
# 3000 range, so these ARE the battery sensors — suffixing them here would
538+
# leave every battery entity empty.
539+
31200: {'name': 'battery_power_high', 'scale': 1, 'unit': '', 'pair': 31201},
540+
31201: {'name': 'battery_power_low', 'scale': 1, 'unit': '', 'pair': 31200,
541+
'combined_scale': 0.1, 'combined_unit': 'W', 'signed': True},
542+
31202: {'name': 'charge_energy_today_high', 'scale': 1, 'unit': '', 'pair': 31203},
543+
31203: {'name': 'charge_energy_today_low', 'scale': 1, 'unit': '', 'pair': 31202,
544+
'combined_scale': 0.1, 'combined_unit': 'kWh'},
545+
31204: {'name': 'charge_power_high', 'scale': 1, 'unit': '', 'pair': 31205},
546+
31205: {'name': 'charge_power_low', 'scale': 1, 'unit': '', 'pair': 31204,
547+
'combined_scale': 0.1, 'combined_unit': 'W', 'signed': True},
548+
31206: {'name': 'discharge_energy_today_high', 'scale': 1, 'unit': '', 'pair': 31207},
549+
31207: {'name': 'discharge_energy_today_low', 'scale': 1, 'unit': '', 'pair': 31206,
550+
'combined_scale': 0.1, 'combined_unit': 'kWh'},
551+
31208: {'name': 'discharge_power_high', 'scale': 1, 'unit': '', 'pair': 31209},
552+
31209: {'name': 'discharge_power_low', 'scale': 1, 'unit': '', 'pair': 31208,
553+
'combined_scale': 0.1, 'combined_unit': 'W', 'signed': True},
554+
31214: {'name': 'battery_voltage', 'scale': 0.1, 'unit': 'V', 'signed': True,
555+
'desc': 'Battery voltage (verified against app, Issue #361)'},
556+
31215: {'name': 'battery_current', 'scale': 0.1, 'unit': 'A', 'signed': True,
557+
'desc': 'Battery current'},
558+
31217: {'name': 'battery_soc', 'scale': 1, 'unit': '%',
559+
'desc': 'Battery SOC (verified against app, Issue #361)'},
560+
31222: {'name': 'battery_temp', 'scale': 0.1, 'unit': '°C', 'signed': True,
561+
'desc': 'Battery temperature'},
562+
},
563+
'holding_registers': {
564+
**VPP_V201_HOLDING_1P,
565+
},
566+
}
567+
461568
# Export all TL-XH profiles
462569
TL_XH_REGISTER_MAPS = {
463570
'TL_XH_3000_10000': TL_XH_3000_10000,
464571
'TL_XH_US_3000_10000': TL_XH_US_3000_10000,
465572
'TL_XH_3000_10000_V201': TL_XH_3000_10000_V201,
466573
'TL_XH_US_3000_10000_V201': TL_XH_US_3000_10000_V201,
467574
'MIN_TL_XH_3000_10000_V201': MIN_TL_XH_3000_10000_V201,
575+
'MIN_TL_XH2_3000_10000_V201': MIN_TL_XH2_3000_10000_V201,
468576
}

0 commit comments

Comments
 (0)